import { HTTPException } from "../../http-exception.js"; import { getCookie, getSignedCookie } from "../../helper/cookie/index.js"; import { Jwt } from "../../utils/jwt/index.js"; //#region src/middleware/jwk/jwk.ts /** * JWK Auth Middleware for Hono. * * @see {@link https://hono.dev/docs/middleware/builtin/jwk} * * @param {object} options - The options for the JWK middleware. * @param {HonoJsonWebKey[] | ((ctx: Context) => Promise | HonoJsonWebKey[])} [options.keys] - The public keys used for JWK verification, or a function that returns them. * @param {string | ((ctx: Context) => Promise | string)} [options.jwks_uri] - If set to a URI string or a function that returns a URI string, attempt to fetch JWKs from it. The response must be a JSON object containing a `keys` array, which will be merged with the `keys` option. * @param {boolean} [options.allow_anon] - If set to `true`, the middleware allows requests without a token to proceed without authentication. * @param {string} [options.cookie] - If set, the middleware attempts to retrieve the token from a cookie with these options (optionally signed) only if no token is found in the header. * @param {string} [options.headerName='Authorization'] - The name of the header to look for the JWT token. Default is 'Authorization'. * @param {string} [options.realm] - The protection space described by the `realm` parameter of the returned `WWW-Authenticate` challenge header. Defaults to the request URL. * @param {AsymmetricAlgorithm[]} options.alg - An array of allowed asymmetric algorithms for JWT verification. Only tokens signed with these algorithms will be accepted. * @param {RequestInit} [init] - Optional init options for the `fetch` request when retrieving JWKS from a URI. * @param {VerifyOptions} [options.verification] - Additional options for JWK payload verification. * @returns {MiddlewareHandler} The middleware handler function. * * @example * ```ts * const app = new Hono() * * app.use("/auth/*", jwk({ * jwks_uri: (c) => `https://${c.env.authServer}/.well-known/jwks.json`, * headerName: 'x-custom-auth-header', // Optional, default is 'Authorization' * })) * * app.get('/auth/page', (c) => { * return c.text('You are authorized') * }) * ``` */ const jwk = (options, init) => { const verifyOpts = options.verification || {}; if (!options || !(options.keys || options.jwks_uri)) throw new Error("JWK auth middleware requires options for either \"keys\" or \"jwks_uri\" or both"); if (!crypto.subtle || !crypto.subtle.importKey) throw new Error("`crypto.subtle.importKey` is undefined. JWK auth middleware requires it."); return async function jwk(ctx, next) { const headerName = options.headerName || "Authorization"; const credentials = ctx.req.raw.headers.get(headerName); let token; if (credentials) { const parts = credentials.split(/\s+/); if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") { const errDescription = "invalid credentials structure"; throw new HTTPException(401, { message: errDescription, res: unauthorizedResponse({ ctx, error: "invalid_request", errDescription, realm: options.realm }) }); } else token = parts[1]; } else if (options.cookie) { if (typeof options.cookie == "string") token = getCookie(ctx, options.cookie); else if (options.cookie.secret) { if (options.cookie.prefixOptions) token = await getSignedCookie(ctx, options.cookie.secret, options.cookie.key, options.cookie.prefixOptions); else token = await getSignedCookie(ctx, options.cookie.secret, options.cookie.key); } else if (options.cookie.prefixOptions) token = getCookie(ctx, options.cookie.key, options.cookie.prefixOptions); else token = getCookie(ctx, options.cookie.key); } if (!token) { if (options.allow_anon) return next(); const errDescription = "no authorization included in request"; throw new HTTPException(401, { message: errDescription, res: unauthorizedResponse({ ctx, error: "invalid_request", errDescription, realm: options.realm }) }); } let payload; let cause; try { const keys = typeof options.keys === "function" ? await options.keys(ctx) : options.keys; const jwks_uri = typeof options.jwks_uri === "function" ? await options.jwks_uri(ctx) : options.jwks_uri; payload = await Jwt.verifyWithJwks(token, { keys, jwks_uri, verification: verifyOpts, allowedAlgorithms: options.alg }, init); } catch (e) { cause = e; } if (!payload) { if (cause instanceof Error && cause.constructor === Error) throw cause; throw new HTTPException(401, { message: "Unauthorized", res: unauthorizedResponse({ ctx, error: "invalid_token", statusText: "Unauthorized", errDescription: "token verification failure", realm: options.realm }), cause }); } ctx.set("jwtPayload", payload); await next(); }; }; function unauthorizedResponse(opts) { const realm = (opts.realm ?? opts.ctx.req.url).replace(/"/g, "\\\""); const errDescription = opts.errDescription.replace(/"/g, "\\\""); return new Response("Unauthorized", { status: 401, statusText: opts.statusText, headers: { "WWW-Authenticate": `Bearer realm="${realm}",error="${opts.error}",error_description="${errDescription}"` } }); } //#endregion export { jwk };