honojs/hono · error · Error

invalid JWKS response. "keys" field is missing

Error message

invalid JWKS response. "keys" field is missing

What it means

verifyWithJwks fetched the JWKS URI successfully but the parsed JSON body has no 'keys' property. The JWKS format (RFC 7517) requires a top-level 'keys' array, so the library refuses to use the document. This usually means the URL points at something that is not a JWKS endpoint.

Source

Thrown at src/utils/jwt/jwt.ts:237

  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)
  }

  let verifyKeys = options.keys ? [...options.keys] : undefined

  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')
    }
    verifyKeys ??= []
    verifyKeys.push(...(data.keys as HonoJsonWebKey[]))
  } else if (!verifyKeys) {
    throw new Error('verifyWithJwks requires options for either "keys" or "jwks_uri" or both')
  }

  const matchingKey = verifyKeys.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)

View on GitHub (pinned to e2740d5a1b)

Solutions

  1. Verify the value of options.jwks_uri is the 'jwks_uri' field from the provider's /.well-known/openid-configuration, not the issuer
  2. curl the JWKS URI and confirm the body is {"keys":[...]}
  3. If a proxy intercepts the request, fix routing or add the correct Host/Authorization headers so the real JWKS is returned
  4. Pass keys directly via options.keys as a fallback instead of fetching

Example fix

// before
await verifyWithJwks(token, { jwks_uri: 'https://auth.example.com' })
// after
const disc = await (await fetch('https://auth.example.com/.well-known/openid-configuration')).json()
await verifyWithJwks(token, { jwks_uri: disc.jwks_uri })
Defensive patterns

Strategy: validation

Validate before calling

const res = await fetch(jwksUri)
const data = await res.json()
if (!('keys' in data)) throw new Error('endpoint is not a JWKS document')
await verifyWithJwks(token, { keys: data.keys })

Type guard

const isJwks = (d: unknown): d is { keys: JsonWebKey[] } =>
  typeof d === 'object' && d !== null && 'keys' in d && Array.isArray((d as any).keys)

Try / catch

try { await verifyWithJwks(token, { jwks_uri }) } catch (e) { if (e instanceof Error && e.message.includes('JWKS')) { /* log URI, alert config issue */ } throw e }

Prevention

When it happens

Trigger: Calling verifyWithJwks({ jwks_uri }) where the URI returns valid JSON without a 'keys' field, e.g. an OIDC discovery document, an ID-token endpoint, an HTML-to-JSON error page, or a misrouted API response.

Common situations: Using the issuer URL instead of the jwks_uri from the discovery document; auth provider changed its JWKS path; a proxy/gateway returns a JSON error object (e.g. {"error":"not found"}) with status 200; typo in the JWKS URL.

Related errors


AI-assisted analysis of honojs/hono@e2740d5a1b (2026-08-28). Data as JSON: /api/errors/6cdef706ba315810. Report an issue: GitHub.