120 lines
4.5 KiB
Plaintext
120 lines
4.5 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/jwt/jwt.ts
|
|
/**
|
|
* JWT Auth Middleware for Hono.
|
|
*
|
|
* @see {@link https://hono.dev/docs/middleware/builtin/jwt}
|
|
*
|
|
* @param {object} options - The options for the JWT middleware.
|
|
* @param {SignatureKey} options.secret - A value of your secret key.
|
|
* @param {string} [options.cookie] - If this value is set, then the value is retrieved from the cookie header using that value as a key, which is then validated as a token.
|
|
* @param {SignatureAlgorithm} options.alg - An algorithm type that is used for verifying (required). Available types are `HS256` | `HS384` | `HS512` | `RS256` | `RS384` | `RS512` | `PS256` | `PS384` | `PS512` | `ES256` | `ES384` | `ES512` | `EdDSA`.
|
|
* @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 {VerifyOptions} [options.verification] - Additional options for JWT payload verification.
|
|
* @returns {MiddlewareHandler} The middleware handler function.
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* const app = new Hono()
|
|
*
|
|
* app.use(
|
|
* '/auth/*',
|
|
* jwt({
|
|
* secret: 'it-is-very-secret',
|
|
* alg: 'HS256',
|
|
* headerName: 'x-custom-auth-header', // Optional, default is 'Authorization'
|
|
* })
|
|
* )
|
|
*
|
|
* app.get('/auth/page', (c) => {
|
|
* return c.text('You are authorized')
|
|
* })
|
|
* ```
|
|
*/
|
|
const jwt = (options) => {
|
|
const verifyOpts = options.verification || {};
|
|
if (!options || !options.secret) throw new Error("JWT auth middleware requires options for \"secret\"");
|
|
if (!options.alg) throw new Error("JWT auth middleware requires options for \"alg\"");
|
|
if (!crypto.subtle || !crypto.subtle.importKey) throw new Error("`crypto.subtle.importKey` is undefined. JWT auth middleware requires it.");
|
|
return async function jwt(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) {
|
|
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 {
|
|
payload = await Jwt.verify(token, options.secret, {
|
|
alg: options.alg,
|
|
...verifyOpts
|
|
});
|
|
} catch (e) {
|
|
cause = e;
|
|
}
|
|
if (!payload) 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}"` }
|
|
});
|
|
}
|
|
const verifyWithJwks = Jwt.verifyWithJwks;
|
|
const verify = Jwt.verify;
|
|
const decode = Jwt.decode;
|
|
const sign = Jwt.sign;
|
|
//#endregion
|
|
export { decode, jwt, sign, verify, verifyWithJwks };
|