DEV Community

John Carroll
John Carroll

Posted on • Edited on

How to share Firebase Authentication across subdomains

This post is intended for people who are already familiar with Firebase, Firebase Authentication, and who are using Firebase in their web apps.

If you use Firebase Authentication in your web apps, you may have run into the problem that Firebase only supports authentication with a single domain. This means that if your application experience is spread across multiple subdomains, your users must sign in to each subdomain separately. More problematically, your users must sign out of each subdomain separately.

If your applications share branding across subdomains, this could pose a security risk. It might be reasonable for a user to expect signing out of app1.domain.com to also sign them out of app2.domain.com. Many popular applications share signed in status across subdomains, e.g. Google itself.

Having spent much longer then I intended to getting single-sign-in working across subdomains, I'm writing this post so that the next person hopefully has it easier.

At a high level, this is our setup:

  1. We have three applications at different domains.
    • accounts.domain.com
    • app1.domain.com
    • app2.domain.com
  2. We have three Firebase Functions
    • ...cloudfunctions.net/users-signin
    • ...cloudfunctions.net/users-checkAuthStatus
    • ...cloudfunctions.net/users-signout

In order to sign in:

  1. Someone navigates to the accounts.domain.com app
  2. They provide their authentication information
  3. That authentication information is sent to our /users-signin cloud function which verifies the information and, if valid, sets a signed __session cookie which contains the user's UID and returns a success indication to the client.
  4. On success, the client calls the /users-checkAuthStatus cloud function which looks for the signed __session cookie, extracts the user UID, and uses the UID and the firebase-admin SDK to mint a custom auth token which it returns to the client.
  5. When the client receives this custom auth token, it uses it to sign in using the firebase javascript SDK.

When someone navigates to one of the other apps, say app1.domain.com, the app first checks to see if the person is already signed in via Firebase Auth. If not, it calls the /users-checkAuthStatus cloud function which looks for the signed __session cookie and returns a custom auth token to the client if appropriate. The client then signs the user in using the custom auth token (if present).

If a user for app1.domain.com isn't signed in and wants to be, you send them over to accounts.domain.com and then redirect them back to app1.domain.com when sign in is complete.

In order to sign out, a client clears the local auth state by calling signOut() with the firebase-js-sdk and also calls ...cloudfunctions.net/users-signout, which clears the __session cookie. Additionally, the client needs to notify any other connected clients that the user has been signed out so that they can call signOut() using the firebase-js-sdk.

Actually making things work, with security.

That's the high level overview, but in order to actually make it work, we need to deal with some stuff like cross-site-scripting, cookies, handling provider auth, etc.

Signing in

To start off, you need to decide how to verify authentication on the server.

