Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899export interface AccessEnv { ACCESS_TEAM_DOMAIN?: string; ACCESS_AUD?: string;}
export interface TeamCertKey { kty: string; kid?: string; use?: string; alg?: string; n?: string; e?: string;}
interface AccessClaims { iss: string; aud: string | string[]; exp: number; iat: number; email?: string; sub?: string;}
const CLOCK_SKEW_MS = 60 * 1000;
const b64urlBytes = (part: string): Uint8Array => { const padded = part .replace(/-/g, '+') .replace(/_/g, '/') .padEnd(part.length + ((4 - (part.length % 4)) % 4), '='); const binary = atob(padded); return Uint8Array.from(binary, (ch) => ch.charCodeAt(0));};
export class VerifiedAccessIdentity { readonly #claims: Readonly<{ email: string }>;
private constructor(claims: { email: string }) { this.#claims = Object.freeze({ email: claims.email }); }
get email(): string { return this.#claims.email; }
static async verify(token: string | null, env: AccessEnv): Promise<VerifiedAccessIdentity | null> { if (!token || !env.ACCESS_TEAM_DOMAIN || !env.ACCESS_AUD) return null; const [head, payload, signature] = token.split('.'); if (!head || !payload || !signature) return null;
try { const header = JSON.parse(new TextDecoder().decode(b64urlBytes(head))) as { alg?: string; kid?: string; }; if (header.alg !== 'RS256' || !header.kid) return null;
const certsResponse = await fetch(`https://${env.ACCESS_TEAM_DOMAIN}/cdn-cgi/access/certs`); if (!certsResponse.ok) throw new Error(`certs endpoint returned ${certsResponse.status}`); const certs = (await certsResponse.json()) as { keys: TeamCertKey[] }; const key = certs.keys.find((k) => k.kid === header.kid); if (!key) return null;
const cryptoKey = await crypto.subtle.importKey( 'jwk', key, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify'], ); const signed = new TextEncoder().encode(`${head}.${payload}`); const valid = await crypto.subtle.verify( 'RSASSA-PKCS1-v1_5', cryptoKey, b64urlBytes(signature) as unknown as ArrayBuffer, signed as unknown as ArrayBuffer, ); if (!valid) return null;
const claims = JSON.parse(new TextDecoder().decode(b64urlBytes(payload))) as AccessClaims; const now = Date.now(); if (claims.iss !== `https://${env.ACCESS_TEAM_DOMAIN}`) return null; const audOk = Array.isArray(claims.aud) ? claims.aud.includes(env.ACCESS_AUD) : claims.aud === env.ACCESS_AUD; if (!audOk) return null; if (typeof claims.exp !== 'number' || typeof claims.iat !== 'number') return null; if (claims.exp * 1000 <= now - CLOCK_SKEW_MS) return null; if (claims.iat * 1000 > now + CLOCK_SKEW_MS) return null;
return new VerifiedAccessIdentity({ email: claims.email ?? claims.sub ?? 'access user', }); } catch { return null; } }}