-
-
Notifications
You must be signed in to change notification settings - Fork 655
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(hono/jwk): JWK Auth Middleware (#3826)
* Update cookie.ts * Integrated `priority` option into setCookie serialization tests * Add kid' to TokenHeader, fix Jwt.sign ignoring privateKey.alg with keyAlg fallback, add decodeHeaders utility - Added "kid" (Key ID) for TokenHeader - Fixed Jwt.sign() ignoring privateKey.alg - Renamed `alg` parameter to `KEYAlg` to differentiate between privateKey.alg - Added utility function `decodeHeaders` to decode only JWT headers * Add ./src/middleware/jwk/jwk.ts to jsr.json * Add hono/jwk to exports * feat(hono/jwk) Hono JWK Middleware main features: - Ability to provide a list of public JWKs to the keys parameter as a simple javascript array [] - Ability to provide a URL in the jwks_uri parameter to fetch keys from + an optional RequestInit (useful for caching if your cloud provider has a modified fetch that supports it, or if you simply want to modify the request) - Ability to provide an async function that returns an array to the keys parameter instead of a direct array, so that it is possible to implement own custom caching layer without a separate middleware - Allows setting a keys directory for multi-key auth systems - Allows auth endpoints to be always updated with the Auth provider's public JWKs directory (often `.well-known/jwks.json`) which makes key rotations without disruptions possible Todo: - More tests. * (feat/Jwt.verifyFromJwks) / batteries included util * Update index.ts * add JwtHeaderRequiresKid exception * using Jwt.verifyFromJwks now * improved jsdoc and formatting * jsdoc update * formatting * testing jwk's `keys` receiving an async function * removed redundancy * add 'Should authorize Keys function' test Added /auth-keys-fn/* & /.well-known/jwks.json testing endpoints to the router. * added jwks_uri test + improved test descriptions * test naming consistency * explicit return fix + moving global declaration merging to own interface * cleaner jsdoc @example * removed commented-out tests unnecessarily inflating changes * ExtendedJsonWebKey -> HonoJsonWebKey * Refactor test to use msw per @yusukebe's suggestion * removed stray log + added minor validation w/ @Code-Hex Also removed redundant "import type {} from '../..'" from 3 different files: - jwk/index.ts - jwt/index.ts - request-id/index.ts * Update index.ts * add more test coverage * more test coverage * lint & format * typo * 100/100 test coverage Note: Moved more code from `hono/jwk` to the `verifyFromJwks` function for backends that require JWK verification logic beyond just a middleware * final touch * added eslint-disable + type export
- Loading branch information
Showing
11 changed files
with
935 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { jwk } from './jwk' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
/** | ||
* @module | ||
* JWK Auth Middleware for Hono. | ||
*/ | ||
|
||
import type { Context } from '../../context' | ||
import { getCookie, getSignedCookie } from '../../helper/cookie' | ||
import { HTTPException } from '../../http-exception' | ||
import type { MiddlewareHandler } from '../../types' | ||
import type { CookiePrefixOptions } from '../../utils/cookie' | ||
import { Jwt } from '../../utils/jwt' | ||
import '../../context' | ||
import type { HonoJsonWebKey } from '../../utils/jwt/jws' | ||
|
||
/** | ||
* 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[] | (() => Promise<HonoJsonWebKey[]>)} [options.keys] - The values of your public keys, or a function that returns them. | ||
* @param {string} [options.jwks_uri] - If this value is set, attempt to fetch JWKs from this URI, expecting a JSON response with `keys` which are added to the provided options.keys | ||
* @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 {RequestInit} [init] - Optional initialization options for the `fetch` request when retrieving JWKS from a URI. | ||
* @returns {MiddlewareHandler} The middleware handler function. | ||
* | ||
* @example | ||
* ```ts | ||
* const app = new Hono() | ||
* | ||
* app.use("/auth/*", jwk({ jwks_uri: "https://example-backend.hono.dev/.well-known/jwks.json" })) | ||
* | ||
* app.get('/auth/page', (c) => { | ||
* return c.text('You are authorized') | ||
* }) | ||
* ``` | ||
*/ | ||
|
||
export const jwk = ( | ||
options: { | ||
keys?: HonoJsonWebKey[] | (() => Promise<HonoJsonWebKey[]>) | ||
jwks_uri?: string | ||
cookie?: | ||
| string | ||
| { key: string; secret?: string | BufferSource; prefixOptions?: CookiePrefixOptions } | ||
}, | ||
init?: RequestInit | ||
): MiddlewareHandler => { | ||
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 credentials = ctx.req.raw.headers.get('Authorization') | ||
let token | ||
if (credentials) { | ||
const parts = credentials.split(/\s+/) | ||
if (parts.length !== 2) { | ||
const errDescription = 'invalid credentials structure' | ||
throw new HTTPException(401, { | ||
message: errDescription, | ||
res: unauthorizedResponse({ | ||
ctx, | ||
error: 'invalid_request', | ||
errDescription, | ||
}), | ||
}) | ||
} 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, | ||
}), | ||
}) | ||
} | ||
|
||
let payload | ||
let cause | ||
try { | ||
payload = await Jwt.verifyFromJwks(token, options, 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', | ||
}), | ||
cause, | ||
}) | ||
} | ||
|
||
ctx.set('jwtPayload', payload) | ||
|
||
await next() | ||
} | ||
} | ||
|
||
function unauthorizedResponse(opts: { | ||
ctx: Context | ||
error: string | ||
errDescription: string | ||
statusText?: string | ||
}) { | ||
return new Response('Unauthorized', { | ||
status: 401, | ||
statusText: opts.statusText, | ||
headers: { | ||
'WWW-Authenticate': `Bearer realm="${opts.ctx.req.url}",error="${opts.error}",error_description="${opts.errDescription}"`, | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
{ | ||
"public_keys": [ | ||
{ | ||
"kid": "hono-test-kid-1", | ||
"kty": "RSA", | ||
"use": "sig", | ||
"alg": "RS256", | ||
"e": "AQAB", | ||
"n": "2XGQh8VC_p8gRqfBLY0E3RycnfBl5g1mKyeiyRSPjdaR7fmNPuC3mHjVWXtyXWSvAuRYPYfL_pSi6erpxVv7NuPJbKaZ-I1MwdRPdG2qHu9mNYxniws73gvF3tUN9eSsQUIBL0sYEOnVMjniDcOxIr3Rgz_RxdLB_FxTDXYhzzG49L79wGV1udILGHq0lqlMtmUX6LRtbaoRt1fJB4rTCkYeQp9r5HYP79PKTR43vLIq0aZryI4CyBkPG_0vGEvnzasGdp-qE9Ywt_J2anQKt3nvVVR4Yhs2EIoPQkYoDnVySjeuRsUA5JQYKThrM4sFZSQsO82dHTvwKo2z2x6ZMw" | ||
}, | ||
{ | ||
"kid": "hono-test-kid-2", | ||
"kty": "RSA", | ||
"use": "sig", | ||
"alg": "RS256", | ||
"e": "AQAB", | ||
"n": "uRVR5DkH22a_FM4RtqvnVxd6QAjdfj8oFYPaxIux7K8oTaBy5YagxTWN0qeKI5lI3nL20cx72XxD_UF4TETCFgfD-XB48cdjnSQlOXXbRPXUX0Rdte48naAt4przAb7ydUxrfvDlbSZe02Du-ZGRzEB6RW6KLFWUvTadI4w33qb2i8hauQuTcRmaIUESt8oytUGS44dXAw3Nqt_NL-e7TRgX5o1u_31Uvet1ofsv6Mx8vxJ6zMdM_AKvzLt2iuoK_8vL4R86CjD3dpal2BwO7RkRl2Wcuf5jxjM4pruJ2RBCpzBieEvSIH8kKHIm9SfTzTDJqRhoXd7KM5jL1GNzyw" | ||
} | ||
], | ||
"private_keys": [ | ||
{ | ||
"kid": "hono-test-kid-1", | ||
"alg": "RS256", | ||
"d": "A5CR2gGPegHwOYUbUzylZvdgUFNWMetOUK7M3TClGdVgSkWpELrTLhpTa3m50KYlG446x03baxUGU4D_MoKx7GukX0-fGCzY17FvWNOwOLACcPMYT3ZwfAQ2_jkBimJxU7CNUtH18KQ-U1B3nQ1apHZc-1Xa6CKIY5nv32yfj6uTrERRLOs7Fn9xpOE4uMHEf-l1ppIEIqK5QkEoPRMCUBABsGBSfiJP2hQVa-R-nezX3kVSxKTxAjDEOkquzb-CKlJW7xN2xQ7p40Wi7lDWZkOapBNGr59Z4gcFfo6f8XpQrqoFjDfsGsdH5q9MH_3lEEtD14wymXNnCoRHNr_mwQ", | ||
"dp": "WMq_BNbd3At-J9VzXgE-aLvPhztS1W8K9xlghITpwAyzhEfCp9mO7IOEVtNWKoEtVFEaZrWKuNWKd-dnzjvydltCkpJ7QhTmiFNFsEzKNJdGQ1Tfsj9658csbVLUOhI4oVcN6kiCa6OdH41Z_JMyN75cTgd4z5h_FRYRRgjoUEU", | ||
"dq": "Lz9vM7L-aEsPJOM5K2PqInLP9HNwDl943S79d_aw6w-JnHPFcu95no6-6nRcd87eSWoTvHZeFgsle4oiV0UpAocEO7xraCBa_Z9o-jGbBfynOLyXMH2l70yWBdCGCzgc_Wg2sKJwiYYXXfGJ3CzSeIRet82Rn54Q9mMlB6Ie8LE", | ||
"e": "AQAB", | ||
"kty": "RSA", | ||
"n": "2XGQh8VC_p8gRqfBLY0E3RycnfBl5g1mKyeiyRSPjdaR7fmNPuC3mHjVWXtyXWSvAuRYPYfL_pSi6erpxVv7NuPJbKaZ-I1MwdRPdG2qHu9mNYxniws73gvF3tUN9eSsQUIBL0sYEOnVMjniDcOxIr3Rgz_RxdLB_FxTDXYhzzG49L79wGV1udILGHq0lqlMtmUX6LRtbaoRt1fJB4rTCkYeQp9r5HYP79PKTR43vLIq0aZryI4CyBkPG_0vGEvnzasGdp-qE9Ywt_J2anQKt3nvVVR4Yhs2EIoPQkYoDnVySjeuRsUA5JQYKThrM4sFZSQsO82dHTvwKo2z2x6ZMw", | ||
"p": "7K-X3xMf3xxdlHTRs17x4WkbFUq4ZCU9L1al88UW2tpoF8ZDLUvaKXeF0vkosKvYUsiHsV1fbGVo6Oy75iII-op-t6-tP3R61nkjaytyJ8p32nbxBI1UWpFxZYNxG_Od07kau3LwkgDh8Ogr6zqmq8-lKoBPio-4K7PY5FiyWzs", | ||
"q": "6y__IKt1n1pTc-S9l1WfSuC96jX8iQhEsGSxnshyNZi59mH1AigkrAw9T5b7OFX7ulHXwuithsVi8cxkq2inNmemxD3koiiU-sv6vg6lRCoZsXFHiUCP-2HoK17sR1zUb6HQpp5MEHY8qoC3Mi3IpkNC7gAbAukbMQo3WlIGqmk", | ||
"qi": "flgM56Nw2hzHHy0Lz8ewBtOkkzfq1r_n6SmSZdU0zWlEp1lLovpHmuwyVeXpQlLJUHqcNVRw0NlwV7EN0rPd4rG3hcMdogj_Jl-r52TYzx4kVpbMEIh4xKs5rFzxbb96A3F9Ox-muRWvfOUCpXxGXCCGqHRmjRUolxDxsiPznuk" | ||
}, | ||
{ | ||
"kid": "hono-test-kid-2", | ||
"alg": "RS256", | ||
"d": "JCIL50TVClnQQyUJ40JDO0b7mGXCrCNzVWP1ATsOhNkbQrBozfOPDoEqi24m81U5GyiRlBraMPboJRizfhxMUdW5RkjVa8pT4blNRR8DrD5b9C9aJir5DYLYgm1itLwNBKZjNBieicUcbSL29KUdNCWAWW6_rfEVRS1U1zxIKgDUPVd6d7jiIwAKuKvGlMc11RGRZj5eKSNMQyLU5u8Qs_VQuoBRNAyWLZZcHMlAWbh3er7m0jkmUDRdVU0y_n1UAGsr9cAxPwf2HtS5j5R2ahEodatsJynnafYtj6jbOR6jvO3N2Vf-NJ7jVY2-kfv1rJd86KAxD-tIAGx2w1VRTQ", | ||
"dp": "wQhiWfdvVxk7ERmYj7Fn04wqjP7o7-72bn3SznGyBSkvpkg1WX4j467vpRtXVn4qxSSMXCj2UMKCrovba2RWHp1cnkvT-TFTbONkBuhOBpbx3TVwgGd-IfDJVa_i89XjiYgtEApHz173kRodEENXxcOj_mbOGyBb9Yl2M45A-tU", | ||
"dq": "ERdP5mdziJ46OsDHTdZ4hOX2ti0EljtVqGo1B4WKXey6DMH0JGHGU_3fFiF4Gomhy3nyGUI7Qhk3kf7lixAtSsk1lWAAeQLPt1r8yZkD5odLKXLyua_yZJ041d3O3wxRYXl3OvzoVy6rPhzRPIaxevNp-Pp5ZNoKfonQPz3bDGc", | ||
"e": "AQAB", | ||
"kty": "RSA", | ||
"n": "uRVR5DkH22a_FM4RtqvnVxd6QAjdfj8oFYPaxIux7K8oTaBy5YagxTWN0qeKI5lI3nL20cx72XxD_UF4TETCFgfD-XB48cdjnSQlOXXbRPXUX0Rdte48naAt4przAb7ydUxrfvDlbSZe02Du-ZGRzEB6RW6KLFWUvTadI4w33qb2i8hauQuTcRmaIUESt8oytUGS44dXAw3Nqt_NL-e7TRgX5o1u_31Uvet1ofsv6Mx8vxJ6zMdM_AKvzLt2iuoK_8vL4R86CjD3dpal2BwO7RkRl2Wcuf5jxjM4pruJ2RBCpzBieEvSIH8kKHIm9SfTzTDJqRhoXd7KM5jL1GNzyw", | ||
"p": "7cY_nFnn4w5pVi7wq_S9FJHIGsxCwogXqSSC_d7yWopbI2rW3Ugx21IMcWT2pnpsF_VYQx5FnNFviFufNOloREOguqci4lBinAilYBf3VXaN_YrxSk4flJmykwm_HBbXpHt_L3t4HBf-uuY-klJxFkeTbBErjxMS0U0EheEpDYU", | ||
"q": "x0UidqgkzWPqXa7vZ5noYTY5e3TDQZ_l8A26lFDKAbB62lXvnp_MhnQYDAx9VgUGYYrXv7UmaH-ZCSzuMM9Uhuw0lXRyojF-TLowNjASMlWbkJsJus3zi_AI4pAKyYnhNADxZrT1kxseI8zHiq0_bQa8qLaleXBTdkpc3Z6M1Q8", | ||
"qi": "x5VJcfnlX9ZhH6eMKx27rOGQrPjQ4BjZgmND7rrX-CSrE0M0RG4KuC4ZOu5XpQ-YsOC_bIzolBN2cHGn4ttPXeUc3y5bnqJYo7FxMdGn4gPRbXlVjCrE54JH_cdkl8cDqcaybjme1-ilNu-vHJWgHPdpbOguhRpicARkptAkOe0" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters