redline
← arena

Both, it says here

SeniorTypeScriptJWTMined
MINED FROM REAL HISTORYhonojs/honoMIT

The code below is the file exactly as it stood in honojs/hono. The defect was introduced on 2025-06-16, reviewed, merged, and fixed on 2026-02-26 in commit bda46ac1 — after 255 days in the tree.

The commit was written by a human contributor to honojs/hono, not by an agent. The ticket and the description on the next two panels are ours — a reconstruction of the case the original change made, so you meet it the way its reviewer did. The code, the defect and the dates are untouched.

AUTH-517the ticket

Allow static keys and a JWKS endpoint at the same time

Today `verifyWithJwks` takes either `keys` or `jwks_uri`. Two deployments need both: one pins an offline break-glass key alongside the provider's rotating set, the other is mid-migration between issuers.

Accept both. When `jwks_uri` is set, fetch it and consider those keys in addition to any `keys` passed in. When only `keys` is given, do not make a network call. Passing neither stays an error.

the case the change madereconstructed by us

`keys` and `jwks_uri` can now be supplied together.

If `jwks_uri` is present we fetch it, validate the response shape — `keys` must be present and must be an array, both checked before use — and merge. If the caller also passed static keys we append the fetched ones to them; if not, the fetched set becomes the key set. The lookup below is unchanged and now searches the union, so a `kid` from either source matches.

The validation order is deliberate and unchanged: `kid` is required, symmetric algorithms are rejected outright to close algorithm confusion, and the header algorithm is checked against `allowedAlgorithms` before any network call is made. An unauthenticated request cannot cause a fetch.

Our reconstruction of the argument the real change made, not a quotation of it. Fluent, specific, and not evidence of anything — which is the point.

src/utils/jwt/jwt.ts67 lines
1export const verifyWithJwks = async (
2 token: string,
3 options: {
4 keys?: HonoJsonWebKey[]
5 jwks_uri?: string
6 verification?: VerifyOptions
7 allowedAlgorithms: readonly AsymmetricAlgorithm[]
8 },
9 init?: RequestInit
10): Promise<JWTPayload> => {
11 const verifyOpts = options.verification || {}
12
13 const header = decodeHeader(token)
14
15 if (!isTokenHeader(header)) {
16 throw new JwtHeaderInvalid(header)
17 }
18 if (!header.kid) {
19 throw new JwtHeaderRequiresKid(header)
20 }
21
22 // Reject symmetric algorithms (HS256, HS384, HS512) to prevent algorithm confusion attacks
23 if (symmetricAlgorithms.includes(header.alg as SymmetricAlgorithm)) {
24 throw new JwtSymmetricAlgorithmNotAllowed(header.alg)
25 }
26
27 // Validate against allowed algorithms
28 if (!options.allowedAlgorithms.includes(header.alg as AsymmetricAlgorithm)) {
29 throw new JwtAlgorithmNotAllowed(header.alg, options.allowedAlgorithms)
30 }
31
32 if (options.jwks_uri) {
33 const response = await fetch(options.jwks_uri, init)
34 if (!response.ok) {
35 throw new Error(`failed to fetch JWKS from ${options.jwks_uri}`)
36 }
37 const data = (await response.json()) as { keys?: JsonWebKey[] }
38 if (!data.keys) {
39 throw new Error('invalid JWKS response. "keys" field is missing')
40 }
41 if (!Array.isArray(data.keys)) {
42 throw new Error('invalid JWKS response. "keys" field is not an array')
43 }
44 if (options.keys) {
45 options.keys.push(...data.keys)
46 } else {
47 options.keys = data.keys
48 }
49 } else if (!options.keys) {
50 throw new Error('verifyWithJwks requires options for either "keys" or "jwks_uri" or both')
51 }
52
53 const matchingKey = options.keys.find((key) => key.kid === header.kid)
54 if (!matchingKey) {
55 throw new JwtTokenInvalid(token)
56 }
57
58 // Verify that JWK's alg matches JWT header's alg when JWK has alg field
59 if (matchingKey.alg && matchingKey.alg !== header.alg) {
60 throw new JwtAlgorithmMismatch(matchingKey.alg, header.alg)
61 }
62
63 return await verify(token, matchingKey, {
64 alg: header.alg,
65 ...verifyOpts,
66 })
67}