One possibility, is to authenticate someone on the accounts.domain.com client normally (using Firebase Auth), and then send their idToken to the server where you use the admin SDK to verify the ID token, verify the issuedAtTime associated with the ID token (e.g. make sure it was created in the last 5 minutes), and verify the provider associated with the ID token (e.g. make sure it wasn't created using a custom auth token).

Another possibility, if someone is authenticating via a provider like Facebook or Twitter, is to authenticate them using that provider's SDK, retrieve the authToken, and send the authToken to the server where you follow the provider's instructions for verifying the token on the server.

However you accomplish passing credentials to the server, if the server determines that authentication is valid, you need to set a __session cookie, which is equal to the Firebase Authentication UID associated with the user, as well as set a cross-site-scripting cookie, which we'll use to protect against cross-site-scripting attacks.

The __session cookie should be signed, secure (meaning HTTPS only) and httponly (meaning javascript can't access it). The cross-site-scripting cookie, we'll call it csst, should be secure but not signed or httponly. Instead, the csst cookie should be created using the jsonwebtoken library which will sign the token and record the token's subject (i.e. auth UID). The domain associated with both cookies should be your application's root domain (i.e. domain.com). This ensures that the cookies are shared between subdomains.

Browsers do not allow you to set cookies for another domain. This is a problem because, by default, your Firebase Functions are on a different domain from your app. You can follow these instructions to use Firebase Hosting & a custom domain for your Functions. Note also that using Firebase Hosting for your functions means that you are restricted to only reading a __session cookie on the server side (this won't impact our consumption of the csst token). Even if you set up Firebase Functions to use your custom domain though, you might still have trouble during development: in my original setup, I was hosting my app locally but deploying Firebase Functions to a special development firebase project. This meant that the domain associated with the functions was not localhost (meaning that a function couldn't set a cookie that the client could see).

There are two workarounds to this situation that I came up with.

  1. Disable cross-site-scripting checks during development and remove the domain specification from the __session cookie. This works because the __session cookie is only read by the Firebase Functions anyway, so it's OK if the __session cookie isn't shared across subdomains (in this case, the domain associated with the __session cookie will be your Firebase Functions Domain).
  2. Serve your functions locally during development. Firebase has a instructions for using their local-emulator in the docs.
    • The local functions emulator now works with node 8+ A problem with the local emulator is it only works for nodejs 6 (FYI, I found the current version of expressjs doesn't work in nodejs6).
    • This is no longer necessary as the local functions emulator now works with node 8+ Another option is to build your own express app to host your functions during development. This is the route...

In order to set cookies, you'll need to use an onRequest Firebase Function rather than an onCall Firebase Function. You'll also need to handle CORS and all that jazz. I'll also call out that Google Chrome has a very unexpected quirk in that it strips the set-cookie header from the response if the set-cookie header is for a different domain. I hate this quirk. I spent hours thinking the cookie wasn't being set when, in fact, it was. See this S.O. issue for more information. Another FYI, when performing CORS requests you need to specify that the request is being made "with credentials" in order for the cookies to be sent. The server also needs to specify that credentials are allows on CORS requests for the credentials to be received.

Checking auth status

Anyway, so after you set the __session cookie and csst cookies on the client. The client can now call the /users-checkAuthStatus endpoint. When doing so, the client will need to find the csst cookie, extract its token, and set the token in a Authorization: Bearer ${token} header for the request. When receiving the request, the checkAuthStatus endpoint extracts the csst token contained in the Authorization header, validates the signature on the token, and makes sure the token's subject matches the auth UID contained in the signed __session cookie. Assuming everything is valid, you use the firebase-admin SDK to mint a custom auth token and send it to the client. If things are invalid, clear any old __session / csst cookies on the client.

Finally, when the client receives one of these custom auth tokens, make sure the client signs in using SESSION auth persistence. This means that the auth state will be persisted through page refreshes, but the moment that every tab associated with a domain is closed, the auth state will be cleared. Whenever an app is initialized, you'll need to:

  1. Check if the person is already signed in.
  2. If not, call the /users-checkAuthStatus endpoint and, if you receive a custom auth token in response, use it to sign the user in.
    • If you receive nothing, you know the user isn't signed in.

Signing out

When someone signs out, the client needs to call the /users-signout endpoint, which will clear any __session / csst cookies on the client, as well as call the signOut() method of the firebase-js-sdk. Additionally, you need to somehow ping any other apps which are open to let them know of the signout -- at which point they should call the signOut() method of the firebase-js-sdk to sign themselves out. As a reminder, if an app is closed, the firebase-js-sdk's auth state has already been cleared.

In order to ping the other open apps to tell them to signout of the firebase-js-sdk, I found the easiest method is to monitor the presence of the csst cookie. If the csst cookie disappears, you know that the person has signed out and your app should call the signOut() method of the firebase sdk.

Wrapping up

Anyway, this wraps up my overview. Getting Firebase Authentication to work across subdomains is not super straightforward, but it is doable without that much work. Unfortunately, you need to be familiar with a number of concepts such as CORS, cookies, JWTs, Firebase Authentication itself, etc.

Good luck!

Edit (5/14/21)

  1. A few people have asked for examples (i.e. code) of a working setup. Obviously I can understand why this would be really helpful, but I don't have any plans to do this (i.e. I'm not willing to spend the time). If someone reading this puts together an example repo and pings me in a comment, I'll update this post with a link to your repo (and credit you) so that other people can benefit.

  2. Another developer came up with a variation of this approach (which they feel is an improvement) and which you can read about here, Cross-Domain Firebase Authentication: A Simple Approach. I haven't tested this approach at all, so I'm sharing it without endorsing it (but always good to have options, right?).

  3. Unrelated, I recently discovered that you can (pretty easily) implement a simple query cache for Firebase Firestore which increases performance and may reduce costs. You can see an overview here:

Top comments (37)

Collapse
 
romshiri profile image
Rom Shiri

Thanks for this article! Is it possible for Firebase Auth to support any sub-domain? I mean, when you don't know the sub domain ahead of time?

Say you allow users to have unique urls for their profile:
user1.subdomain.com
user2.subdomain.com

Collapse
 
johncarroll profile image
John Carroll

Is it possible for Firebase Auth to support any sub-domain? I mean, when you don't know the sub domain ahead of time?

I'm not sure. I haven't had to deal with subdomain authentication in firebase for a while now (I'm no longer working on the app that sparked this article).

Collapse
 
brianburton profile image
Brian Burton

Thank you for pointing me in the right direction with this post. I came up with a simpler approach that doesn't require csst cookies, let me know what you think:
dev.to/brianburton/cross-domain-fi...

Collapse
 
johncarroll profile image
John Carroll • Edited

Happy you've found this post helpful! I added a comment to your article but I'll repeat it here:

I like the general idea behind your approach, 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?

See this discussion which I had with another user who proposed using revokeRefreshTokens().

Collapse
 
johncarroll profile image
John Carroll

FYI, I added a link to your article to the bottom of mine.

Collapse
 
mikgross profile image
Mikael

Do you know if the following would work?

1) In the auth.domain.com app, authenticate the user and get a token
2) redirect the user to app.domain.com with a parameter that is set to be the token
3) check the token provided by extending the Function service from auth.domain.com project to app.domain.com
4) if valid perform new authentication from app.domain.com and issue new token to user (I guess this would not allow users to have multiple clients openned and working at the same time -- does the shared cookie solves this issue?)

