@m-kopa/platform-auth
v0.8.0
Published
Cloudflare Access JWT validation.
Readme
platform-auth
Cloudflare Access JWT validation.
Install
npm install platform-authUsage
import {
createJwksCache,
parseAccessJwtFromHeaders,
verifyAccessJwt,
} from "platform-auth";
const jwks = createJwksCache(process.env.CF_ACCESS_JWKS_URL!);
const token = parseAccessJwtFromHeaders(request.headers);
if (token === null) return new Response("Unauthorized", { status: 401 });
const user = await verifyAccessJwt(token, {
teamDomain: process.env.CF_ACCESS_TEAM_DOMAIN!,
audience: process.env.CF_ACCESS_AUD!,
jwks,
});Gateway-fronted apps (confined origins)
If your app is fronted by the platform Entra-OIDC gateway and its pages.dev
origin is confined (confine_origin=true), the apex bouncer strips
Cf-Access-Jwt-Assertion. Read the gateway's un-clobbered
X-Launchpad-User-Assertion instead, and verify against the gateway's
issuer / audience / JWKS:
import { parseGatewayAssertionFromHeaders, verifyAccessJwt } from "platform-auth";
const token = parseGatewayAssertionFromHeaders(request.headers);
if (token === null) return new Response("Unauthorized", { status: 401 });
const user = await verifyAccessJwt(token, {
teamDomain: process.env.GATEWAY_ISSUER!,
audience: process.env.GATEWAY_AUD!,
jwks: createJwksCache(process.env.GATEWAY_JWKS_URL!),
});With the Hono middleware, pass header: "gateway" (default is "cf-access"):
createPlatformAuthMiddleware({
header: "gateway",
teamDomain: env.GATEWAY_ISSUER,
audience: env.GATEWAY_AUD,
jwks: createJwksCache(env.GATEWAY_JWKS_URL),
});Each call reads exactly one explicitly-chosen header — there is no dual-read fallback.
Integration-lane requests (machine callers)
An approved integration calls your app through the gateway with an API key.
The gateway checks the key, strips Authorization and Cookie, and forwards the request with a short-lived (5 minute) X-Launchpad-Integration-Assertion naming the integration.
There is no user on these requests, so verify the assertion with verifyIntegrationAssertion, never with verifyAccessJwt or verifyJwt (both reject it).
import {
createJwksCache,
parseIntegrationAssertionFromHeaders,
verifyIntegrationAssertion,
} from "@m-kopa/platform-auth";
const jwks = createJwksCache(env.GATEWAY_JWKS_URL);
const token = parseIntegrationAssertionFromHeaders(request.headers);
if (token === null) return new Response("Unauthorized", { status: 401 });
const { name } = await verifyIntegrationAssertion(token, {
issuer: env.GATEWAY_ISSUER,
host: env.GATEWAY_AUD, // your app's hostname, never the request URL
method: request.method,
path: new URL(request.url).pathname,
jwks,
});
// `name` is the integration, e.g. "sales-backend". Authorise on it.Take host from GATEWAY_AUD, not from the request URL.
On v1 (Pages) origins the request your code sees is addressed to *.pages.dev, so a host read from the URL would never match, and trusting it would let the caller pick the audience.
method and path do come from the request you are serving: that is what binds the assertion to this one endpoint.
If you have to verify the assertion without this package, check every one of these and fail closed on any miss:
algisRS256and the signature verifies against the gateway JWKS (kidfrom the header).issequals the gateway issuer.audequalsintegration:<host>, withhostfromGATEWAY_AUD.expis in the future andnbfis not in the future (small clock tolerance only).typequalslaunchpad-integration.substarts withintegration:and the rest matches^[a-z0-9][a-z0-9-]{0,62}$; that rest is the integration name.htmequals the request method, upper-cased.htuequals the request pathname exactly, with no normalisation (a trailing slash is a different path).
License
MIT
