Colin McDonnell @colinhacks
updated November 20th, 2020
Feb 24, 2021: a previous version of this post contained a bug where a new cookie would be created each time you signed out and back in again. These (identical) cookies would stack up cause problems. If you're having issues, pass the
{ path: '/' }
option tonookies.set
callΒ β details below.
I had a hell of a time figuring out how to get Firebase Auth, Next.js, tokens, cookies, and getServerSideProps
to play nice together. I'm hoping this breakdown will spare you the same suffering!
To jump straight to the sample repo, head to https://github.com/colinhacks/next-firebase-ssr π€
I'm currently building a new app. My goal is to do all data fetching inside getServerSideProps
, just like the bad ole days of HTML templating. There are a lot of reasons for this.
As usual, Dan Abramov says it best:
Weβve reached peak complexity with SPA. The pendulum will swing a bit back to things on the server. But it will be a new take β a hybrid approach with different tradeoffs than before. Obviously Iβm thinking React will be a part of that wave.
β Dan Abramov (@dan_abramov) August 3, 2020
It seems like the SSR-centric approach to building apps with Next.js is gaining steam fast. So I'm extremely confused why the official Next.js with-firebase-authentication example project doesn't demonstrate how to do it! It only demonstrates how to make authenticated POST requests to an API, not authenticate users inside getServerSideProps
.
So below I explain how to use Next.js and Firebase Auth to:
getServerSideProps
getServerSideProps
Let's dive in.
You've probably done this already. Should look something like this:
// firebaseClient.ts
import * as firebase from 'firebase/app';
import 'firebase/auth';
if (typeof window !== 'undefined' && !firebase.apps.length) {
firebase.initializeApp({
apiKey: 'APIKEY',
authDomain: 'myproject-123.firebaseapp.com',
databaseURL: 'https://myproject-123.firebaseio.com',
projectId: 'myproject-123',
storageBucket: 'myproject-123.appspot.com',
messagingSenderId: '123412341234',
appId: '1:1234123412341234:web:1234123421342134d',
});
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);
}
export { firebase };
The
firebase.apps.length
check is a clever way of preventing Next.js from accidentally re-initalizing your SDK when Next.js hot reloads your application!
You've probably done this already too. Check out the Firebase docs for guidance. It'll look something like this:
// firebaseAdmin.ts
import * as firebaseAdmin from 'firebase-admin';
// get this JSON from the Firebase board
// you can also store the values in environment variables
import serviceAccount from './secret.json';
if (!firebaseAdmin.apps.length) {
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert({
privateKey: serviceAccount.private_key,
clientEmail: serviceAccount.client_email,
projectId: serviceAccount.project_id,
}),
databaseURL: 'https://YOUR_PROJECT_ID.firebaseio.com',
});
}
export { firebaseAdmin };
Alternatively you can store the secret.json
file elsewhere on your file system and tell Firebase where you find it using the GOOGLE_APPLICATION_CREDENTIALS
environment variable. For details check out the Firebase Admin setup documentation here.
Now we're getting to the meat and potatoes. We're going to use React Context to implement the authentication logic. Let's start by writing a simple provider:
import { createContext } from 'react';
const AuthContext = createContext<{ user: firebase.User | null }>({
user: null,
});
export function AuthProvider({ children }: any) {
const [user, setUser] = useState<firebase.User | null>(null);
// handle auth logic here...
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
}
In the AuthProvider
component we initialize a user
state and pass it into our React component hierarchy using React Context. If you haven't seen this pattern before, I recommend reading the official docs on Context before continuing.
Now let's add a useEffect
hook that initializes our Firebase authentication listener:
import nookies from 'nookies';
const AuthContext = createContext<{ user: firebase.User | null }>({
user: null,
});
export function AuthProvider({ children }: any) {
const [user, setUser] = useState<firebase.User | null>(null);
useEffect(() => {
return firebase.auth().onIdTokenChanged(async (user) => {
if (!user) {
setUser(null);
nookies.set(undefined, 'token', '', { path: '/' });
} else {
const token = await user.getIdToken();
setUser(user);
nookies.set(undefined, 'token', token, { path: '/' });
}
});
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
}
This is where the magic happens. We're hooking into the firebase.auth().onIdTokenChanged
event listener; if you've never used it, it's identical to onAuthStateChanged
but it also fires when the user's ID token is refreshed.
In the onIdTokenChanged
callback, we check if the user is still signed in. If they are, we set the user with setUser
.
Here's the most important part: we also set a token
cookie that contains the user's ID token. (To accomplish this, I'm using the excellent nookies
package by the great and powerful @maticzav.)
Feb 24, 2021: passing the
{ path: '/' }
option tonookies.set
prevents your app from creating a new token each time this code runs!
Now all outgoing requests β both API requests and page navigations! β will contain the user's ID token as a cookie! There's an example of this further down the page.
A previous version of this post incorrectly assumed that Firebase automatically refreshes the ID token on an hourly basis. As it turns out, that's not true. Firebase only does so if it is maintaining an active connect to Firestore or Realtime Database. If you aren't using one of these, services we need to refresh the tokens ourselves.
Below is the final version of our AuthProvider. Note the new useEffect
hook that automatically force-refreshes the Firebase token every 10 minutes.
import nookies from 'nookies';
const AuthContext = createContext<{ user: firebase.User | null }>({
user: null,
});
export function AuthProvider({ children }: any) {
const [user, setUser] = useState<firebase.User | null>(null);
// listen for token changes
// call setUser and write new token as a cookie
useEffect(() => {
return firebase.auth().onIdTokenChanged(async (user) => {
if (!user) {
setUser(null);
nookies.set(undefined, 'token', '', { path: '/' });
} else {
const token = await user.getIdToken();
setUser(user);
nookies.set(undefined, 'token', token, { path: '/' });
}
});
}, []);
// force refresh the token every 10 minutes
useEffect(() => {
const handle = setInterval(async () => {
const user = firebaseClient.auth().currentUser;
if (user) await user.getIdToken(true);
}, 10 * 60 * 1000);
// clean up setInterval
return () => clearInterval(handle);
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
}
You may be wondering why you need to use React Context at all. Let's consider a more naive approach to writing this hook.
// DONT DO THIS
export const useAuth = () => {
const [user, setUser] = useState<firebase.User | null>(null);
useEffect(() => {
return firebase.auth().onIdTokenChanged(async (user) => {
if (user) {
setUser(user);
} else {
setUser(null);
}
});
}, []);
return { user };
};
This actually works fine. However it would create a new user
state variable and a new listener every time you use the hook. We want to make sure the user
variable is the same everywhere throughout our application to avoid difficult-to-debug synchronization issues. We only want a single reference to the currently signed in user that is shared throughout our app.
The documentation for setting up a Custom App page with Next.js are at https://nextjs.org/docs/advanced-features/custom-app. It's the ideal place to add "wrapper" code that you want to exist on all other pages β like Context providers.
// pages/_app.tsx
import type { AppProps } from 'next/app';
import { AuthProvider } from '../auth';
function App({ Component, pageProps }: AppProps) {
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
}
export default App;
Now that the context and provider is set up, our actual useAuth
hook is dead simple:
export const useAuth = () => {
return useContext(AuthContext);
};
Below is a complete working example of how to implement an authenticated route. If the user isn't properly signed in (e.g. if the token
cookies doesn't exist or if the token verification fails) the user is redirected to the /login
page.
Otherwise, you can confidently fetch and return data belonging to the user!
// /pages/authenticated.tsx
import React from 'react';
import nookies from 'nookies';
import { InferGetServerSidePropsType, GetServerSidePropsContext } from 'next';
import { firebaseAdmin } from '../firebaseAdmin';
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
try {
const cookies = nookies.get(ctx);
const token = await firebaseAdmin.auth().verifyIdToken(cookies.token);
// the user is authenticated!
const { uid, email } = token;
// FETCH STUFF HERE!! π
return {
props: { message: `Your email is ${email} and your UID is ${uid}.` },
};
} catch (err) {
// either the `token` cookie didn't exist
// or token verification failed
// either way: redirect to the login page
ctx.res.writeHead(302, { Location: '/login' });
ctx.res.end();
// `as never` prevents inference issues
// with InferGetServerSidePropsType.
// The props returned here don't matter because we've
// already redirected the user.
return { props: {} as never };
}
};
export default (
props: InferGetServerSidePropsType<typeof getServerSideProps>
) => (
<div>
<p>{props.message}</p>
</div>
);
If you've made it this far you must think the ideas in this post are pretty useful! Consider retweeting this to get the word out: π
Wrote up a guide to authenticated server-side rendering with Nextjs and Firebase Auth.
β Colin McDonnell (@colinhacks) August 17, 2020
There' s a surprising lack of documentation or sample code for this use case, given that it may well become a best practice over the next couple years. 1/nhttps://t.co/22hVqp8Fy5
The full code of this project β plus some other goodies! a sign up form! a logout button! β is available at https://github.com/colinhacks/next-firebase-ssr. Clone that repo to hit the ground running. ππ½ββοΈπ¨
By the way, I'm currently working on an extremely rad library that lets you define your API with TypeScript and auto-generate a strongly typed SDK that you can safely use on the client. I think it could be a gamechanger for full-stack TypeScript/Next.js development. Join the mailing list below to stay updated.
Subscribe