Collapse
 
johncarroll profile image
John Carroll

My guess is no. For one, it sounds like someone would need to re-login each time they navigated to a new subdomain.

Collapse
 
mikgross profile image
Mikael

That's correct.

To circumvent setting a cookie in Functions and the problem of passing them through to the client, couldn't we just pass the cookie ID in the response, and set it at client level? Am I missing a security issue? That would relive me of a big pain...

Thread Thread
 
johncarroll profile image
John Carroll • Edited

Well the entire purpose of this post is to share sign in status between subdomains, and you’re not doing that (in your proposal at least).

Thread Thread
 
mikgross profile image
Mikael

Maybe I am misunderstanding something here or badly expressing myself..

What I am doing atm:
1) Sign-in and get token ID from the client
2) send the token ID to functions, validate token ID, generate session cookie and a jwt token in Functions, return the two values to the client
3) in the client set the two cookies for domain *.domain.com
4) all cookies are available accross all subdomains (tested), so I can perform the authchecks required

Something I am missing?

Thread Thread
 
johncarroll profile image
John Carroll • Edited

That sounds good. Sounds like the same thing this blog post suggests. Honestly though, I'm no security expert. I'm confident enough that following the steps in this post will work and is secure (after having spent a while researching it), but I have no advice if you are looking to do some variation of this.

This post is really just a summary of my findings after spending a week working on this problem. Beyond what's here though, you're on your own.

Collapse
 
eghernqvist profile image
eghernqvist

Fantastic documentation of the flow, got pretty much the entire flow implemented except for the mentioned monitoring of the csst cookie. All implementations of any sort of cookie "event listeners" I find have some setInterval hackiness going on, so I'm curious if you have any pointer as to how you'd monitor the presence of the cookie?

Cheers, again!

Collapse
 
johncarroll profile image
John Carroll

Sorry, I missed this comment when you originally made it. I also monitor the presence of the cookie using setInterval (I don't think there's any other way to do it). Specifically, I use the interval() observable creation function from rxjs.

Collapse
 
skaaptjop profile image
skaaptjop

Thanks. I've been looking for examples that do exactly this!

One question though, for signout, what about just revoking the refresh token? That should log the user out everywhere.

Collapse
 
johncarroll profile image
John Carroll • Edited

I've been there 👍.

That should log the user out everywhere.

