Both, it says here
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.
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.
`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.
export const verifyWithJwks = async ( token: string, options: { keys?: HonoJsonWebKey[] jwks_uri?: string verification?: VerifyOptions allowedAlgorithms: readonly AsymmetricAlgorithm[] }, init?: RequestInit): Promise<JWTPayload> => { const verifyOpts = options.verification || {} const header = decodeHeader(token) if (!isTokenHeader(header)) { throw new JwtHeaderInvalid(header) } if (!header.kid) { throw new JwtHeaderRequiresKid(header) } // Reject symmetric algorithms (HS256, HS384, HS512) to prevent algorithm confusion attacks if (symmetricAlgorithms.includes(header.alg as SymmetricAlgorithm)) { throw new JwtSymmetricAlgorithmNotAllowed(header.alg) } // Validate against allowed algorithms if (!options.allowedAlgorithms.includes(header.alg as AsymmetricAlgorithm)) { throw new JwtAlgorithmNotAllowed(header.alg, options.allowedAlgorithms) } if (options.jwks_uri) { const response = await fetch(options.jwks_uri, init) if (!response.ok) { throw new Error(`failed to fetch JWKS from ${options.jwks_uri}`) } const data = (await response.json()) as { keys?: JsonWebKey[] } if (!data.keys) { throw new Error('invalid JWKS response. "keys" field is missing') } if (!Array.isArray(data.keys)) { throw new Error('invalid JWKS response. "keys" field is not an array') } if (options.keys) { options.keys.push(...data.keys) } else { options.keys = data.keys } } else if (!options.keys) { throw new Error('verifyWithJwks requires options for either "keys" or "jwks_uri" or both') } const matchingKey = options.keys.find((key) => key.kid === header.kid) if (!matchingKey) { throw new JwtTokenInvalid(token) } // Verify that JWK's alg matches JWT header's alg when JWK has alg field if (matchingKey.alg && matchingKey.alg !== header.alg) { throw new JwtAlgorithmMismatch(matchingKey.alg, header.alg) } return await verify(token, matchingKey, { alg: header.alg, ...verifyOpts, })}