In this section, we'll see how to
send the user's ID token to the server
validate the ID token on the server
use the ID token to dynamically prepare HTML to send to the client
Recap¶
Quick recap of our findings thus far..
User signs up or signs in
A POST
request is issued to the Firebase Authentication backend
The Authentication backend returns an ID token as a JWT with info about the user
The user info gets stored on the client, in the IndexedDB
How to send the ID Token to the server¶
After a user signs up or signs in, we redirect that user to the home page. In turn, the src/app/page.js
route code is executed on the server.
src/app/page.js
export default function HomePage () {
console. log ( "I'm running on the server!" )
return < div >Hello World </ div >
}
Our goal is to send the ID token from the client to the server so that we can access it within this route. Then we can dynamically render the home page.
How do we send the ID Token to the server? There are a couple ways we can do this..
Upon sign up or sign in, we can put the ID token into a cookie. (Remember, cookies are sent to the server on every request, automatically.)
Following the work by David Bitta and others , we can implement a service worker that intercepts all fetch requests originating from the client and adds the idToken
to them.
Cookie method¶
When the user signs up / signs in, we'll grab their idToken
and stick it in a cookie. Then it'll be sent to the server automatically on subsequent requests.
Set cookie¶
Here's how you can set the cookie after the user signs up / signs in. Note that this code runs on the client.
src/app/signup/SignUpForm.jsx
src/app/signin/SignInForm.jsx
"use client"
// (imports and other code not shown) ...
createUserWithEmailAndPassword (auth, email, password)
. then ( async ( userCredential ) => {
const user = userCredential.user
// Get the idToken from the user object
return user. getIdToken (). then (( idToken ) => {
// Build the cookie string
let cookie = `idToken=${ idToken }; SameSite=Lax;`
if (window.location.hostname !== "localhost" ) cookie += " secure;"
// Set the cookie
document.cookie = cookie
// Redirect to the home page
// refresh() to prevent Next.js caching
router. push ( "/" )
router. refresh ()
})
})
. catch (( error ) => {
console. log ( "Error:" , error)
})
// (jsx and other code not shown) ...
"use client"
// (imports and other code not shown) ...
signInWithEmailAndPassword (auth, email, password)
. then ( async ( userCredential ) => {
const user = userCredential.user
// Get the idToken from the user object
return user. getIdToken (). then (( idToken ) => {
// Build the cookie string
let cookie = `idToken=${ idToken }; SameSite=Lax;`
if (window.location.hostname !== "localhost" ) cookie += " secure;"
// Set the cookie
document.cookie = cookie
// Redirect to the home page
// refresh() to prevent Next.js caching
router. push ( "/" )
router. refresh ()
})
})
. catch (( error ) => {
console. log ( "Error:" , error)
})
// (jsx and other code not shown) ...
idToken
is just a string that looks like this 👇
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6InJpY2t5QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYXV0aF90aW1lIjoxNzI0MjU0NzQ3LCJ1c2VyX2lkIjoiUXZvUjR5a1NpS2tJeTAwazh1eERVcENOdE5HciIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsicmlja3lAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifSwiaWF0IjoxNzI0MjU0NzQ3LCJleHAiOjE3MjQyNTgzNDcsImF1ZCI6ImZpcmVhdXRoNTUiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vZmlyZWF1dGg1NSIsInN1YiI6IlF2b1I0eWtTaUtrSXkwMGs4dXhEVXBDTnROR3IifQ.
The secure
flag prevents the cookie from being delivered over unsecured HTTP. That is, it can only be delivered over HTTPS. However, when you develop on localhost, you're not using HTTPS, so you can't use the secure
flag.
idToken
is just a string that looks like this 👇
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6InJpY2t5QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiYXV0aF90aW1lIjoxNzI0MjU0NzQ3LCJ1c2VyX2lkIjoiUXZvUjR5a1NpS2tJeTAwazh1eERVcENOdE5HciIsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsicmlja3lAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifSwiaWF0IjoxNzI0MjU0NzQ3LCJleHAiOjE3MjQyNTgzNDcsImF1ZCI6ImZpcmVhdXRoNTUiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vZmlyZWF1dGg1NSIsInN1YiI6IlF2b1I0eWtTaUtrSXkwMGs4dXhEVXBDTnROR3IifQ.
The secure
flag prevents the cookie from being delivered over unsecured HTTP. That is, it can only be delivered over HTTPS. However, when you develop on localhost, you're not using HTTPS, so you can't use the secure
flag.
Now, when a user signs up or signs in, you should see this cookie appear in the cookies section of the Application in Chrome DevTools.
You should also see this cookie being delivered to the server when you inspect Network requests.
Accessing the cookie server-side¶
With Next.js, you can get access to the cookie server-side with cookies().get()
👇
src/app/page.js
import { cookies } from "next/headers"
import { Home } from "./Home"
export default async function HomePage () {
const idToken = cookies (). get ( "idToken" )
console. log ( "idToken" , idToken)
return < div >Hello World </ div >
}
Now we need to verify the token's authenticity.
Validate the idToken
server-side¶
You can use the verifyIdToken()
method to verify the ID token, server-side. You'll need to prep a few things to make this work.
Install the firebase-admin
package. (npm install firebase-admin
)
Instantiate a firebase admin app with initializeApp
.
See my guide on this step ->
Add the following environment variables to .env.development
.env.development
### Firebase Emulator
FIRESTORE_EMULATOR_HOST="127.0.0.1:8080"
FIREBASE_AUTH_EMULATOR_HOST="127.0.0.1:9099"
FIREBASE_STORAGE_EMULATOR_HOST="127.0.0.1:9199"
In particular, FIREBASE_AUTH_EMULATOR_HOST
tells the verifyIdToken()
method to relax the rules for checking the ID token while the Emulator is running.
Here's what my project looks like at this stage 👇
Project Structure
fireauth
├── .env.development
├── .firebaserc
├── .git
│ └── ...
├── .gitignore
├── .vscode
│ ├── settings.json
│ └── tasks.json
├── README.md
├── firebase-debug.log
├── firebase.json
├── firestore-debug.log
├── firestore.indexes.json
├── firestore.rules
├── functions
│ ├── .gitignore
│ ├── index.js
│ ├── package-lock.json
│ └── package.json
├── jsconfig.json
├── next.config.mjs
├── package-lock.json
├── package.json
├── src
│ ├── app
│ │ ├── Home.jsx
│ │ ├── layout.js
│ │ ├── page.js
│ │ ├── signin
│ │ │ ├── SignInForm.jsx
│ │ │ └── page.js
│ │ └── signup
│ │ ├── SignUpForm.jsx
│ │ └── page.js
│ └── firebase
│ ├── firebase.js
│ └── firebaseAdmin.js
└── ui-debug.log
Now I can make the home page dynamic depending on the presence of the ID token and its details. For example,
src/app/page.js
import { adminAuth } from "@/firebase/firebaseAdmin"
import { cookies } from "next/headers"
import Link from "next/link"
import { SignOutButton } from "./SignOutButton"
export default async function HomePage () {
const idToken = cookies (). get ( "idToken" )
// Verify and decode the token
const idTokenDecoded =
idToken && ( await adminAuth. verifyIdToken (idToken.value))
return (
<>
< div >Home page</ div >
{ idTokenDecoded ? (
< SignOutButton />
) : (
<>
< Link href = "/signup" >Sign up</ Link >
< span > | </ span >
< Link href = "/signin" >Sign in</ Link >
</>
) }
</>
)
}
idToken {
name : 'idToken' ,
value : 'eyJhbGciOiJub 25 lIiwidHlwIjoiSldUIn 0 .eyJlbWFpbCI 6 Imx 1 Y 3 lAZ 21 haWwuY 29 tIiwiZW 1 haWxfdmVyaWZpZWQiOmZhbHNlLCJhdXRoX 3 RpbWUiOjE 3 MjQyNjI 2 NzcsInVzZXJfaWQiOiJ 6 cnlGTENuc 21 rbGVxWkZES 0 U 0 TUtKbzhLNmlrIiwiZmlyZWJhc 2 UiOnsiaWRlbnRpdGllcyI 6 eyJlbWFpbCI 6 WyJsdWN 5 QGdtYWlsLmNvbSJdfSwic 2 lnbl 9 pbl 9 wcm 92 aWRlciI 6 InBhc 3 N 3 b 3 JkIn 0 sImlhdCI 6 MTcyNDI 2 MjY 3 NywiZXhwIjoxNzI 0 MjY 2 Mjc 3 LCJhdWQiOiJmaXJlYXV 0 aDU 1 IiwiaXNzIjoiaHR 0 cHM 6 Ly 9 zZWN 1 cmV 0 b 2 tlbi 5 nb 29 nbGUuY 29 tL 2 ZpcmVhdXRoNTUiLCJzdWIiOiJ 6 cnlGTENuc 21 rbGVxWkZES 0 U 0 TUtKbzhLNmlrIn 0 .'
}
idTokenDecoded {
email : '[email protected] ' ,
email_verified : false ,
auth_time : 1724262677 ,
user_id : 'zryFLCnsmkleqZFDKE 4 MKJo 8 K 6 ik' ,
firebase : { identities : { email : [ Array ] }, sign_in_provider : 'password' },
iat : 1724262677 ,
exp : 1724266277 ,
aud : 'fireauth 55 ' ,
iss : 'https: //securetoken.google.com/fireauth55',
sub: 'zryFLCnsmkleqZFDKE 4 MKJo 8 K 6 ik' ,
uid : 'zryFLCnsmkleqZFDKE 4 MKJo 8 K 6 ik'
}
src/app/SignOutButton.jsx
"use client"
import { auth } from "@/firebase/firebase"
import { signOut } from "firebase/auth"
import { useRouter } from "next/navigation"
export function SignOutButton () {
const router = useRouter ()
const handleSignOut = () => {
signOut (auth)
// Delete the idToken cookie
document.cookie = 'idToken=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
// Refresh the page
router. refresh ()
}
return < button onClick = { handleSignOut } >Sign out</ button >
}
I didn't do any error handling in the case where the idToken
is provided but it's invalid. You need to implement that!
Let's see it in action.
So smooth. Awesome! 🤙
...but there's a problem 😱
The ID token is short-lived. It expires in one hour. Oof!
createSessionCookie()
¶
Rather than rely on the short-lived token issued by Firebase, why don't we just exchange it for our own long-lived token? That's exactly the solution proposed here .
Rather than copy the linked article, I'll just outline the steps.
Create an API endpoint ("route handler " in Next.js) that recives the short-lived token and exchanges is for a long-lived token. The key to this step is to use the createSessionCookie()
method.
Tweak the Sign up and Sign in methods such that they make make a request to that API endpoint with the ID Token. If successful, the API should return a response with the Set-Cookie
with the new token.
On subsequent requests to the server, use the verifySessionCookie()
method to verify the session cookie.
Service worker method¶
The service worker method is described in detail here . I'm too burned out to add details to this approach, but if you're struggling to understand it, reach out to me.