How to share Firebase Authentication across subdomains
Tags: firebase
Published: 2019-03-09 01:24:53

---
title: How to share Firebase Authentication across subdomains
published: true
description: Ensure that users only need to sign into your app once.
tags: firebase
---

*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](https://en.wikipedia.org/wiki/Cross-site_scripting).

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`](https://github.com/auth0/node-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](https://stackoverflow.com/a/51461847/5490505) 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](https://firebase.google.com/docs/hosting/functions#using_cookies) (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](https://firebase.google.com/docs/functions/local-emulator).
   - 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](https://firebase.google.com/docs/functions/http-events) rather than an `onCall` Firebase Function. You'll also need to handle [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/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](https://stackoverflow.com/a/55010828/5490505) 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](https://firebase.google.com/docs/auth/web/custom-auth) 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](https://firebase.google.com/docs/auth/web/auth-state-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](https://dev.to/brianburton/cross-domain-firebase-authentication-a-simple-approach-337k). 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:
   - [https://dev.to/johncarroll/surprisingly-simple-method-to-increase-firestore-performance-reduce-cost-2gcg](https://dev.to/johncarroll/surprisingly-simple-method-to-increase-firestore-performance-reduce-cost-2gcg)