While integrating Firebase Auth into one of my personal projects, I found out that the Firebase Admin SDK requires a Node.js environment, which doesn’t work with the Edge runtime used by Next.js middleware. The solution is to verify ID tokens using a third-party JWT library https://firebase.google.com/docs/auth/admin/verify-id-tokens. I’m using jose (JSON Object Signing and Encryption) module to verify ID tokens.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { NextResponse } from 'next/server';
import { importX509, jwtVerify, JWTPayload } from 'jose';

const secretKey = process.env.JWT_SECRET_KEY;
const firebaseProjectId = process.env.FIREBASE_PROJECT_ID;

if (!secretKey || !firebaseProjectId) {
  throw new Error(
    'Missing JWT_SECRET_KEY or FIREBASE_PROJECT_ID environment variable'
  );
}

let publicKeysCache: Record<string, string> | null = null;

const getPublicKeys = async (): Promise<Record<string, string>> => {
  if (publicKeysCache) {
    return publicKeysCache;
  }

  const res = await fetch(
    'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
  );
  if (!res.ok) {
    throw new Error('Failed to fetch public keys');
  }

  const publicKeys = await res.json();
  publicKeysCache = publicKeys;
  return publicKeys;
};

const verifyFirebaseJwt = async (firebaseJwt: string): Promise<JWTPayload> => {
  const publicKeys = await getPublicKeys();

  const { payload } = await jwtVerify(
    firebaseJwt,
    async (header) => {
      const x509Cert = publicKeys[header.kid!];
      if (!x509Cert) {
        throw new Error('Invalid key ID');
      }
      return await importX509(x509Cert, 'RS256');
    },
    {
      issuer: `https://securetoken.google.com/${firebaseProjectId}`,
      audience: firebaseProjectId,
      algorithms: ['RS256'],
    }
  );

  return payload;
};

const createJsonResponse = (data: any, status: number): NextResponse => {
  return new NextResponse(JSON.stringify(data), {
    status,
    headers: {
      'Content-Type': 'application/json',
    },
  });
};

export async function middleware(req: any): Promise<NextResponse> {
  const token = req.headers.get('Authorization')?.split('Bearer ')[1];

  if (!token) {
    return createJsonResponse({ error: 'Unauthorized' }, 401);
  }

  try {
    await verifyFirebaseJwt(token);
    return NextResponse.next();
  } catch (error) {
    console.error('JWT verification failed:', error);
    return createJsonResponse({ error: 'Unauthorized' }, 401);
  }
}

export const config = {
  matcher: ['/api/:path*'],
};
  1. Fetching Public Keys: The getPublicKeys function fetches the public keys from the Google API. These keys are used to verify the JWT tokens.

  2. Token Verification: The verifyFirebaseJwt function verifies the token using the public key corresponding to the kid (key ID) in the token header. It uses the jose module’s jwtVerify function to ensure the token is valid and issued by Firebase.

  3. Middleware Function: The middleware function extracts the JWT from the Authorization header. It verifies the token and either allows the request to proceed or returns an unauthorized response.