Ben
Ben

Reputation: 21685

How do I verify a Firebase sessionCookie (JWT) using jose?

Setup

  1. I'm following the docs here.
  2. I'm using Next.js middleware which means I cannot use Firebase functions to verify my cookie. So, I'm attempting to verify it with jose.
  3. The Firebase docs state

ensure that the session cookie was signed by the private key corresponding to the token's kid claim. Get the public key from https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys and use a JWT library to verify the signature.

What I've tried

// middleware.js

import { NextResponse } from 'next/server'
import * as jose from 'jose'

export async function middleware(request) {
  // Check the cookies
  const allCookies = request.cookies.getAll()
  console.log(allCookies)

  // Get the sessionCookie (if it exists)
  const sessionCookie = request.cookies.get("sessionCookie")
  console.log("sessionCookie", sessionCookie)

  // Test verification using a hard-coded public key from 
  // https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys
  const alg = 'RS256'
  const spki = `-----BEGIN CERTIFICATE-----
  MIIDHDCCAgSgAwIBAgIEIhgI5zANBgkqhkiG9w0BAQsFADAzMQ8wDQYDVQQDEwZH
  aXRraXQxEzARBgNVBAoTCkdvb2dsZSBJbmMxCzAJBgNVBAYTAlVTMB4XDTE5MDEx
  NDIzMjgzMVoXDTIwMDEwOTIzMjgzMVowMzEPMA0GA1UEAxMGR2l0a2l0MRMwEQYD
  VQQKEwpHb29nbGUgSW5jMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQAD
  ggEPADCCAQoCggEBAMLh7U8PNsOebb1HVLDa81W/CZK8DrdYl9Vlwj/0/GF7kfLP
  zF4qyShw1zAZddlzt7f6lDy2ZvmQ1nqW0IRy8xgYTeAb6aCLY+rm4DEnwJOCnAVo
  m1xpgcOVExsXXpleWsP0MugM5xa91Y79CYkVTevPZThgjqfGjan3GiXLQwIJTLG8
  xEXXSzmMDeW6dP6CBVPsbbTYSat6CX2nBm2YoJ4v2dij3DQBzOD/d5WLaYL4yE9p
  TjmreitFvs1r0AZF53Cq5ju7M10indVyk1zwfK+tuk7VnCZz/mBHEPxT9+Q5EWFi
  lVVkZ+2Qau0qRIvar/1QukVpyEsVHPZH+aZ9ilMCAwEAAaM4MDYwDAYDVR0TAQH/
  BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAjAOBgNVHQ8BAf8EBAMCB4AwDQYJ
  KoZIhvcNAQELBQADggEBAH4ca9fhlBd9XmaX9E0rZ8QCWT8AnxcVLY+S+nHJg2IT
  dj7ZytNJWttKEssOdslrQyamxt2SdQSfpd2UpA+fCO2sRSJOBEA3squGrj1yh1Pt
  lajf2TM1MwpdO7ZqBbfE3hLH7srBbVXHxKsUemepxviC+dpF0u4o3EjqHpAvkruO
  4RtC4grbFBNRIbBTu7oJHpKnBJWxK7w11a5Sabs6reAuFh68QaVAQzrscYP/M4i9
  SPwArWKoCJgrxxVof1N1cal4UpziEH9OaWa7RfyZTYAeEJ/atDo5s8AACPUH542f
  JEwmwAJKbJKLRoFYJJXCPeKp1Q7EXQHJNNdS2ABjw+w=
  -----END CERTIFICATE-----`

  const publicKey = await jose.importSPKI(spki, alg)
  const { payload, protectedHeader } = await jose.jwtVerify(sessionCookie.value, publicKey)
  
  console.log(protectedHeader)
  console.log(payload)
}

This produces the error

error "spki" must be SPKI formatted string

Questions

  1. How do I make this work?

  2. Why does https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys provide not one, but five different public keys?

  3. Each public key appears to have a unique (key, value) pair. For example

    "skIBNg": "-----BEGIN CERTIFICATE-----\nMIIDHDCCAgSgAwIB...vb2dsZSBJbmMxCzAJBgNVBAYTA.f+9Vg=\n-----END CERTIFICATE-----\n"
    

    What is the purpose of the key "skIBNg"? I'm not currently using it.

Upvotes: 1

Views: 450

Answers (1)

Ben
Ben

Reputation: 21685

The JWT header includes a kid like skIBNg. This kid tells you which one of the five public keys to use.

Furthermore, I needed to use jose.importX509(), not jose.importSPKI(). (The giveaway was BEGIN CERTIFICATE in the string as opposed to BEGIN PUBLIC KEY).

Here's some incomplete code to make this work.

import { NextResponse } from "next/server";
import * as jose from "jose";

export async function middleware(request) {

  // Get the sessionCookie (if it exists)
  const sessionCookie = request.cookies.get("sessionCookie");

  // Decode header
  const protectedHeader = jose.decodeProtectedHeader(sessionCookie.value)
  
  // Decode payload
  // const claims = jose.decodeJwt(sessionCookie.value)

  // Make sure the alg header is RS256
  // Make sure the kid header is in the public_keys keys
  if (!(protectedHeader.alg == "RS256" && protectedHeader.hasOwnProperty('kid'))) {
    NextResponse.redirect(new URL('/signin', request.url))
  }

  // Fetch public keys
  const publicKeysResponse = await fetch("https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys")
  const publicKeysData = await publicKeysResponse.json()

  // Get the correct public key
  const x509 = publicKeysData[protectedHeader.kid]
  const { alg } = protectedHeader

  // Validate and fetch the payload
  const ecPublicKey = await jose.importX509(x509, alg)
  const { payload } = await jose.jwtVerify(sessionCookie.value, ecPublicKey)

  // ...
}

Upvotes: 2

Related Questions