-
-
Notifications
You must be signed in to change notification settings - Fork 656
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
feat(hono/jwk): JWK Auth Middleware #3826
Conversation
…yAlg 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
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.
Added /auth-keys-fn/* & /.well-known/jwks.json testing endpoints to the router.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## next #3826 +/- ##
==========================================
- Coverage 91.71% 90.79% -0.92%
==========================================
Files 160 164 +4
Lines 10195 10434 +239
Branches 2885 3061 +176
==========================================
+ Hits 9350 9474 +124
- Misses 844 959 +115
Partials 1 1 ☔ View full report in Codecov by Sentry. |
Note that the test code I removed some commented out tests to give a clearer picture (Now at 307 lines of test code). But they did remind me—we need some signed cookie unit tests if we're gonna make sure to support everything in |
Hi @Beyondo Thanks! I'll check this later. |
@yusukebe No problem! I originally named the new interface Take your time! I'm using the fork temporarily, so no rush at all. |
Hey @Code-Hex ! I'll review this later, but can you take a look at it too? |
@yusukebe I'm sorry I missed this PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was mostly LGTM!
Also removed redundant "import type {} from '../..'" from 3 different files: - jwk/index.ts - jwt/index.ts - request-id/index.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@Code-Hex Thank you for the review! Hi, @Beyondo, Almost all are good, but the test coverage is decreased. Of course, it should not be 100%, but is there any chance to add more tests? |
Hey, @yusukebe ! I improved test coverage by around +15% focusing on testing the important parts, such as signed cookies and missing |
Thank you so much! If it's ready, please ping me. I'll review it. |
Note: Moved more code from `hono/jwk` to the `verifyFromJwks` function for backends that require JWK verification logic beyond just a middleware
@yusukebe Yo, everything, at least JWK-related, is covered & all hono tests pass. I think it should be decent enough for review now |
Thank you for your hard work! Almost done. I've left comments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Hi @Beyondo Looks good. Thank you for your hard work! I'll merge it into the |
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [hono](https://hono.dev) ([source](https://github.com/honojs/hono)) | dependencies | minor | [`4.6.20` -> `4.7.0`](https://renovatebot.com/diffs/npm/hono/4.6.20/4.7.0) | --- ### Release Notes <details> <summary>honojs/hono (hono)</summary> ### [`v4.7.0`](https://github.com/honojs/hono/releases/tag/v4.7.0) [Compare Source](honojs/hono@v4.6.20...v4.7.0) ### Release Notes Hono v4.7.0 is now available! This release introduces one helper and two middleware. - Proxy Helper - Language Middleware - JWK Auth Middleware Plus, Standard Schema Validator has been born. Let's look at each of these. #### Proxy Helper We sometimes use the Hono application as a reverse proxy. In that case, it accesses the backend using `fetch`. However, it sends an unintended headers. ```ts app.all('/proxy/:path', (c) => { // Send unintended header values to the origin server return fetch(`http://${originServer}/${c.req.param('path')}`) }) ``` For example, `fetch` may send `Accept-Encoding`, causing the origin server to return a compressed response. Some runtimes automatically decode it, leading to a `Content-Length` mismatch and potential client-side errors. Also, you should probably remove some of the headers sent from the origin server, such as `Transfer-Encoding`. [Proxy Helper](https://hono.dev/docs/helpers/proxy) will send requests to the origin and handle responses properly. The above headers problem is solved simply by writing as follows. ```ts import { Hono } from 'hono' import { proxy } from 'hono/proxy' app.get('/proxy/:path', (c) => { return proxy(`http://${originServer}/${c.req.param('path')}`) }) ``` You can also use it in more complex ways. ```ts app.get('/proxy/:path', async (c) => { const res = await proxy( `http://${originServer}/${c.req.param('path')}`, { headers: { ...c.req.header(), 'X-Forwarded-For': '127.0.0.1', 'X-Forwarded-Host': c.req.header('host'), Authorization: undefined, }, } ) res.headers.delete('Set-Cookie') return res }) ``` Thanks [@​usualoma](https://github.com/usualoma)! #### Language Middleware [Language Middleware](https://hono.dev/docs/middleware/builtin/language) provides 18n functions to Hono applications. By using the `languageDetector` function, you can get the language that your application should support. ```ts import { Hono } from 'hono' import { languageDetector } from 'hono/language' const app = new Hono() app.use( languageDetector({ supportedLanguages: ['en', 'ar', 'ja'], // Must include fallback fallbackLanguage: 'en', // Required }) ) app.get('/', (c) => { const lang = c.get('language') return c.text(`Hello! Your language is ${lang}`) }) ``` You can get the target language in various ways, not just by using `Accept-Language`. - Query parameters - Cookies - `Accept-Language` header - URL path Thanks [@​lord007tn](https://github.com/lord007tn)! #### JWK Auth Middleware Finally, middleware that supports JWK (JSON Web Key) has landed. Using [JWK Auth Middleware](https://hono.dev/docs/middleware/builtin/jwk), you can authenticate by verifying JWK tokens. It can access keys fetched from the specified URL. ```ts import { Hono } from 'hono' import { jwk } from 'hono/jwk' app.use( '/auth/*', jwk({ jwks_uri: `https://${backendServer}/.well-known/jwks.json`, }) ) app.get('/auth/page', (c) => { return c.text('You are authorized') }) ``` Thanks [@​Beyondo](https://github.com/Beyondo)! #### Standard Schema Validator [Standard Schema](https://standardschema.dev/) provides a common interface for TypeScript validator libraries. [Standard Schema Validator](https://github.com/honojs/middleware/tree/main/packages/standard-validator) is a validator that uses it. This means that Standard Schema Validator can handle several validators, such as Zod, Valibot, and ArkType, with the same interface. The code below really works! ```ts import { Hono } from 'hono' import { sValidator } from '@​hono/standard-validator' import { type } from 'arktype' import * as v from 'valibot' import { z } from 'zod' const aSchema = type({ agent: 'string', }) const vSchema = v.object({ slag: v.string(), }) const zSchema = z.object({ name: z.string(), }) const app = new Hono() app.get( '/:slag', sValidator('header', aSchema), sValidator('param', vSchema), sValidator('query', zSchema), (c) => { const headerValue = c.req.valid('header') const paramValue = c.req.valid('param') const queryValue = c.req.valid('query') return c.json({ headerValue, paramValue, queryValue }) } ) const res = await app.request('/foo?name=foo', { headers: { agent: 'foo', }, }) console.log(await res.json()) ``` Thanks [@​muningis](https://github.com/muningis)! #### New features - feat(helper/proxy): introduce proxy helper honojs/hono#3589 - feat(logger): include query params honojs/hono#3702 - feat: add language detector middleware and helpers honojs/hono#3787 - feat(hono/context): add buffer returns honojs/hono#3813 - feat(hono/jwk): JWK Auth Middleware honojs/hono#3826 - feat(etag): allow for custom hashing methods to be used to etag honojs/hono#3832 - feat(router): support greedy matches with subsequent static components honojs/hono#3888 #### All changes - docs(CONTRIBUTING): remove text about `yarn` by [@​EdamAme-x](https://github.com/EdamAme-x) in honojs/hono#3878 - refactor(request): `toLowerCase()` is unnecessary for `req.header()` by [@​yusukebe](https://github.com/yusukebe) in honojs/hono#3880 - fix(helper/adapter): correct `env` type by [@​yusukebe](https://github.com/yusukebe) in honojs/hono#3885 - chore(test): update to vitest 3 by [@​yasuaki640](https://github.com/yasuaki640) in honojs/hono#3861 - fix(router/trie-router): fix label with trailing wildcard pattern by [@​usualoma](https://github.com/usualoma) in honojs/hono#3892 - feat(helper/proxy): introduce proxy helper by [@​usualoma](https://github.com/usualoma) in honojs/hono#3589 - feat(logger): include query params by [@​ryuapp](https://github.com/ryuapp) in honojs/hono#3702 - feat(factory): Allow HonoOptions<E> with factory by [@​miyaji255](https://github.com/miyaji255) in honojs/hono#3786 - feat: add language detector middleware and helpers by [@​lord007tn](https://github.com/lord007tn) in honojs/hono#3787 - feat(hono/context): add buffer returns by [@​askorupskyy](https://github.com/askorupskyy) in honojs/hono#3813 - feat(hono/jwk): JWK Auth Middleware by [@​Beyondo](https://github.com/Beyondo) in honojs/hono#3826 - feat(etag): allow for custom hashing methods to be used to etag by [@​EdamAme-x](https://github.com/EdamAme-x) in honojs/hono#3832 - feat(router): support greedy matches with subsequent static components. by [@​usualoma](https://github.com/usualoma) in honojs/hono#3888 - fix(client): correct inferring empty object from`c.json({})` by [@​yusukebe](https://github.com/yusukebe) in honojs/hono#3873 - Next by [@​yusukebe](https://github.com/yusukebe) in honojs/hono#3896 - chore(runtime-tests): add `deno.lock` by [@​yusukebe](https://github.com/yusukebe) in honojs/hono#3897 #### New Contributors - [@​lord007tn](https://github.com/lord007tn) made their first contribution in honojs/hono#3787 **Full Changelog**: honojs/hono@v4.6.20...v4.7.0 </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNjIuMiIsInVwZGF0ZWRJblZlciI6IjM5LjE2Mi4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=--> Reviewed-on: https://git.tristess.app/alexandresoro/ouca-backend/pulls/537 Reviewed-by: Alexandre Soro <[email protected]> Co-authored-by: renovate <[email protected]> Co-committed-by: renovate <[email protected]>
Hello, I recently needed a JWK middleware for my projects, but I figured contributing it to hono has the potential to save me and others a lot of time.
Middleware Features:
options.keys
to a static array of public keysHonoJsonWebKey[]
in code.options.keys
to an async function that returns aPromise<HonoJsonWebKey[]>
for flexibilityoptions.jwks_uri
to fetch JWKs from a URI, after which it appends those fetched keys to providedkeys
if anyinit
parameter (only used forjwks_uri
)—useful if your host supports caching through custom init options.Added extra:
JwtHeaderRequiresKid
exception. Since the middleware requires presence of akid
field in the header in order to select the correct key.Jwt.verifyFromJwks
util function (batteries included).Addressed issues:
Other code changes:
JsonWebKey
to havekid?: string
(This is a standard: https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.4)kid?: string
to TokenHeaderJwt.sign(payload, privateKey, alg)
whereprivateKey.alg
was always ignored and onlyalg
parameter was considered.Suggestion: Removal of the default
'HS256'
value fromJwt.sign
and makealg
optional, so that if neitherprivateKey.alg
noralg
is defined, throw an error. My 'philosophy' for this is that it would be misleading for hono to display= 'HS256'
when the privateKey can internally have a different algorithm, such as when it is of typeJsonWebKey
orCryptoKey
(Though we only care aboutJsonWebKey
here). Makingalg
a "fallback" explicit parameter could be better.Example Usage:
Using
jwks_uri
Using
jwks_uri
and an optionalinit
Using
keys
from an arrayUsing
keys
as an async function (custom logic / caching)Tests:
I adapted a lot from JWT's own units tests to make sure some basic JWT validation exists, then added the actual validation for the JWK middleware's own functionality which is mostly: (1) getting keys through various methods, (2) selecting the appropriate JWK based on
kid
(without fully decoding the JWT—only the header), and then (3) verifying using the utilityJwt.verify
. Step 2 and 3 are done byJwt.verifyFromJwks
.JWK Middleware Test
Full Test
bun run format:fix && bun run lint:fix
to format the code (Done)Feel free to contribute more unit tests, extend and/or modify for the better.