Skip to main content
doc0blade47/honodocumentation home
Sign in

JWT and JWK

Last updated Oct 12, 2026
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Hono provides native JSON Web Token (JWT) and JSON Web Key (JWK) utilities alongside dedicated middleware handlers to authenticate HTTP requests securely across Web Standard runtimes. By leveraging crypto.subtle, the implementation handles cryptographic signing, signature verification, and dynamic key resolution without external cryptographic dependencies.

Sources: src/utils/jwt/jwt.ts:1-29, src/utils/jwt/jws.ts:1-12, src/middleware/jwt/jwt.ts:1-20, src/middleware/jwk/jwk.ts:1-16

Token Signing and Key Resolution

Signing Workflow and Serialisation

Token generation begins by serialising the payload and header objects into Base64Url-encoded strings without padding. The sign function first encodes the JWTPayload object using encodeJwtPart, which converts the JSON-serialised payload into UTF-8 bytes and transforms it via encodeBase64Url, stripping any trailing = characters.

const encodeJwtPart = (part: unknown): string =>
  encodeBase64Url(utf8Encoder.encode(JSON.stringify(part)).buffer).replace(/=/g, '')
const encodeSignaturePart = (buf: ArrayBufferLike): string => encodeBase64Url(buf).replace(/=/g, '')

Sources: src/utils/jwt/jwt.ts:31-34

Next, the header is constructed. If the supplied privateKey is an object containing an alg property, the algorithm and optional kid (Key ID) are extracted into the header. Otherwise, the provided algorithm parameter (defaulting to 'HS256') is used alongside typ: 'JWT'. The header and payload are concatenated with a dot delimiter (${encodedHeader}.${encodedPayload}) to form the partial token.

export const sign = async (
  payload: JWTPayload,
  privateKey: SignatureKey,
  alg: SignatureAlgorithm = 'HS256'
): Promise<string> => {
  const encodedPayload = encodeJwtPart(payload)
  let encodedHeader
  if (typeof privateKey === 'object' && 'alg' in privateKey) {
    alg = privateKey.alg as SignatureAlgorithm
    encodedHeader = encodeJwtPart({ alg, typ: 'JWT', kid: privateKey.kid })
  } else {
    encodedHeader = encodeJwtPart({ alg, typ: 'JWT' })
  }
 
  const partialToken = `${encodedHeader}.${encodedPayload}`
 
  const signaturePart = await signing(privateKey, alg, utf8Encoder.encode(partialToken))
  const signature = encodeSignaturePart(signaturePart)
 
  return `${partialToken}.${signature}`
}

Sources: src/utils/jwt/jwt.ts:56-76

Call-Chain Execution Walkthrough

When sign invokes the signing routine, execution flows through the JWS and Web Crypto resolution layers.

  1. sign in src/utils/jwt/jwt.ts invokes signing with the private key, algorithm identifier, and encoded partial token bytes.
  2. signing in src/utils/jwt/jws.ts calls getKeyAlgorithm(alg) to map the string algorithm to a Web Crypto KeyAlgorithm parameter structure.
  3. getKeyAlgorithm inspects the algorithm name; if an unsupported identifier is supplied, it falls through or throws JwtAlgorithmNotImplemented.
  4. signing then calls importPrivateKey(privateKey, algorithm) to prepare the cryptographic key instance.
  5. crypto.subtle.sign executes against the imported CryptoKey to produce the raw signature buffer, which is subsequently encoded and appended to the token string.

Sources: src/utils/jwt/jwt.ts:72-72, src/utils/jwt/jws.ts:29-37, src/utils/jwt/jws.ts:122-179, src/utils/jwt/types.ts:6-11

Web Crypto Key Resolution

The importPrivateKey function handles diverse key representations, including raw strings, PEM-encoded structures, JWK objects, and existing CryptoKey instances. It ensures that the resulting key is assigned the CryptoKeyUsage.Sign usage flag.

async function importPrivateKey(key: SignatureKey, alg: KeyImporterAlgorithm): Promise<CryptoKey> {
  if (!crypto.subtle || !crypto.subtle.importKey) {
    throw new Error('`crypto.subtle.importKey` is undefined. JWT auth middleware requires it.')
  }
  if (isCryptoKey(key)) {
    if (key.type !== 'private' && key.type !== 'secret') {
      throw new Error(
        `unexpected key type: CryptoKey.type is ${key.type}, expected private or secret`
      )
    }
    return key
  }
  const usages = [CryptoKeyUsage.Sign]
  if (typeof key === 'object') {
    return await crypto.subtle.importKey('jwk', key, alg, false, usages)
  }
  if (key.includes('PRIVATE')) {
    return await crypto.subtle.importKey('pkcs8', pemToBinary(key), alg, false, usages)
  }
  return await crypto.subtle.importKey('raw', utf8Encoder.encode(key), alg, false, usages)
}

