Problem
I was having issues implementing SAML single sign-on (SSO) authentication with Firebase on our NextJS web app hosted on Vercel.
We wanted to use signInWithRedirect()
(apparently this happens with signInWithPopup()
too), however we were running into two issues:
- When checking
getRedirectResult()
, I would get anull
user - The app would display an error
missing initial state
Error:
Unable to process request due to missing initial state. This may happen if browser sessionStorage is inaccessible or accidentally cleared.
Complications: Browser Security
Modern web browser have security features that block third-party storage access (aka block third-party cookies) to protect user privacy by stopping third-party content providers and advertisers from tracking users across websites.
(End users could manually disabling security settings like "Prevent Cross-site Tracking", however that's not really a reasonable solution).
Firebase Auth and Vercel
The Firebase Authentication SDK relies on third-party browser storage access for its authentication process which roughly follows these steps:
- Firebase opens an iframe to an intermediary page hosted under
<authDomain>/__/auth/iframe
<authDomain>/__/auth/iframe
redirects to the Identity Provider's (IdP) login page- On login success, the IdP redirects the user to the ACS URL
<authDomain>/__/auth/handler
storing the auth result in query params - On
<authDomain>/__/auth/handler
, the auth result is stored in browser storage underauthDomain
- The iframe is closed and returned redirected back to the app
- The app reads the auth result from the
authDomain
's browser storage
On a Firebase Hosted app, the authDomain
is the same domain as the hostind domain: <projectId>.firebaseapp.com
. In this case, the browser has no issues with cross-origin browser storage access.
However, on our Vercel hosted app, the Firebase SDK authDomain
was still pointing cross-origin to <projectId>.firebaseapp.com
and the browser prevents our app from cross-domain storage access needed to complete the authentication process.
Solution:
The Firebase team has an article listing the different approaches to resolving this issue: Best practices for using signInWithRedirect on browsers that block third-party storage access.
For our app hosted on Vercel, we utilized a proxy to redirect auth requests to firebaseapp.com:
- Update the SAML provider's redirect ACS URL to point to our custom domain
redirect_uri ACS URL: https://myapp.com/__/auth/handler
- Configure the Firebase SDK
authDomain
to point to our custom domain:myapp.com
import { initializeApp } from 'firebase/app';
const firebaseConfig = {
authDomain: 'myapp.com',
...
};
export const firebaseApp = initializeApp(firebaseConfig);
- Use a NextJS rewrite to proxy any requests to
myapp.com/__/auth/*
to<projectId>.firebaseapp.com/__/auth/*
module.exports = {
async rewrites() {
return [
{
source: "/__/auth/:path*",
destination: `https://<projectId>.firebaseapp.com/__/auth/:path*`,
},
];
},
};
The proxy rewrite masks the destination path of authDomain
, so although <projectId>.firebaseapp.com/__/auth/*
is handling the auth logic with the IdP, it appears to the browser as if <projectId>.firebaseapp.com/__/auth/*
is on the origin as our app, eliminating any cross-origin storage access.
The Proxied Firebase Auth Flow
- The web app calls Firebase method
signInWithRedirect()
signInWithRedirect()
redirects the user tomyapp.com/__/auth/iframe
-
NextJS rewrite reverse-proxies the request
myapp.com/__/auth/*
-><projectId>.firebaseapp.com/__/auth/*
- Firebase initiates the authentication flow with the corresponding IdP
- On successful IdP login, the IdP redirects the user to the ACS URL with the auth result:
myapp.com/__/auth/handler?apiKey=xxxx
-
NextJS rewrite reverse proxies the request
myapp.com/__/auth/*
-><projectId>.firebaseapp.com/__/auth/*
- Firebase receives the auth result and stores the result in browser storage under
myapp.com
- Firebase closes the iframe and redirects the user back to the app
- Browser storage of the auth domain (
myapp.com
) can be accessed