"Logging the user out everywhere" sounds like a solution specific to your app. In general, users don't want to log out everywhere. They just want to log out of that browser.

Collapse
 
skaaptjop profile image
skaaptjop

Good point. Revoking the refresh is quite drastic.

Collapse
 
borgiabusiness profile image
Dan Borgia

I love the explanation, and yeah there really isn't any documentation to this at all. It's frustrating.
The only thing that would have made this post better is to showcase some of the code involved to get a better grasp of how this can be done.

Collapse
 
madnik7 profile image
Mohammad Nikravan

I have evaluated Google Identity Platform /Amazon Cognito and Azure B2C. This approach is work but as far as I understood should not be used because it is not secure. Google Identify Platform (Firebase Authentication) does not support multidomain.

Usually we need to sign in by a single credential across all subdomains but it doesn't mean we can use the same access token! It sounds that this approach uses a single access token for all subdomains which is not secure why?

Imagine you have an admin panel and a simple game in your organization such as admin.domain.com and funnygame.domain.com. In this scenario you are giving the admin access to the funny game team because they can use that token to access admin portal. access tokens must have separate audience or scope for each individual subdomain.

Collapse
 
johncarroll profile image
John Carroll • Edited

Hi Mohammad,

You seem to be conflating authentication, which this article discusses, with authorization, which this article does not discuss.

Authentication is the process of determining who someone is. This article helps you do this across subdomains in a secure way.

Authorization is the process of determining if a known or unknown client has access to something. This article does not help you with this. Just because you've authenticated someone and know who they are on every subdomain, does not mean you need to give them access to all (or any) data on each subdomain. You said, "Imagine you have an admin panel and a simple game in your organization such as admin.domain.com and funnygame.domain.com. In this scenario you are giving the admin access to the funny game team because they can use that token to access admin portal." But this is only true if you don't implement authorization. For example, you can (and probably should!) have different Firestore security rules for admin.domain.com and funnygame.domain.com.

If you're not sure how to authorize users within Firebase, you'll need to learn how to do that by reading the Firebase documentation or other articles on the internet. That's out of scope for this article.

Also FYI, this is incorrect:

It sounds that this approach uses a single access token for all subdomains which is not secure

The approach discussed here results in the user having a different Firebase auth token for each subdomain. This is necessary because Firebase Authentication (again, authentication not authorization) doesn't support single sign-in across subdomains. From the article:

When someone navigates to one of the other apps, say app1.domain.com, the app first checks to see if the person is already signed in via Firebase Auth. If not, it calls the /users-checkAuthStatus cloud function which looks for the signed __session cookie and returns a custom auth token to the client if appropriate. The client then signs the user in using the custom auth token (if present).

I'll also point out that having a single authentication token shared between subdomains is not inherently insecure (but obviously it could be depending on the authorization strategy).

PS: I'll reiterate the very first sentence of this article

This post is intended for people who are already familiar with Firebase, Firebase Authentication, and who are using Firebase in their web apps.

Collapse
 
madnik7 profile image
Mohammad Nikravan

Dear John,
Thank you for your complete answer.

You seem to be conflating authentication, which this article discusses, with authorization, which this article does not discuss.

I completely understand the difference between authorization and authentication. Still, it is impossible to authorize an authenticated user if you don't have enough information in its id-token or access token.

The approach discussed here results in the user having a different Firebase auth token for each subdomain. This post is intended for people who are already familiar with Firebase, Firebase Authentication, and who are using Firebase in their web apps.

Yes, Anyone who needs to find the proper tools looks for some key points and possibilities before implementing the whole system. Indeed, Your article works if there is something in authentication tokens so the access-token function can distinguish the domain. However, it sounds like it needs a lot of customization to achieve this, especially when I compare it with AWS Cognito. It would be so helpful if you put a sample of authenticated tokens (JWT) in each step of your article.

To sign-in:

  1. Someone navigates to the accounts.domain.com app! (perhaps redirecting from the subdomain)
  2. They provide their authentication information
  3. That authentication information is sent to our /users-signin cloud function which verifies the information and, if valid, sets a signed __session cookie which contains the user's UID and returns a success indication to the client