Sources: src/utils/jwt/jws.ts:54-76

Warning

When passing an existing CryptoKey to importPrivateKey, its type property must be either 'private' or 'secret'. Passing a public key where a private or secret key is required causes an immediate error.

Sources: src/utils/jwt/jws.ts:58-64

Token Verification and Claims Validation

Claims Verification Options

Token validation begins with structural decoding and signature verification, separating cryptographic checks from payload claims assertions. The decode function splits the token by periods, verifying that exactly three parts exist before parsing the header and payload components. Sources: src/utils/jwt/jwt.ts:264-279

export const decode = (token: string): { header: TokenHeader; payload: JWTPayload } => {
  const parts = token.split('.')
  if (parts.length !== 3) {
    throw new JwtTokenInvalid(token)
  }
  try {
    const header = decodeJwtPart(parts[0]) as TokenHeader
    const payload = decodeJwtPart(parts[1]) as JWTPayload
    return {
      header,
      payload,
    }
  } catch {
    throw new JwtTokenInvalid(token)
  }
}

Sources: src/utils/jwt/jwt.ts:264-279

The VerifyOptions type configures how standard registered claims are validated against the current system time and expected constraints during token verification. Sources: src/utils/jwt/jwt.ts:78-94

OptionTypeDefaultPurpose
issstring | RegExp—Expected issuer used for verifying the iss claim
nbfbooleantrueVerify that the current timestamp is after or equal to the nbf claim
expbooleantrueVerify that the current timestamp is before the exp claim
iatbooleantrueVerify that the iat claim is a valid number not in the future
audstring | string[] | RegExp—Expected audience used for verifying the aud claim
algSignatureAlgorithm—Required signature algorithm for verification

Sources: src/utils/jwt/jwt.ts:78-94

Note

If the alg option is omitted during verification, the verifier immediately throws a JwtAlgorithmRequired error to prevent algorithm substitution vulnerabilities. Sources: src/utils/jwt/jwt.ts:99-101

JSON Web Key Set Middleware

JWKS Retrieval and Key Matching

The verifyWithJwks function retrieves and validates JSON Web Key Sets (JWKS) to verify incoming tokens without pre-configured static keys. It first decodes the token header and checks for a valid header structure and a mandatory kid (Key ID) property. If the header lacks a kid, JwtHeaderRequiresKid is thrown. Sources: src/utils/jwt/jwt.ts:209-216

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

Sources: src/utils/jwt/jwt.ts:197-216

Warning

Symmetric algorithms such as HS256, HS384, and HS512 are explicitly prohibited during JWKS verification. Any token header using a symmetric algorithm causes verifyWithJwks to throw a JwtSymmetricAlgorithmNotAllowed error to prevent algorithm confusion attacks. Sources: src/utils/jwt/jwt.ts:190-221

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

Sources: src/utils/jwt/jwt.ts:218-226

When an options.jwks_uri is supplied, verifyWithJwks fetches the URI using the optional RequestInit init argument, parses the JSON response, and ensures that a keys array exists. These fetched keys are merged with any statically provided options.keys. The verifier then finds the specific key where key.kid === header.kid. Sources: src/utils/jwt/jwt.ts:228-251

  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)
  }
 
  return await verify(token, matchingKey, {
    alg: header.alg,
    ...verifyOpts,
  })
}

Sources: src/utils/jwt/jwt.ts:228-262

Verification Execution and Call Chain

The execution flow for JWKS-based verification proceeds through structural header inspection and key resolution.

  1. verifyWithJwks — Entry point for verifying tokens against local keys or a remote JWKS endpoint. Sources: src/utils/jwt/jwt.ts:197-210
  2. decodeHeader — Splits the token by periods and decodes the first segment into a TokenHeader object. Sources: src/utils/jwt/jwt.ts:281-290
  3. JwtTokenInvalid — Thrown if the token does not contain exactly three parts or if part decoding fails. Sources: src/utils/jwt/jwt.ts:265-268, src/utils/jwt/jwt.ts:283-289

Sources: src/utils/jwt/jwt.ts:209-210, src/utils/jwt/jwt.ts:281-290

Note

