If you're reading this you've probably just discovered that Firebase Auth only authenticates for a single domain, yet you need to share that authentication across domains and subdomains and not sure where to start.
I was in the same boat and discovered one developer's approach that helped to point me in the right direction. Here is a simpler approach that's just as secure but with less plumbing and allows the user to authenticate through any of your subdomains.
How Does it Work?
I. Initial Authentication
First the user authenticates using Firebase Auth to obtain an ID Token on the client side on any subdomain. For this example the user authenticates through
app1.domain.com
.The user then sends that ID Token via POST to the Cloud Functions endpoint
/auth/login
on the same subdomain,app1.domain.com
.The Firebase Hosted website rewrites
/auth
to theAuthFunction
Cloud Function.AuthFunction
takes the ID token, verifies it usingverifyIdToken()
and then callscreateSessionCookie()
and assigns that value to the__session
cookie with the domain.domain.com
giving it access to all of the subdomains of the requesting domain. The cookie should havehttpOnly=true
,secure=true
andsameSite=strict
set.
Note: __session
is the only cookie name that Firebase Hosting allows you to use. Any other cookies get stripped. Source
II. Cross-Domain Authentication
Assume the user is now authenticated on
app1.domain.com
but we need the user to be authenticated onapp2.domain.com
. First the user checks if it's authenticated with Firebase Auth client side, then it makes aGET
request tohttps://app2.domain.com/auth/status
.The Firebase Hosted website rewrites
/auth
to the communalAuthFunction
Cloud Function.The
/auth/status
endpoint checks for the existence of the__session
cookie, then validates the session cookie value withverifySessionCookie()
. If valid it callscreateCustomToken(<uid>)
and returns the custom token to the client. If not it returns a 401 error and clears the__session
cookie.If a
200
status code is returned, the client takes the custom token and calls Firebase'ssignInWithCustomToken()
passing the custom token. Now the user is authenticated onapp2.domain.com
.
If a401
status code is returned, the client logs out through Firebase Auth and is sent to the login page.
Destroying a Session
Finally when a user logs out, the /auth/logout
endpoint should be called to perform two actions:
Clear the
__session
coookie.Optionally call
revokeRefreshTokens(<uid>)
to revoke all tokens for that user across all devices. Otherwise if you want to revoke authentication on a single device you'll need to monitor for the presence of the__session
cookie and if it vanishes the user should be logged out using Firebase Auth. The latter requires that the cookie'shttpOnly
property is set tofalse
.
To be continued with code examples.
Top comments (19)
We've built a version based on the original post by John Carrol you link above and are struggling with complex logic that is periodically causing logouts for new users. We are using the __sesion token but also making use of the suggested CSRF cookie. Are you aware of any security implications to you having omitted that step recommended by John Carrol in his linked post? I would love to simplify the logic we are using and remove additional steps if they are not required.
John's approach is solid and if implemented correctly should be secure. The above approach I'd call a refinement. The primary improvements here are using a single Firebase session cookie across all domains for stateless JWT authentication and no cross-domain requests.
The CSRF protections should be implemented no matter what, I didn't include that because it seemed out of scope however just making an
httpOnly
__session
cookie and strict a strict single-domain CORS policy on the/auth/*
endpoints would make any XSS attack difficult. The only change I would recommend from his approach would be to pass the CSRF token with a custom HTTP header and not a cookie, but that's splitting hairs.Also if you're not using Firebase session cookies that may be the cause of your users getting randomly logged out.
Thanks for following this up. I'll double check the __session cookie we are assigning, it could very well be that it's not the Firebase session cookie.
Interesting. But wouldn't calling
revokeRefreshTokens(<uid>)
sign the user out of every browser and every device? Not just the browser/device they are trying to sign out of?Yes unfortunately it's a nuke 'em all approach, but alternatively the client could monitor the presence of the
__session
cookie and log the user out client side on each subdomain if it's missing.Updated the last step to show how to revoke a user's authentication across all devices or just the current device.
Interesting approach. At step (I.2) do you suggest POSTing the ID Token inside the http body to app1.domain.com/auth/login? If so, how is it different from POSTing it to a http cloud function directly (possibly hosted on auth.domain.com)?
For your first question, yes I POST the ID token inside the body to
/auth/login
.For your second question, the
/auth/login
endpoint checks and sets a session cookie for that domain, so it needs to be on the same domain. Another minor benefit is that it also avoids potential CORS errors.This looks really great but I understand that with
createSessionCookie()
the maximum expiresIn value you can set is two weeks? That means that users would have to sign again every two weeks correct? It's too bad they don't allow for longer cookies.Perhaps one could do this to extend the cookie, but that would only work if the user is using one of the subdomains more than once every two weeks. 🤔
When the user hits
/auth/status
you can always refresh the token and update the session cookie.Hi! Very interesting post, I have been trying to implement this with 2 Firebase apps from the same project, I am pretty sure that it'll work in production (if both apps run on the same domain) but I am having a lot of problems getting this to run locally. Modern cookie setting for Chrome and most browsers prevent cross-site cookies from going through insecure connections, as of today, there is no way to run the emulators on HTTPS. I tried using self-signed certificates on localhost but the browsers won't allow HTTPS even with that (even with chrome://flags/#allow-insecure-localhost set to enabled in Chrome). At the end all this means that app 1 is setting the cookie, but the browser is never sending it to app 2. Any suggestions? Did you ever got this to work locally?
For local dev it may be easier to add some code to determine if it's running locally and then just faking the session. Perhaps you could set up a reverse proxy in front with nginx. Not sure if it'll work with the firebase local environment but worth a shot.
Are there any code examples for this?
No I never put together a proof of concept example, but honestly I didn't expect so many requests for an example
But why do we need to create custom token, can't we simply validate the requests using verifySessionCookie() each time we get a request?
Two tokens for two different purposes. The custom token that you generate is for client-side authentication using the firebase library in the browser. You can authenticate the visitor using the session cookie for server-side requests, but the firebase client-side library in the browser won't know who the person is.
If you add simple Code to this Idea .. I will be appreciated :)
Thanks for the example. I was wondering if it works for separate domains like domain1.com and domain2.com or if it only works for sub-domains?
For multiple TLDs you'd need use a SAML-style flow.
Have you cooked up any code examples?