Files
SnowLuma/.pnpm-store/v11/files/a2/4c05e27afe5a3eaa8f1f21261ed48077a73ef2db907be7d019a245b0d844e80198955f3f2332257ed59d6bfb20cd78f46ccf67b35e4b51240814b20a3a65eb
T
TanTan 2a69b23b47
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

122 lines
5.3 KiB
Plaintext

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[]> | HonoJsonWebKey[])} [options.keys] - The public keys used for JWK verification, or a function that returns them.
* @param {string | ((ctx: Context) => Promise<string> | 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 };