http-jwt
Verifies a JSON Web Token (JWT) on incoming HTTP requests and attaches the decoded payload to request.internal and request.context. The token can be read from the Authorization: Bearer ... header (default) or from a cookie.
Two key sources are supported:
- A shared secret (
secretKey) for HMAC algorithms likeHS256. - A public key from
request.internal(internalKey), typically populated by@middy/kmswhen the signing key lives in AWS KMS. RSA, ECC NIST P-curve, and Ed25519 KMS key specs are mapped to the corresponding JWS algorithm automatically.
This middleware does not check role / scope / permission claims. See Validating roles below for a small custom middleware you can drop in alongside it.
Install
To install this middleware you can use NPM:
npm install --save @middy/http-jwt
npm install --save jose Options
secretKey(string) (optional): Symmetric secret used to verify the token. Required when nointernalKeyis provided. Must be paired withalgorithm.internalKey(string) (optional): Key onrequest.internalholding the verification key. Typically the key populated by@middy/kms({ publicKey, keySpec }). Required whensecretKeyis not set.algorithm(string) (optional): JWS algorithm to enforce (e.g.HS256,RS256,ES256,EdDSA). Required when usingsecretKey. When usinginternalKeywith a KMS-sourced key, the algorithm is inferred fromkeySpecif not provided.cookieName(string) (optional): When set, the token is read from this cookie name instead of theAuthorizationheader.audience(string | string[]) (optional): Expectedaudclaim. Verification fails if the token’s audience does not match.issuer(string | string[]) (optional): Expectedissclaim.clockTolerance(number) (default0): Clock skew tolerance in seconds applied toexp/nbfchecks.payloadKey(string) (defaultjwt): Key under which the decoded payload is stored on bothrequest.internalandrequest.context.
NOTES:
- A missing or malformed token, an invalid signature, or a failed claim check throws a
401 Unauthorized. Pair withhttp-error-handlerto convert it into a proper HTTP response. - Exactly one of
secretKeyorinternalKeymust be provided.
Sample usage
With a shared secret (HS256)
import middy from '@middy/core'
import httpJwt from '@middy/http-jwt'
import httpErrorHandler from '@middy/http-error-handler'
const lambdaHandler = (event, context) => {
// context.jwt holds the decoded payload
return { statusCode: 200, body: JSON.stringify({ sub: context.jwt.sub }) }
}
export const handler = middy()
.use(
httpJwt({
secretKey: process.env.JWT_SECRET,
algorithm: 'HS256',
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: 5,
}),
)
.use(httpErrorHandler())
.handler(lambdaHandler) With a KMS-hosted public key
Pair @middy/kms with @middy/http-jwt to verify tokens that were signed with an AWS KMS asymmetric key. The KMS middleware fetches the public key once per cold start and caches it; http-jwt reads it via internalKey and derives the algorithm from the key spec.
import middy from '@middy/core'
import kms from '@middy/kms'
import httpJwt from '@middy/http-jwt'
import httpErrorHandler from '@middy/http-error-handler'
const lambdaHandler = (event, context) => {
return { statusCode: 200, body: JSON.stringify({ sub: context.jwt.sub }) }
}
export const handler = middy()
.use(
kms({
fetchData: {
jwtKey: 'alias/jwt-signing-key',
},
}),
)
.use(
httpJwt({
internalKey: 'jwtKey',
issuer: 'https://auth.example.com',
audience: 'api.example.com',
}),
)
.use(httpErrorHandler())
.handler(lambdaHandler) Reading the token from a cookie
httpJwt({
secretKey: process.env.JWT_SECRET,
algorithm: 'HS256',
cookieName: 'session',
}) Validating roles
@middy/http-jwt only verifies the signature and standard claims (iss, aud, exp, nbf). Role / scope / permission claims are application-specific and intentionally left to userland. The following inline middleware reads the decoded payload from request.context (under payloadKey) and rejects the request when the required role is missing.
import middy from '@middy/core'
import httpJwt from '@middy/http-jwt'
import httpErrorHandler from '@middy/http-error-handler'
import { createError } from '@middy/util'
const requireRole = (requiredRole, { payloadKey = 'jwt', claim = 'roles' } = {}) => ({
before: (request) => {
const payload = request.context[payloadKey]
const roles = payload?.[claim]
const has = Array.isArray(roles)
? roles.includes(requiredRole)
: roles === requiredRole
if (!has) {
throw createError(403, 'Forbidden', {
cause: { package: 'custom/require-role', data: `Missing role: ${requiredRole}` },
})
}
},
})
const lambdaHandler = (event, context) => {
return { statusCode: 200, body: JSON.stringify({ ok: true }) }
}
export const handler = middy()
.use(httpJwt({ secretKey: process.env.JWT_SECRET, algorithm: 'HS256' }))
.use(requireRole('admin'))
.use(httpErrorHandler())
.handler(lambdaHandler) Order matters: requireRole must run after httpJwt so the decoded payload is already on the context.
Bundling
jose is a peer dependency. To keep it out of your Lambda bundle, add jose to your bundler’s exclude list and provide it via a Lambda Layer.