Here there is no trace of the audience domain, and I couldn't understand how "/users-checkAuthStatus cloud function" can generate the token and validate the authenticity of the requested subdomain.
Perhaps I need to learn much more and implement it to understand.

Collapse
 
chanhlt12 profile image
chanhlt12

Thank you for your helpful post.
But I don't understand why we need Firebase functions here.
I means why we don't create corresponding endpoints on the accounts.domain.com itself like this:

  • accounts.domain.com/users-signin
  • accounts.domain.com/users-checkAuthStatus
  • accounts.domain.com/users-signout

Could you please explain more on this?

Collapse
 
johncarroll profile image
John Carroll

But I don't understand why we need Firebase functions here.

My approach uses Firebase functions to accomplish the goal. I imagine there are other ways of accomplishing the goal as well.

Collapse
 
colinbrilliantlabs profile image
ColinBrilliantLabs

I've been stuck on this for like a week and I'm hoping you can help. What is the link between firebase's session variables and the front end? I use response.set to set the session cookie, but then when I call another cloud function, request.cookies is null. Am I supposed to store firebase's session variable locally and then pass it back to the cloud or something? If so, any advice on where I can figure that out (I've been searching through EVERYTHING on Google and I'm desperate haha.) As a workaround I thought about storing the uid in a cookie and then minting a login token across subdomains but this is obviously a major security risk because someone could hack in another uid and get access to their account without their credentials.

Here is my sign in server code:
corsMiddleware(request, response, () => {
console.log(request.body);
//admin.auth().
const idToken = request.body.idToken;
const expiresIn = 60 * 60 * 24 * 5 * 1000;
admin.auth().createSessionCookie(idToken, {expiresIn})
.then((sessionCookie) => {
// Set cookie policy for session cookie.
const options = {maxAge: expiresIn, httpOnly: false, secure: false};
response.cookie('__session', sessionCookie, options);
console.log(sessionCookie);
response.end(JSON.stringify({status: 'success'}));
//return response;
}).catch(error=>{
response.status(401).send('UNAUTHORIZED REQUEST!');

    }) 

});
Enter fullscreen mode Exit fullscreen mode

Here is my checkSessionCookie server function:
const sessionCookie = request.cookies.__session || '';
// Verify the session cookie. In this case an additional check is added to detect
// if the user's Firebase session was revoked, user deleted/disabled, etc.
admin.auth().verifySessionCookie(
sessionCookie, true /** checkRevoked */)
.then((decodedClaims) => {
console.log("Got cookie");
console.log(decodedClaims);
//serveContentForUser('/profile', request, response, decodedClaims);
})
.catch(error => {
console.log("No cookie");
// Session cookie is unavailable or invalid. Force user to login.
//res.redirect('/login');
});

Collapse
 
johncarroll profile image
John Carroll

Sorry, I've never used the admin.auth().createSessionCookie() method and I'm not familiar with it.

Collapse
 
bogdan1988 profile image
Bogdan

Hi John

Could you please share how you have implemented firebase auth credentials check in users-signin function?
Did you use frontend SDK and passed uid to backend?
Or just using npm firebase module?
If the second I wonder if it is the right way?
Please see stackoverflow.com/questions/503709...

Thanks

Collapse
 
johncarroll profile image
John Carroll • Edited

Could you please share how you have implemented firebase auth credentials check in users-signin function?

I'm not 100% sure I know what you are asking, but I handled signin this way:

Another possibility, if someone is authenticating via a provider like Facebook or Twitter, is to authenticate them using that provider's SDK, retrieve the authToken, and send the authToken to the server where you follow the provider's instructions for verifying the token on the server

This being said, when I set things up I didn't realize (until later) that the firebase sdk's IdToken contained the provider which issued the token (which is important to prevent someone from using a custom ID token to authenticate again and again). Since it does, I'd probably use this method if I were doing things over again:

One possibility, is to authenticate someone on the accounts.domain.com client normally (using Firebase Auth), and then send their idToken to the server where you use the admin SDK to verify the ID token, verify the issuedAtTime associated with the ID token (e.g. make sure it was created in the last 5 minutes), and verify the provider associated with the ID token (e.g. make sure it wasn't created using a custom auth token).