64 lines
1.8 KiB
TypeScript
64 lines
1.8 KiB
TypeScript
import { createRemoteJWKSet, jwtVerify, type JWTVerifyGetKey } from 'jose';
|
|
|
|
const ACCESS_HOST_SUFFIX = '.cloudflareaccess.com';
|
|
const remoteKeySets = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
|
|
|
|
export function normalizeAccessTeamDomain(value: string): string {
|
|
const raw = value.trim();
|
|
if (!raw) throw new Error('Cloudflare Access team domain is empty');
|
|
|
|
const url = new URL(raw.includes('://') ? raw : `https://${raw}`);
|
|
if (
|
|
url.protocol !== 'https:' ||
|
|
url.username ||
|
|
url.password ||
|
|
url.port ||
|
|
(url.pathname !== '/' && url.pathname !== '') ||
|
|
url.search ||
|
|
url.hash ||
|
|
!url.hostname.endsWith(ACCESS_HOST_SUFFIX)
|
|
) {
|
|
throw new Error('Cloudflare Access team domain must be an HTTPS cloudflareaccess.com origin');
|
|
}
|
|
|
|
return url.origin;
|
|
}
|
|
|
|
function getRemoteKeySet(issuer: string): ReturnType<typeof createRemoteJWKSet> {
|
|
let keySet = remoteKeySets.get(issuer);
|
|
if (!keySet) {
|
|
keySet = createRemoteJWKSet(new URL(`${issuer}/cdn-cgi/access/certs`), {
|
|
timeoutDuration: 5_000,
|
|
cooldownDuration: 30_000,
|
|
cacheMaxAge: 10 * 60_000
|
|
});
|
|
remoteKeySets.set(issuer, keySet);
|
|
}
|
|
return keySet;
|
|
}
|
|
|
|
export async function verifyAccessJwt(
|
|
token: string,
|
|
teamDomain: string,
|
|
audience: string,
|
|
keySet?: JWTVerifyGetKey
|
|
): Promise<string> {
|
|
const issuer = normalizeAccessTeamDomain(teamDomain);
|
|
const expectedAudience = audience.trim();
|
|
if (!expectedAudience) throw new Error('Cloudflare Access audience is empty');
|
|
|
|
const { payload } = await jwtVerify(token, keySet ?? getRemoteKeySet(issuer), {
|
|
algorithms: ['RS256'],
|
|
issuer,
|
|
audience: expectedAudience,
|
|
clockTolerance: 5,
|
|
requiredClaims: ['iss', 'aud', 'exp', 'email']
|
|
});
|
|
|
|
if (typeof payload.email !== 'string' || !payload.email.trim()) {
|
|
throw new Error('Cloudflare Access JWT email claim is invalid');
|
|
}
|
|
|
|
return payload.email.trim();
|
|
}
|