import * as Boom from "@hapi/boom"; import * as Hoek from "@hapi/hoek"; import * as Hapi from "@hapi/hapi"; import { promisify } from "util"; import jwt from "jsonwebtoken"; import jwksClient, { hapiJwt2KeyAsync } from "jwks-rsa"; import type { IAppConfig } from "../../config"; const CF_JWT_HEADER_NAME = "cf-access-jwt-assertion"; const CF_JWT_ALGOS = ["RS256"]; const verifyToken = (settings: any) => { const { audience, issuer } = settings; const client = jwksClient({ jwksUri: `${issuer}/cdn-cgi/access/certs`, }); return async (token: any) => { const getKey = (header: any, callback: any) => { client.getSigningKey(header.kid, (err, key) => { if (err) throw Boom.serverUnavailable( "failed to fetch cloudflare access jwks" ); callback(undefined, key?.getPublicKey()); }); }; const opts = { algorithms: CF_JWT_ALGOS, audience, issuer, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any return (promisify(jwt.verify) as any)(token, getKey, opts); }; }; const handleCfJwt = (verify: any) => async ( request: Hapi.Request, h: Hapi.ResponseToolkit ) => { const token = request.headers[CF_JWT_HEADER_NAME]; if (token) { try { await verify(token); } catch (error) { console.error(error); return Boom.unauthorized("invalid cloudflare access token"); } } return h.continue; }; const defaultOpts = { issuer: undefined, audience: undefined, strategyName: "clouflareaccess", validate: undefined, }; const cfJwtRegister = async (server: Hapi.Server, options: any): Promise => { server.dependency(["hapi-auth-jwt2"]); const settings = Hoek.applyToDefaults(defaultOpts, options); const verify = verifyToken(settings); const { validate, strategyName, audience, issuer } = settings; server.ext("onPreAuth", handleCfJwt(verify)); server.auth.strategy(strategyName!, "jwt", { key: hapiJwt2KeyAsync({ jwksUri: `${issuer}/cdn-cgi/access/certs`, }), cookieKey: false, urlKey: false, headerKey: CF_JWT_HEADER_NAME, validate, verifyOptions: { audience, issuer, algorithms: ["RS256"], }, }); }; export const registerCloudflareAccessJwt = async ( server: Hapi.Server, config: IAppConfig ): Promise => { const { audience, domain } = config.cfaccess; // only enable this plugin if cloudflare access config is configured if (audience && domain) { server.log(["auth"], "cloudflare access authorization enabled"); await server.register({ plugin: { name: "cloudflare-jwt", version: "0.0.1", register: cfJwtRegister, }, options: { issuer: `https://${domain}`, audience, validate: (decoded: any, _request: any) => { const { email, name } = decoded; return { isValid: true, credentials: { user: { email, name } }, }; }, }, }); } };