Created
September 29, 2023 00:20
-
-
Save jacwright/1225ab9e243a50ae3000f64a16687940 to your computer and use it in GitHub Desktop.
Basic idea of how you would verify a Firebase auth JWT on the server in Cloudflare.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import jwt from '@tsndr/cloudflare-worker-jwt'; | |
import { StatusError } from '../lib/errors'; | |
import { Config, RouterRequest, SocketAuth } from '../types'; | |
export function uses(request: RouterRequest) { | |
return (request.headers.get('authorization') || '').match(/^Bearer /i); | |
} | |
export async function auth(request: RouterRequest, noError?: boolean) { | |
return authSocket( | |
request.config, | |
request, | |
request.headers.get('authorization') || '', | |
typeof request.query.delegate === 'string' ? request.query.delegate : undefined, | |
noError | |
); | |
} | |
export async function authSocket( | |
config: Config, | |
authInfo: SocketAuth, | |
idToken: string, | |
delegate = '', | |
noError?: boolean | |
) { | |
idToken = idToken.replace(/^\s*bearer\s+/i, ''); | |
if (!idToken) { | |
if (noError) return; | |
throw new StatusError(401, 'Authorization header required'); | |
} | |
let payload: any; | |
try { | |
payload = await verify(config.auth.firebase.projectId, idToken); | |
} catch (err) { | |
// console.error(err); | |
throw new StatusError(401, 'Authorization invalid'); | |
} | |
if (!payload.sub) { | |
if (!noError) throw new StatusError(401, 'Authorization invalid'); | |
} else { | |
authInfo.uid = payload.sub; | |
authInfo.admin = payload.admin; | |
} | |
} | |
async function verify(projectId: string, token: string) { | |
if (typeof token !== 'string') throw new Error('JWT token must be a string'); | |
const tokenParts = token.split('.'); | |
if (tokenParts.length !== 3) throw new Error('JWT token must consist of 3 parts'); | |
const { | |
header: { alg, kid }, | |
payload, | |
} = jwt.decode(token) as { header: any; payload: any }; | |
if (payload.nbf && payload.nbf > Math.floor(Date.now() / 1000)) throw 'JWT token not yet valid'; | |
if (payload.exp && payload.exp <= Math.floor(Date.now() / 1000)) throw 'JWT token expired'; | |
const jsonWebKey = await getPublicKey(kid); | |
if (alg !== 'RS256' || !jsonWebKey || payload.iss !== `https://securetoken.google.com/${projectId}`) { | |
throw new Error('JWT invalid'); | |
} | |
const importAlgorithm = { name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' } }; | |
const key = await crypto.subtle.importKey('jwk', jsonWebKey, importAlgorithm, false, ['verify']); | |
const verified = await crypto.subtle.verify( | |
importAlgorithm, | |
key, | |
parseBase64URL(tokenParts[2]), | |
(jwt as any)._utf8ToUint8Array(`${tokenParts[0]}.${tokenParts[1]}`) | |
); | |
if (!verified) throw new Error('JWT invalid'); | |
return payload; | |
} | |
let publicKeys: Record<string, JsonWebKey>; | |
async function getPublicKey(kid: string): Promise<JsonWebKey> { | |
if (!publicKeys) { | |
// Found this resource here https://stackoverflow.com/a/71988314/835542 since the documented one provides x509 certs, not directly useful | |
const response = await fetch( | |
'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com' | |
); | |
const cacheControl = response.headers.get('Cache-Control') as string; | |
const age = parseInt(cacheControl.replace(/^.*max-age=(\d+).*$/, '$1')); | |
setTimeout(() => ((publicKeys as any) = undefined), age * 1000); | |
publicKeys = ((await response.json()) as { keys: FirebaseJsonWebKey[] }).keys.reduce( | |
(map, key) => (map[key.kid] = key) && map, | |
{} as Record<string, JsonWebKey> | |
); | |
} | |
return publicKeys[kid]; | |
} | |
interface FirebaseJsonWebKey extends JsonWebKey { | |
kid: string; | |
} | |
function parseBase64URL(s: string) { | |
return new Uint8Array( | |
Array.prototype.map.call(atob(s.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '')), (c: string) => | |
c.charCodeAt(0) | |
) as number[] | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment