-
-
Notifications
You must be signed in to change notification settings - Fork 754
feat(hono/jwk): JWK Auth Middleware #3826
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
e1b65a3
Update cookie.ts
Beyondo 3f0e470
Integrated `priority` option into setCookie serialization tests
Beyondo f90407c
Merge branch 'honojs:main' into main
Beyondo c16eb91
Add kid' to TokenHeader, fix Jwt.sign ignoring privateKey.alg with ke…
Beyondo 82ba1da
Add ./src/middleware/jwk/jwk.ts to jsr.json
Beyondo 298f5f0
Add hono/jwk to exports
Beyondo b2e5b53
feat(hono/jwk)
Beyondo 9453a9d
(feat/Jwt.verifyFromJwks) / batteries included util
Beyondo df0d32a
Update index.ts
Beyondo e809fae
add JwtHeaderRequiresKid exception
Beyondo be92784
using Jwt.verifyFromJwks now
Beyondo 69fe514
Merge branch 'honojs:main' into main
Beyondo 5082072
improved jsdoc and formatting
Beyondo 47f9d46
jsdoc update
Beyondo 28a7b97
formatting
Beyondo abbf23c
testing jwk's `keys` receiving an async function
Beyondo 763f1fd
removed redundancy
Beyondo 60f10be
add 'Should authorize Keys function' test
Beyondo c3d054c
added jwks_uri test + improved test descriptions
Beyondo 485b3dd
test naming consistency
Beyondo d6ec5ef
explicit return fix + moving global declaration merging to own interface
Beyondo ff2e8ac
cleaner jsdoc @example
Beyondo bb1cad8
removed commented-out tests unnecessarily inflating changes
Beyondo 582334c
ExtendedJsonWebKey -> HonoJsonWebKey
Beyondo e26aece
Refactor test to use msw per @yusukebe's suggestion
Beyondo 4f99220
removed stray log + added minor validation w/ @Code-Hex
Beyondo be0267c
Update index.ts
Beyondo dacce55
add more test coverage
Beyondo c7fcfb2
more test coverage
Beyondo b357e53
lint & format
Beyondo cea4150
typo
Beyondo 7c4faf1
100/100 test coverage
Beyondo dbc6032
final touch
Beyondo c9919a0
added eslint-disable + type export
Beyondo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.