If a matching JWK defines an explicit alg property, it must strictly match the alg declared in the JWT header. A mismatch causes a JwtAlgorithmMismatch exception. Sources: src/utils/jwt/jwt.ts:253-256

JWT Authentication Middleware Flow

Credential Extraction and Token Resolution

The jwt middleware intercepts incoming HTTP requests to locate and validate JSON Web Tokens from configured request headers or cookies. It first checks the request headers using options.headerName (defaulting to 'Authorization'). When credentials are found in the header, the middleware splits the string by whitespace and expects exactly two parts where the first segment case-insensitively equals 'bearer'. If the credential structure is invalid, it throws an HTTPException with status code 401. Sources: src/middleware/jwt/jwt.ts:76-95

return async function jwt(ctx, next) {
  const headerName = options.headerName || 'Authorization'
 
  const credentials = ctx.req.raw.headers.get(headerName)
  let token
  if (credentials) {
    const parts = credentials.split(/\s+/)
    if (parts.length !== 2 || parts[0].toLowerCase() !== 'bearer') {
      const errDescription = 'invalid credentials structure'
      throw new HTTPException(401, {
        message: errDescription,
        res: unauthorizedResponse({
          ctx,
          error: 'invalid_request',
          errDescription,
        }),
      })
    } else {
      token = parts[1]
    }

Sources: src/middleware/jwt/jwt.ts:76-95

If no header credentials exist, the middleware evaluates options.cookie. The cookie option can be provided as a plain string key or an object containing key, an optional secret for signed cookies, and optional prefixOptions. Depending on these options, the token is extracted using getCookie or getSignedCookie. Sources: src/middleware/jwt/jwt.ts:96-117

} else if (options.cookie) {
  if (typeof options.cookie == 'string') {
    token = getCookie(ctx, options.cookie)
  } else if (options.cookie.secret) {
    if (options.cookie.prefixOptions) {
      token = await getSignedCookie(
        ctx,
        options.cookie.secret,
        options.cookie.key,
        options.cookie.prefixOptions
      )
    } else {
      token = await getSignedCookie(ctx, options.cookie.secret, options.cookie.key)
    }
  } else {
    if (options.cookie.prefixOptions) {
      token = getCookie(ctx, options.cookie.key, options.cookie.prefixOptions)
    } else {
      token = getCookie(ctx, options.cookie.key)
    }
  }
}

Sources: src/middleware/jwt/jwt.ts:96-117

Warning

If neither header credentials nor a valid cookie yields a token string, the middleware immediately rejects the request with a 401 HTTP exception and an invalid_request error description. Sources: src/middleware/jwt/jwt.ts:119-129

if (!token) {
  const errDescription = 'no authorization included in request'
  throw new HTTPException(401, {
    message: errDescription,
    res: unauthorizedResponse({
      ctx,
      error: 'invalid_request',
      errDescription,
    }),
  })
}

Sources: src/middleware/jwt/jwt.ts:119-129

Verification, Context Injection, and 401 Responses

Once a token is extracted, the middleware invokes Jwt.verify with the configured secret, alg, and verification options. If verification fails or throws an error, a 401 exception is raised with an invalid_token error and a WWW-Authenticate challenge header. Upon successful verification, the decoded payload is stored in the execution context under the key 'jwtPayload', and control proceeds to the next handler via await next(). Sources: src/middleware/jwt/jwt.ts:131-157

  let payload
  let cause
  try {
    payload = await Jwt.verify(token, options.secret, {
      alg: options.alg,
      ...verifyOpts,
    })
  } catch (e) {
    cause = e
  }
  if (!payload) {
    throw new HTTPException(401, {
      message: 'Unauthorized',
      res: unauthorizedResponse({
        ctx,
        error: 'invalid_token',
        statusText: 'Unauthorized',
        errDescription: 'token verification failure',
      }),
      cause,
    })
  }
 
  ctx.set('jwtPayload', payload)
 
  await next()
}

Sources: src/middleware/jwt/jwt.ts:131-157

Unauthorized responses are constructed via unauthorizedResponse, which formats a 401 status response carrying a WWW-Authenticate header containing the request URL, error type, and error description. Sources: src/middleware/jwt/jwt.ts:160-173

function unauthorizedResponse(opts: {
  ctx: Context
  error: string
  errDescription: string
  statusText?: string
}) {
  return new Response('Unauthorized', {
    status: 401,
    statusText: opts.statusText,
    headers: {
      'WWW-Authenticate': `Bearer realm="${opts.ctx.req.url}",error="${opts.error}",error_description="${opts.errDescription}"`,
    },
  })
}

