/** * The default {@link App.create.Options.identify} hook: verifies a Cloudflare * Access JWT in-worker, so a request sent directly to the worker URL (bypassing * the edge gate) still can't reach the admin routes (defense in depth). * * The only admin module that imports `jose`, keeping that dependency out of the * IdP-agnostic core. Self-hosters not on Access supply their own `identify`. */ import * as jose from 'jose' import type * as App from './App.js' /** Header Cloudflare Access sets with the signed identity assertion (a JWT). */ const headerName = 'cf-access-jwt-assertion' /** Cookie Cloudflare Access sets as a fallback carrier for the same JWT. */ const cookieName = 'CF_Authorization' /** * Builds an {@link App.create.Options.identify} hook that verifies the Cloudflare * Access JWT against the team-domain JWKS (fetched lazily, cached by `jose`). Any * verification failure resolves to `null` (deny) rather than throwing. * * Verifies the signature and issuer (the team domain). The audience (`aud`) is * not checked, so any token minted for any app in the team's org is accepted — * fine when everyone with org access is trusted. * * @example * ```ts * import { App, Access } from 'tapimo/admin' * * App.create({ * // ... * identify: Access.cloudflareAccess({ * teamDomain: 'https://tempo.cloudflareaccess.com', * }), * }) * ``` */ export function cloudflareAccess( options: cloudflareAccess.Options, ): (request: Request) => Promise { const { teamDomain } = options // Created once and reused: `jose` caches keys and refetches on unknown `kid`. const issuer = teamDomain.replace(/\/+$/, '') const jwks = jose.createRemoteJWKSet(new URL(`${issuer}/cdn-cgi/access/certs`)) return async (request) => { const token = readToken(request) if (!token) return null try { const { payload } = await jose.jwtVerify(token, jwks, { issuer }) const email = payload['email'] return typeof email === 'string' && email ? { email } : null } catch { // Any verification failure is a denial, not an error to surface. return null } } } export declare namespace cloudflareAccess { /** Options for {@link cloudflareAccess}. */ type Options = { /** Team domain (e.g. `https://.cloudflareaccess.com`); the token issuer and JWKS host. */ teamDomain: string } } /** * Extracts the Access JWT: the `Cf-Access-Jwt-Assertion` header, falling back to * the `CF_Authorization` cookie. Returns `undefined` when neither is present. */ function readToken(request: Request): string | undefined { const header = request.headers.get(headerName) if (header) return header const cookie = request.headers.get('cookie') if (!cookie) return undefined for (const part of cookie.split(';')) { const [name, ...rest] = part.trim().split('=') if (name === cookieName && rest.length) return rest.join('=') } return undefined }