Skip to content

Instantly share code, notes, and snippets.

@jacwright
Created September 29, 2023 00:20
Show Gist options
  • Save jacwright/1225ab9e243a50ae3000f64a16687940 to your computer and use it in GitHub Desktop.
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.
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