Sources: src/middleware/jwt/jwt.ts:160-173

Supported Cryptographic Algorithms and Errors

Supported JWA Signature Algorithms and Web Crypto Mappings

JSON Web Algorithms (JWA) are mapped to underlying Web Crypto API operations via getKeyAlgorithm in the JWS utility module. Supported algorithms encompass HMAC-SHA variants (HS256, HS384, HS512), RSA signatures using PKCS#1 v1.5 (RS256, RS384, RS512), RSA Probabilistic Signature Scheme (PS256, PS384, PS512), and Elliptic Curve Digital Signature Algorithm variants (ES256, ES384, ES512) alongside EdDSA.

AlgorithmJWA CategoryWeb Crypto Algorithm NameHash FunctionAdditional Parameters
HS256SymmetricHMACSHA-256—
HS384SymmetricHMACSHA-384—
HS512SymmetricHMACSHA-512—
RS256AsymmetricRSASSA-PKCS1-v1_5SHA-256—
RS384AsymmetricRSASSA-PKCS1-v1_5SHA-384—
RS512AsymmetricRSASSA-PKCS1-v1_5SHA-512—
PS256AsymmetricRSA-PSSSHA-256saltLength: 32
PS384AsymmetricRSA-PSSSHA-384saltLength: 48
PS512AsymmetricRSA-PSSSHA-512saltLength: 64
ES256AsymmetricECDSASHA-256namedCurve: 'P-256'
ES384AsymmetricECDSASHA-384namedCurve: 'P-384'
ES512AsymmetricECDSASHA-512namedCurve: 'P-521'
EdDSAAsymmetricEd25519——

Sources: src/utils/jwt/jwa.ts:7-21, src/utils/jwt/jws.ts:122-211

