http-paseto
Verifies a PASETO v4.public token 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.
The verification key is read from request.internal under internalKey, typically populated by @middy/kms when the Ed25519 signing key lives in AWS KMS.
Only v4.public (Ed25519-signed) tokens are accepted. v4.local, v3.*, v2.*, and v1.* are rejected with 401 Unauthorized.
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-paseto
npm install --save paseto Options
internalKey(string) (required): Key onrequest.internalholding the verification key. Typically the key populated by@middy/kms({ publicKey, keySpec }wherekeySpecisECC_NIST_ED25519).cookieName(string) (optional): When set, the token is read from this cookie name instead of theAuthorizationheader.audience(string) (optional): Expectedaudclaim.issuer(string) (optional): Expectedissclaim.clockTolerance(string) (optional): Clock skew tolerance forwarded topaseto’sV4.verify(e.g."5 seconds"). See the paseto docs for accepted formats.payloadKey(string) (defaultpaseto): Key under which the decoded payload is stored on bothrequest.internalandrequest.context.
NOTES:
- A missing or malformed token, an unsupported version/purpose, an invalid signature, or a failed claim check throws a
401 Unauthorized. Pair withhttp-error-handlerto convert it into a proper HTTP response. - The KMS key behind a PASETO
v4.publicdeployment must be an Ed25519 key (ECC_NIST_ED25519).
Sample usage
With a KMS-hosted public key
import middy from '@middy/core'
import kms from '@middy/kms'
import httpPaseto from '@middy/http-paseto'
import httpErrorHandler from '@middy/http-error-handler'
const lambdaHandler = (event, context) => {
// context.paseto holds the decoded payload
return { statusCode: 200, body: JSON.stringify({ sub: context.paseto.sub }) }
}
export const handler = middy()
.use(
kms({
fetchData: {
pasetoKey: 'alias/paseto-signing-key',
},
}),
)
.use(
httpPaseto({
internalKey: 'pasetoKey',
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: '5 seconds',
}),
)
.use(httpErrorHandler())
.handler(lambdaHandler) Reading the token from a cookie
httpPaseto({
internalKey: 'pasetoKey',
cookieName: 'session',
}) Validating roles
@middy/http-paseto only verifies the signature and standard claims (iss, aud, exp, nbf). Role / scope / permission claims are application-specific. 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 kms from '@middy/kms'
import httpPaseto from '@middy/http-paseto'
import httpErrorHandler from '@middy/http-error-handler'
import { createError } from '@middy/util'
const requireRole = (requiredRole, { payloadKey = 'paseto', 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(kms({ fetchData: { pasetoKey: 'alias/paseto-signing-key' } }))
.use(httpPaseto({ internalKey: 'pasetoKey' }))
.use(requireRole('admin'))
.use(httpErrorHandler())
.handler(lambdaHandler) Order matters: requireRole must run after httpPaseto so the decoded payload is already on the context.
Bundling
paseto is a peer dependency. To keep it out of your Lambda bundle, add paseto to your bundler’s exclude list and provide it via a Lambda Layer.