Middleware Reference
Every middleware exported by auth-h3client is documented here. Import them from auth-h3client/v1 or auth-h3client/v2. When using the Nuxt module, all middleware is auto-imported inside the server/ directory and the global chain is wired automatically. For other setups, register the global middlewares and route-level wrappers explicitly as shown in the H3 or Nitro setup.
configuration(...) exactly once at startup before any middleware runs.For the global middleware stack order applied automatically, see Security: Request Lifecycle. For route-level middleware chains per registered endpoint, see the Routes Reference.
Authentication guards
These middleware and wrappers enforce token presence, session validity, and HMAC signature verification before your handler runs.
ensureValidCredentials
The core token rotation middleware. Checks the access token expiry state and rotates both the access and refresh tokens when needed by calling the IAM /auth/user/refresh-session endpoint. Deduplicates concurrent rotation attempts on the same session so that only one rotation call is sent to the IAM service even if multiple requests arrive simultaneously.
On success, sets event.context.accessToken and event.context.session. Returns a 202 MFA response rather than throwing when the session requires step-up verification.
Reads
| Source | Value |
|---|---|
__Secure-a cookie | The current access token |
session cookie | The current refresh token |
canary_id cookie | The visitor fingerprint for anomaly binding |
Sets
| Target | Value |
|---|---|
event.context.accessToken | The current (or newly rotated) access token string |
event.context.session | The current session cookie value |
Responses on failure
| Status | Meaning |
|---|---|
202 | MFA challenge, step-up verification required |
401 | No valid session found |
429 | IAM service rate limit exceeded |
defineAuthenticatedEventHandler or defineOptionalAuthenticationEvent. Both wrappers call it internally.hmacSignatureMiddleware
Generates HMAC-SHA256 signature headers for every outbound request the module sends to the IAM service. Reads the current request method and URL path, computes the signature using the sharedSecret from the configuration, and stores the resulting headers on event.context.authHeaders. serviceToService reads those headers when forwarding requests to the IAM service.
Reads
| Source | Value |
|---|---|
| Request method | HTTP verb of the current inbound request |
| Request URL path | Path and query string of the current inbound request |
Sets
| Target | Value |
|---|---|
event.context.authHeaders | { 'X-Client-Id', 'X-Timestamp', 'X-Request-Id', 'X-Signature' } |
enableHmac: true in the configuration. When disabled, no headers are added. See the HMAC guide for setup and the IAM HMAC documentation for the server-side signature verification.CSRF middleware
generateCsrfCookie
Mints a signed __Host-csrf cookie when one is not already present on the request. The token is a 32-byte random hex string, signed with an expiring HMAC payload using cryptoCookiesSecret.
Cookie attributes
| Attribute | Value |
|---|---|
| Name | __Host-csrf |
HttpOnly | false (the client must read it to inject it as a header) |
SameSite | Strict |
Secure | true |
MaxAge | 1800 seconds |
Runs automatically in global middleware. Also runs on all GET magic link routes to prepare a fresh cookie for the page that will be rendered.
verifyCsrfCookie
Validates the __Host-csrf cookie signature and expiry, then checks that the X-CSRF-Token request header contains a value that matches the token stored inside the cookie payload. The comparison uses isSameBuffer for timing-safe equality to prevent timing side-channel attacks.
Reads
| Source | Value |
|---|---|
__Host-csrf cookie | The signed CSRF token |
X-CSRF-Token header | The raw token submitted by the client |
Responses on failure
| Status | Code | Meaning |
|---|---|---|
403 | CSRF_MISSING | The __Host-csrf cookie is absent |
403 | CSRF_INVALID | Cookie signature or expiry check failed |
403 | TOKEN_INVALID | Header value does not match the cookie payload |
See CSRF for the double-submit pattern and executeRequest integration.
Bot detection middleware
isIPValid
Extracts and validates the client IP address from the request headers using net.isIP(). Does not make any network call. Runs as the first step in the global middleware chain.
Reads
| Source | Value |
|---|---|
| Request headers | Client IP address |
Responses on failure
| Status | Meaning |
|---|---|
403 | Invalid or missing IP address |
botDetectorMiddleware
Forwards the visitor fingerprint to the IAM service /check endpoint. Returns HTTP 403 when the visitor's bot score exceeds the configured ban threshold. When enableFireWallBans: true, calls banIp to block the IP at the operating system firewall level.
On the first request from a visitor, the middleware calls /check and sets __Host-dr_i_n, a signed host-only cookie. On every subsequent request where both __Host-dr_i_n and canary_id are present and the __Host-dr_i_n signature is valid, the middleware skips calling the IAM service entirely. If the signature verification fails, the request is rejected with 403 and code CANARY_TEMPERING.
canary_id cookie issued by the IAM service and the __Host-dr_i_n cookie set by this middleware are required for session operations including token rotation. Disabling bot detection means these cookies are never set and subsequent IAM calls will fail.Reads
| Source | Value |
|---|---|
canary_id cookie | Visitor fingerprint forwarded to IAM /check |
__Host-dr_i_n cookie | Signed tracking cookie: if present and valid, skips IAM call |
Sets
| Target | Value |
|---|---|
__Host-dr_i_n cookie | HMAC-signed tracking cookie (HttpOnly, SameSite: Strict, Secure, 2-hour TTL) |
event.context.trackingResult | Parsed JSON response from the IAM /check endpoint |
Responses on failure
| Status | Code | Meaning |
|---|---|---|
403 | NOT_ALLOWED | Visitor bot score exceeds ban threshold |
403 | CANARY_TEMPERING | __Host-dr_i_n cookie signature verification failed |
502 | AUTH_SERVER_ERROR | IAM /check endpoint unreachable |
See Bot Detection for the full flow and the enableFireWallBans configuration.
OAuth middleware
OAuthTokensValidations
Validates the OAuth provider callback before the success handler runs. Reads the authorization code and state from the request (query string for GET, form body for POST), verifies the signed state cookie, and for OIDC providers verifies the ID token signature against the provider's JWKS endpoint, the nonce, azp, and at_hash claims, and that userinfo.sub matches the ID token subject. On success, sets event.context.provider, event.context.userData, and event.context.accessToken.
Called automatically by useOAuthRoutes as the first middleware in the /oauth/callback/:provider chain. Export it directly when building a custom OAuth callback route.
Reads
| Source | Value |
|---|---|
state{provider} cookie | Signed state token for CSRF protection during the OAuth flow |
pkce_v{provider} cookie | PKCE code verifier (when supportPKCE: true) |
nonce{provider} cookie | Nonce for OIDC ID token verification |
| Query / form body | code, state, error, iss |
Sets
| Target | Value |
|---|---|
event.context.provider | The matched provider name from configuration |
event.context.userData | User info object from the OAuth provider |
event.context.accessToken | OAuth access token from the provider |
Responses on failure
| Status | Meaning |
|---|---|
302 | OAuth error from provider, redirects to redirectUrlOnError |
400 | State mismatch, missing code, or token validation failure |
500 | Token exchange or JWKS verification error |
See OAuth and OIDC for provider configuration and the full OIDC verification sequence.
Request validation middleware
limitBytes(maxBytes)
Factory function. Returns a middleware that reads the raw request body and throws HTTP 403 with code INVALID_CONTENT_TYPE if the byte length exceeds maxBytes. Pass 0 to reject any body entirely. Applied before JSON parsing, so oversized payloads cannot consume memory during deserialization.
Import
import { limitBytes } from 'auth-h3client'
Signature
function limitBytes(maxBytes: number): EventHandler
Usage
await limitBytes(1024)(event) // Reject bodies over 1 KB
await limitBytes(0)(event) // Reject any body
Responses on failure
| Status | Meaning |
|---|---|
403 | Body exceeds maxBytes (error code: INVALID_CONTENT_TYPE) |
contentType(expected)
Factory function. Returns a middleware that validates the Content-Type request header against the expected value. Throws HTTP 403 if the header is missing or does not match.
Import
import { contentType } from 'auth-h3client'
Signature
function contentType(expected: string): EventHandler
Usage
await contentType('application/json')(event)
Responses on failure
| Status | Code | Meaning |
|---|---|---|
403 | INVALID_CONTENT_TYPE | Content-Type missing or does not match expected value |
defineByteLimiterHandler(handler, limitBytesTo, method)
Higher-order wrapper that asserts the request method, reads the raw request
body exactly once, rejects bodies larger than limitBytesTo, parses JSON into
event.context.body, and then calls your handler. The API token management
wrapper uses this parser so request-size checks happen before any
action runs.
If the request body is empty, the wrapper leaves event.context.body
undefined and continues. If Content-Length is present and already exceeds the
limit, the request is rejected before the full body is processed.
Parameters
| Parameter | Type | Description |
|---|---|---|
handler | EventHandler | Your H3 handler |
limitBytesTo | number | Maximum accepted request body size in bytes |
method | 'POST' | 'PUT' | 'PATCH' | The only HTTP method accepted by the wrapper |
Sets
| Target | Value |
|---|---|
event.context.body | Parsed JSON object, or undefined for an empty body |
Responses on failure
| Status | Meaning |
|---|---|
400 | JSON parse failed |
403 | Body exceeds limitBytesTo |
405 | Wrong HTTP method |
export default defineByteLimiterHandler(async (event) => {
const body = event.context.body
return { ok: true, body }
}, 2048, 'POST')
Event handler wrappers
Wrappers are higher-order functions that accept your handler and return a new handler with enforcement built in. Import them from auth-h3client/v1 or auth-h3client/v2. When using the Nuxt module, they are auto-imported inside the server/ directory.
defineAuthenticatedEventHandler(handler)
The standard wrapper for protected routes. Runs hmacSignatureMiddleware, then ensureValidCredentials (token rotation), then calls getCachedUserData against the IAM service. Populates event.context.authorizedData with the verified session data. Throws HTTP 401 on failure. Returns HTTP 202 with mfaRequired when the session requires step-up verification.
Sets
| Target | Value |
|---|---|
event.context.authorizedData | { authorized, userId?, roles?, ipAddress, userAgent, date, reason?, error?, message? } |
Responses on failure
| Status | Meaning |
|---|---|
202 | MFA challenge, step-up verification required |
401 | Not authenticated or invalid session |
429 | Rate limit exceeded |
500 | IAM service error |
export default defineAuthenticatedEventHandler(async (event) => {
const { userId, roles } = event.context.authorizedData
return { userId, roles }
})
defineOptionalAuthenticationEvent(handler)
Attempts authentication using the same internal pipeline as defineAuthenticatedEventHandler. On success, populates event.context.authorizedData. On any auth failure other than a rate limit, sets event.context.authorizedData to undefined and continues to your handler as a guest. HTTP 429 responses are still propagated.
Sets
| Target | Value |
|---|---|
event.context.authorizedData | Verified session data, or undefined for unauthenticated requests |
Responses on failure
| Status | Meaning |
|---|---|
429 | Rate limit exceeded (always propagated, even for guests) |
export default defineOptionalAuthenticationEvent(async (event) => {
const user = event.context.authorizedData // undefined for guests
return user ? { content: 'private', userId: user.userId } : { content: 'public' }
})
defineVerifiedCsrfHandler(handler)
Calls verifyCsrfCookie before running the handler. Does not check authentication. Use this when you need CSRF protection on a route that is accessible to both guests and authenticated users.
Responses on failure
| Status | Meaning |
|---|---|
403 | CSRF cookie missing, invalid, or header mismatch |
export default defineVerifiedCsrfHandler(async (event) => {
const body = await readBody(event)
return { ok: true }
})
defineAuthenticatedEventPostHandlers(handler)
Combines defineAuthenticatedEventHandler, defineVerifiedCsrfHandler, and assertMethod('POST') in that order. The correct choice for any state-changing endpoint that requires authentication.
Responses on failure
| Status | Meaning |
|---|---|
202 | MFA challenge |
401 | Not authenticated |
403 | CSRF failure or wrong HTTP method |
429 | Rate limit exceeded |
export default defineAuthenticatedEventPostHandlers(async (event) => {
const { userId } = event.context.authorizedData
await updateSettings(userId, await readBody(event))
return { ok: true }
})
defineAuthenticatePublicApi(handler, userPrivilege)
Protects a route with API token verification. The wrapper
reads X-API-KEY from the incoming request, forwards it to the IAM
/api/public/verify endpoint together with the privilege floor defined by
userPrivilege, and only then calls your handler.
defineAuthenticatePublicApi behind the Nuxt
global auth middleware or a manual isIPValid -> botDetectorMiddleware -> generateCsrfCookie chain. These requests are machine-to-machine API-key
verification calls, so applying the browser session middleware can lead to
bot-detector rate limits or bans. Keep the global middleware for regular auth
routes, getApiListsController, and defineApiManagementHandler, because
those browser session flows still need bot detection and the CSRF cookie.On success, the wrapper stores the verified token metadata on
event.context.apiVerification. On failure, it returns a normalized JSON
response instead of throwing.
Reads
| Source | Value |
|---|---|
X-API-KEY header | Raw API token presented by the caller |
userPrivilege argument | Minimum privilege level your route requires |
Sets
| Target | Value |
|---|---|
event.context.apiVerification | { name, tokenId, userId, createdAt, expiresAt, lastUsed, usageCount, providedPrivilege } |
Responses on failure
| Status | Meaning |
|---|---|
401 | API key missing |
429 | Too many invalid verification attempts |
| IAM status | IAM verification rejected the token |
500 | IAM service unreachable or returned no response |
export default defineAuthenticatePublicApi(async (event) => {
const token = event.context.apiVerification
return { ok: true, tokenId: token.tokenId, userId: token.userId }
}, 'demo')
defineApiManagementHandler(handler, allowedPrivilege, updateToNewPrivilege?)
Builds a complete authenticated POST pipeline for API token management routes.
It wraps defineAuthenticatedEventPostHandlers(...) with
defineByteLimiterHandler(..., 2000, 'POST'), validates
event.context.params against the supported actions, and then proxies the
request to the IAM API token management endpoints. This means the raw request
body is size-checked and parsed into event.context.body before the
action-specific logic runs.
Supported actions are new-token, revoke, metadata, rotate,
ip-restriction-update, and privilege-update.
list-metadata on POST because token listing is handled by
getApiListsController over GET.For every action except new-token, the wrapper first calls
/api/manage/list-metadata to resolve the token's public_identifier. This
means the client submits only tokenId, and the wrapper maps that token ID to
publicIdentifier and name before it calls IAM.
allowedPrivilege controls the privilege that will be assigned to newly
created tokens. updateToNewPrivilege is optional, but it must be provided if
you want the privilege-update action to succeed.
Accepted request bodies
| Action | Body accepted by defineApiManagementHandler | IAM body sent by the wrapper | See |
|---|---|---|---|
new-token | { name, prefix, ipv4?, expires? } | { name, prefix, ipv4?, expires?, privilege: allowedPrivilege } | Creating Tokens |
revoke | { tokenId } | { tokenId, publicIdentifier, name } | Revocation |
metadata | { tokenId } | { tokenId, publicIdentifier, name } | Metadata |
rotate | { tokenId } | { tokenId, publicIdentifier, name } | Rotation |
ip-restriction-update | { tokenId, ipv4? } | { tokenId, publicIdentifier, name, ipv4? } | IP Restriction |
privilege-update | { tokenId } | { tokenId, publicIdentifier, name, newPrivilege: updateToNewPrivilege } | Privileges |
list-metadata | Rejected on POST | Not sent | Token Listing |
The wrapper schema requires prefix on new-token and validates
ip-restriction-update with an optional ipv4 array.
The client request body never supplies privilege or newPrivilege. Your
application decides those values through the allowedPrivilege and
updateToNewPrivilege arguments.
On new-token, IAM returns rawPublicId in addition to the raw key. The
wrapper removes rawPublicId before it populates event.context.newApiToken.
Sets
| Action | event.context field | Shape |
|---|---|---|
new-token | newApiToken | { rawApiKey, expiresAt } |
ip-restriction-update | ipRestrictionUpdate | { msg } |
privilege-update | privilegeUpdate | { msg } |
revoke | revoke | string or { msg, invalidedTokenId, userId } |
metadata | extensiveMetadata | { tokenMeta, counts } |
rotate | rotate | { msg, newRawToken, newExpiry } |
Responses on failure
| Status | Meaning |
|---|---|
202 | MFA challenge pending |
400 | Invalid JSON body or list-metadata attempted on POST |
401 | Session missing, unauthorized action, or token not owned by the user |
403 | CSRF failure, oversized body, or privilege-update not permitted |
404 | Invalid route action or invalid action-specific input |
429 | Rate limited by IAM |
| IAM status | IAM action failed and returned { ok: false, reason } |
export default defineApiManagementHandler(async (event) => {
const action = event.context.params?.action
if (action === 'new-token') {
return { ok: true, data: event.context.newApiToken }
}
if (action === 'metadata') {
return { ok: true, data: event.context.extensiveMetadata }
}
return { ok: true }
}, 'demo', 'protected')
defineVerifiedMagicLinkGetHandler(handler)
Validates incoming magic link query parameters before running the handler. Enforces GET method, checks that the canary_id, session, and __Secure-a cookies are present, validates the query string against VerificationLinkSchema (visitor, token, random, reason), and calls the IAM service to verify the link. Populates event.context.link and event.context.reason on success.
Sets
| Target | Value |
|---|---|
event.context.link | The link action type string (e.g., 'Custom MFA') |
event.context.reason | The link reason string |
Responses on failure
| Status | Meaning |
|---|---|
400 | Link invalid, expired, or already used (forwarded from IAM) |
405 | Not a GET request |
401 | Required session cookies missing |
export default defineVerifiedMagicLinkGetHandler(async (event) => {
const { reason } = event.context
return { ok: true, reason }
})
__Host-csrf cookie will be present yet.defineMfaCodeVerifierHandler(handler)
Validates an MFA code submission after a magic link is verified. Enforces POST method, verifies the CSRF cookie, limits the request body to 8 MB, validates link query parameters against VerificationLinkSchema, reads event.context.body.code (a 7-digit numeric string), and sends the code to the IAM service for verification. On success, new tokens are issued and applied to the response cookies, and event.context.limitedMetaData is populated with the verified session metadata before your handler runs.
Reads
| Source | Value |
|---|---|
event.context.body.code | The 7-digit OTP code submitted by the user |
| Query parameters | token, random, reason, visitor |
Sets
| Target | Value |
|---|---|
event.context.limitedMetaData | Verified session metadata returned by the IAM service |
Responses on failure
| Status | Meaning |
|---|---|
400 | Malformed code, invalid or expired code, or link parameter mismatch |
403 | CSRF failure or user banned |
405 | Not a POST request |
403 | Body exceeds 8 MB (error code: INVALID_CONTENT_TYPE) |
export default defineMfaCodeVerifierHandler(async (event) => {
// Code verified, new tokens already applied to the response
return { ok: true }
})
defineDeduplicatedEventHandler(handler)
Wraps a handler with request-level deduplication using lockAsyncAction. Concurrent requests with the same session identity are coalesced so that only one execution runs at a time. Results are cached briefly to serve requests that arrive after the first completes.
The built-in login, logout, signup, password reset, and MFA handlers already use this wrapper internally.
export default defineDeduplicatedEventHandler(async (event) => {
await processCheckout(event)
return { ok: true }
})
Setup
defineAuthConfiguration(nitro, config) v1 only
One-call Nitro plugin setup function. Validates and freezes the configuration, registers httpLogger, mounts all four route registrars (useAuthRoutes, useOAuthRoutes, bounceRouter, magicLinksRouter with 'api' prefix) on nitro.router, and logs a startup confirmation. Use this inside a Nitro plugin to wire everything in a single call instead of registering each piece manually.
import { defineAuthConfiguration } from 'auth-h3client/v1'
export default defineNitroPlugin((nitro) => {
defineAuthConfiguration(nitro, {
server: { auth_location: 'https://iam.example.com' },
// ...
})
})
Parameters
| Parameter | Type | Description |
|---|---|---|
nitro | NitroApp | The Nitro app instance provided by the plugin callback |
config | Configuration | The full configuration object, passed to configuration(config) internally |
auth-h3client/v1. For H3 v2 setups, call configuration() and each route registrar individually.Error utilities
throwHttpError(log, event, code, status, title, message)
Logs an error with structured context and throws an H3 error with the given status, title, and message. Use this inside custom handlers to produce consistent JSON error responses.
Import
import { throwHttpError } from 'auth-h3client'
Parameters
getLogger().child({...}) to create a scoped logger.throwHttpError(log, event, 'FORBIDDEN', 403, 'Forbidden', 'Access denied')
notFoundHandler
Returns a 404 JSON response. Used as a fallback inside handlers that receive an invalid magic link or an unknown route.
Logging middleware
httpLogger()
Returns an H3 middleware function that logs every request and response as structured JSON using pino. Register it on your H3 app or Nitro instance during startup, or rely on the Nuxt module to register it automatically. See Logging for v1 and v2 registration examples.
Features
- Assigns a unique
X-Request-Idheader per request (or passes through an existing one) - Selects log level by response status:
infofor 2xx/3xx,warnfor 4xx,errorfor 5xx - Skips logging for static asset paths
See Logging for configuring the logger level.
Wrapper quick reference
| Wrapper | HMAC | Token rotation | Session check | CSRF | Method |
|---|---|---|---|---|---|
defineAuthenticatedEventHandler | Yes | Yes | Yes | No | Any |
defineOptionalAuthenticationEvent | Yes | Yes | Yes (optional) | No | Any |
defineVerifiedCsrfHandler | No | No | No | Yes | Any |
defineAuthenticatedEventPostHandlers | Yes | Yes | Yes | Yes | POST only |
defineAuthenticatePublicApi | Yes | No | API key verification | No | Any |
defineApiManagementHandler | Yes | Yes | Yes | Yes | POST only |
defineVerifiedMagicLinkGetHandler | No | No | No | No | GET only |
defineMfaCodeVerifierHandler | No | No | No | Yes | POST only |
defineDeduplicatedEventHandler | No | No | No | No | Any |