function getKeyAlgorithm(name: SignatureAlgorithm): KeyAlgorithm {
  switch (name) {
    case 'HS256':
      return {
        name: 'HMAC',
        hash: {
          name: 'SHA-256',
        },
      } satisfies HmacImportParams
    case 'HS384':
      return {
        name: 'HMAC',
        hash: {
          name: 'SHA-384',
        },
      } satisfies HmacImportParams
    case 'HS512':
      return {
        name: 'HMAC',
        hash: {
          name: 'SHA-512',
        },
      } satisfies HmacImportParams
    case 'RS256':
      return {
        name: 'RSASSA-PKCS1-v1_5',
        hash: {
          name: 'SHA-256',
        },
      } satisfies RsaHashedImportParams
    case 'RS384':
      return {
        name: 'RSASSA-PKCS1-v1_5',
        hash: {
          name: 'SHA-384',
        },
      } satisfies RsaHashedImportParams
    case 'RS512':
      return {
        name: 'RSASSA-PKCS1-v1_5',
        hash: {
          name: 'SHA-512',
        },
      } satisfies RsaHashedImportParams
    case 'PS256':
      return {
        name: 'RSA-PSS',
        hash: {
          name: 'SHA-256',
        },
        saltLength: 32, // 256 >> 3
      } satisfies RsaPssParams & RsaHashedImportParams
    case 'PS384':
      return {
        name: 'RSA-PSS',
        hash: {
          name: 'SHA-384',
        },
        saltLength: 48, // 384 >> 3
      } satisfies RsaPssParams & RsaHashedImportParams
    case 'PS512':
      return {
        name: 'RSA-PSS',
        hash: {
          name: 'SHA-512',
        },
        saltLength: 64, // 512 >> 3,
      } satisfies RsaPssParams & RsaHashedImportParams
    case 'ES256':
      return {
        name: 'ECDSA',
        hash: {
          name: 'SHA-256',
        },
        namedCurve: 'P-256',
      } satisfies EcdsaParams & EcKeyImportParams
    case 'ES384':
      return {
        name: 'ECDSA',
        hash: {

Sources: src/utils/jwt/jws.ts:122-211

Caution

For PS256, PS384, and PS512, the salt length is strictly derived from the hash bit length divided by eight (256 >> 3 yields 32, 384 >> 3 yields 48, and 512 >> 3 yields 64). Specifying a custom or non-standard salt length outside these Web Crypto mappings will cause signature verification or generation to fail. Sources: src/utils/jwt/jws.ts:166-194

Custom Error Types

JWT parsing, validation, and cryptographic routines throw specialized error classes defined in src/utils/jwt/types.ts. Each error sets a descriptive message and assigns its name property to match the class identifier.

Error ClassConstructor ArgumentsError Message Pattern
JwtAlgorithmNotImplementedalg: string${alg} is not an implemented algorithm
JwtAlgorithmRequirednoneJWT verification requires "alg" option to be specified
JwtAlgorithmMismatchexpected: string, actual: stringJWT algorithm mismatch: expected "${expected}", got "${actual}"
JwtTokenInvalidtoken: stringinvalid JWT token: ${token}
JwtTokenNotBeforetoken: stringtoken (${token}) is being used before it's valid
JwtTokenExpiredtoken: stringtoken (${token}) expired
JwtTokenIssuedAtcurrentTimestamp: number, iat: numberInvalid "iat" claim, must be a valid number lower than "${currentTimestamp}" (iat: "${iat}")
JwtTokenIssuerexpected: string | RegExp, iss: string | nullexpected issuer "${expected}", got ${iss ? "${iss}": 'none'}
JwtHeaderInvalidheader: objectjwt header is invalid: ${JSON.stringify(header)}
JwtHeaderRequiresKidheader: objectrequired "kid" in jwt header: ${JSON.stringify(header)}
JwtSymmetricAlgorithmNotAllowedalg: stringsymmetric algorithm "${alg}" is not allowed for JWK verification
JwtAlgorithmNotAllowedalg: string, allowedAlgorithms: readonly string[]algorithm "${alg}" is not in the allowed list: [...]
JwtTokenSignatureMismatchedtoken: stringtoken(${token}) signature mismatched
JwtPayloadRequiresAudpayload: objectrequired "aud" in jwt payload: ${JSON.stringify(payload)}
JwtTokenAudienceexpected: string | string[] | RegExp, aud: string | string[]expected audience "${...}", got "${aud}"

Sources: src/utils/jwt/types.ts:6-115

export class JwtAlgorithmNotImplemented extends Error {
  constructor(alg: string) {
    super(`${alg} is not an implemented algorithm`)
    this.name = 'JwtAlgorithmNotImplemented'
  }
}
 
export class JwtAlgorithmRequired extends Error {
  constructor() {
    super('JWT verification requires "alg" option to be specified')
    this.name = 'JwtAlgorithmRequired'
  }
}
 
export class JwtAlgorithmMismatch extends Error {
  constructor(expected: string, actual: string) {
    super(`JWT algorithm mismatch: expected "${expected}", got "${actual}"`)
    this.name = 'JwtAlgorithmMismatch'
  }
}
 
export class JwtTokenInvalid extends Error {
  constructor(token: string) {
    super(`invalid JWT token: ${token}`)
    this.name = 'JwtTokenInvalid'
  }
}
 
export class JwtTokenNotBefore extends Error {
  constructor(token: string) {
    super(`token (${token}) is being used before it's valid`)
    this.name = 'JwtTokenNotBefore'
  }
}
 
export class JwtTokenExpired extends Error {
  constructor(token: string) {
    super(`token (${token}) expired`)
    this.name = 'JwtTokenExpired'
  }
}
 
export class JwtTokenIssuedAt extends Error {
  constructor(currentTimestamp: number, iat: number) {
    super(
      `Invalid "iat" claim, must be a valid number lower than "${currentTimestamp}" (iat: "${iat}")`
    )
    this.name = 'JwtTokenIssuedAt'
  }
}
 
export class JwtTokenIssuer extends Error {
  constructor(expected: string | RegExp, iss: string | null) {
    super(`expected issuer "${expected}", got ${iss ? `"${iss}"` : 'none'} `)
    this.name = 'JwtTokenIssuer'
  }
}
 
export class JwtHeaderInvalid extends Error {
  constructor(header: object) {
    super(`jwt header is invalid: ${JSON.stringify(header)}`)
    this.name = 'JwtHeaderInvalid'
  }
}
 
export class JwtHeaderRequiresKid extends Error {
  constructor(header: object) {
    super(`required "kid" in jwt header: ${JSON.stringify(header)}`)
    this.name = 'JwtHeaderRequiresKid'
  }
}
 
export class JwtSymmetricAlgorithmNotAllowed extends Error {
  constructor(alg: string) {
    super(`symmetric algorithm "${alg}" is not allowed for JWK verification`)
    this.name = 'JwtSymmetricAlgorithmNotAllowed'
  }
}
 
export class JwtAlgorithmNotAllowed extends Error {

Sources: src/utils/jwt/types.ts:6-115

Was this page helpful?