honojs/hono · error · JwtHeaderRequiresKid

required "kid" in jwt header: ${JSON.stringify(header)}

Error message

required "kid" in jwt header: ${JSON.stringify(header)}

What it means

Thrown by verifyWithJwks when the JWT header is structurally valid but has no kid (key ID) claim. JWK-based verification requires kid to select which key from the JWKS/keys list is used.

Source

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

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

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

View on GitHub (pinned to e2740d5a1b)

Solutions

  1. If the token is symmetric/shared-secret, use verify() with the secret key instead of verifyWithJwks
  2. If using an asymmetric issuer, ensure it includes kid in the JOSE header (most OIDC providers do)
  3. If you already know the exact key, pass it to verify() directly and skip JWKS lookup
  4. Check whether the JWKS has only one key — some libraries allow kid-less selection, but this one requires kid

Example fix

// before
await verifyWithJwks(hs256TokenWithoutKid, opts) // throws
// after
await verify(hs256TokenWithoutKid, secret, { alg: 'HS256' })
Defensive patterns

Strategy: validation

Validate before calling

const header = decodeHeader(token)
if (!header.kid) { /* route to verify() with shared secret or reject */ }

Type guard

const hasKid = (h: unknown): h is { kid: string } =>
  typeof h === 'object' && h !== null && typeof (h as { kid?: unknown }).kid === 'string'

Try / catch

try { await verifyWithJwks(token, opts) } catch (e) { if (e instanceof JwtHeaderRequiresKid) return unauthorized('missing kid'); throw e }

Prevention

When it happens

Trigger: verifyWithJwks(token, options) on a token whose header omits kid — e.g. a symmetric-HS256 token or a custom-minted token without kid.

Common situations: Using verifyWithJwks for tokens signed with a shared secret (no kid); tokens from a legacy issuer that doesn't set kid; testing with manually crafted tokens; confusion between verify() (single fixed key) and verifyWithJwks() (key lookup by kid).

Related errors


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