Ben Gorman

Ben Gorman

Life's a garden. Dig it.

In this section, we'll see how to

  1. send the user's ID token to the server
  2. validate the ID token on the server
  3. use the ID token to dynamically prepare HTML to send to the client

Recap

Quick recap of our findings thus far..

  1. User signs up or signs in
  2. A POST request is issued to the Firebase Authentication backend
  3. The Authentication backend returns an ID token as a JWT with info about the user
  4. 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..

  1. 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.)
  2. 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.

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.

  1. Install the firebase-admin package. (npm install firebase-admin)

  2. Instantiate a firebase admin app with initializeApp.

    See my guide on this step ->

  3. 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>
        </>
      )}
    </>
  )
}
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>
}

Error Handling

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.

  1. 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.
  2. 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.
  3. 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.