Route Protection
The module exports a set of event handler wrappers that enforce
authentication, API-key verification, CSRF requirements, request-body limits,
and method requirements before your handler runs. Each wrapper is a
higher-order function that accepts your handler and returns a new handler with
the enforcement logic built in. Import them from auth-h3client (defaults to
v1) or auth-h3client/v2 depending on your H3 version. When using the Nuxt
module, all wrappers are auto-imported inside the server/ directory.
The API token flows use two dedicated wrappers:
- machine-to-machine protection with
defineAuthenticatePublicApi - authenticated browser-side token management with
defineApiManagementHandler
Wrapper overview
| Wrapper | Session auth | API key | CSRF | Method |
|---|---|---|---|---|
defineAuthenticatedEventHandler | Yes | No | No | Any |
defineOptionalAuthenticationEvent | Optional | No | No | Any |
defineVerifiedCsrfHandler | No | No | Yes | Any |
defineAuthenticatedEventPostHandlers | Yes | No | Yes | POST only |
defineAuthenticatePublicApi | No | Yes | No | Any |
defineApiManagementHandler | Yes | No | Yes | POST only |
defineByteLimiterHandler | No | No | No | POST, PUT, or PATCH |
defineVerifiedMagicLinkGetHandler | No | No | No | GET only |
defineMfaCodeVerifierHandler | No | No | Yes | POST only |
defineDeduplicatedEventHandler | No | No | No | Any |
defineAuthenticatedEventHandler
The standard wrapper for protected routes. It runs token rotation via ensureValidCredentials, verifies the session against the IAM service, and populates event.context.authorizedData with the verified user data. A missing or invalid session throws HTTP 401. An MFA challenge returns HTTP 202 with a mfaRequired body.
export default defineAuthenticatedEventHandler(async (event) => {
const { userId, roles } = event.context.authorizedData
return { userId, roles }
})
event.context.authorizedData is typed as ServerResponse:
interface ServerResponse {
authorized: boolean
userId?: string
roles?: string[] | string
ipAddress: string
userAgent: string
date: string
reason?: string
error?: string
message?: string
}
defineOptionalAuthenticationEvent
Use this wrapper for routes that serve both authenticated users and guests. It attempts authentication and populates event.context.authorizedData when successful. If authentication fails for any reason other than a rate limit, it sets event.context.authorizedData to undefined and continues to your handler as a guest. HTTP 429 rate limit responses are still propagated.
export default defineOptionalAuthenticationEvent(async (event) => {
const user = event.context.authorizedData // undefined for guests
if (user) {
return { content: 'private content', userId: user.userId }
}
return { content: 'public content' }
})
defineVerifiedCsrfHandler
Validates the CSRF cookie and the X-CSRF-Token request header before running the handler. Does not check authentication. Use this wrapper when you need CSRF protection without requiring a login, for example on forms accessible to both guests and authenticated users.
export default defineVerifiedCsrfHandler(async (event) => {
const body = await readBody(event)
// CSRF is valid; process the form submission
return { ok: true }
})
defineAuthenticatedEventPostHandlers
Combines authentication, CSRF validation, and a POST method assertion in one wrapper. This is the correct choice for any state-changing endpoint that requires a login.
export default defineAuthenticatedEventPostHandlers(async (event) => {
const { userId } = event.context.authorizedData
await deleteAccount(userId)
return { ok: true }
})
This wrapper combines defineAuthenticatedEventHandler,
defineVerifiedCsrfHandler, and assertMethod('POST') in that order.
defineApiManagementHandler builds on this wrapper for API token lifecycle
operations, then adds a 2 KB JSON body limit, action validation, and token
identity mapping in the server layer.
defineAuthenticatePublicApi
Use this wrapper for machine-to-machine routes that accept an API token in the
X-API-KEY header instead of a browser session. The wrapper forwards that key
to the IAM /api/public/verify endpoint, along with the privilege you
define for the route, and only then calls your handler.
defineAuthenticatePublicApi on routes that bypass the Nuxt global auth
middleware or any manual isIPValid -> botDetectorMiddleware -> generateCsrfCookie chain. This wrapper protects machine-to-machine API-key
requests, so running the browser middleware on the same route can trigger
bot-detector rate limits or bans. Keep the global middleware for regular auth
routes, getApiListsController, and defineApiManagementHandler, because
those browser session flows still depend on bot detection and the CSRF cookie.On success, the wrapper populates event.context.apiVerification with the
verified token metadata. On failure, it returns a normalized JSON response and
uses the same status code returned by IAM. Rate limits are only applied to
invalid or abusive verification attempts. Successful verification calls are not rate
limited by the wrapper.
export default defineAuthenticatePublicApi(async (event) => {
const token = event.context.apiVerification
return {
ok: true,
tokenId: token.tokenId,
userId: token.userId,
privilege: token.providedPrivilege,
}
}, 'demo')
event.context.apiVerification is typed as VerifySuccessResponse:
interface VerifySuccessResponse {
name: string
tokenId: number
userId: number
createdAt: string
expiresAt: string
lastUsed: string
usageCount: number
providedPrivilege: 'custom' | 'demo' | 'restricted' | 'protected' | 'full'
}
defineApiManagementHandler
Use this wrapper for authenticated POST routes that create, rotate, revoke, or
inspect API tokens on behalf of the logged-in user. The implementation wraps
defineAuthenticatedEventPostHandlers(...) with
defineByteLimiterHandler(..., 2000, 'POST'), so the raw body is size-checked
and parsed into event.context.body before the action-specific IAM proxy logic
runs.
The wrapper supports these actions:
new-tokenrevokemetadatarotateip-restriction-updateprivilege-update
The wrapper accepts different request bodies depending on the action:
| Action | Request body accepted by the wrapper | What the wrapper adds before calling IAM |
|---|---|---|
new-token | { name, prefix, ipv4?, expires? } | privilege: allowedPrivilege |
revoke | { tokenId } | publicIdentifier, name |
metadata | { tokenId } | publicIdentifier, name |
rotate | { tokenId } | publicIdentifier, name |
ip-restriction-update | { tokenId, ipv4? } | publicIdentifier, name |
privilege-update | { tokenId } | publicIdentifier, name, newPrivilege: updateToNewPrivilege |
For every action except new-token, the client submits only tokenId. The
wrapper first calls IAM /api/manage/list-metadata, resolves the matching
token row, and then forwards publicIdentifier and name to IAM. This keeps
token identity details in the server layer instead of exposing them to the
client.
allowedPrivilege controls the privilege assigned to newly created tokens.
updateToNewPrivilege is optional, but you must provide it if you want the
privilege-update action to succeed. It controls the level that can be
updated, and the client request body does not supply newPrivilege.
On new-token, IAM returns rawPublicId in addition to the raw key. The
wrapper removes rawPublicId and exposes only { rawApiKey, expiresAt } on
event.context.newApiToken.
For the raw IAM route contracts behind these wrapper calls, see the IAM API token docs.
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 }
}
if (action === 'rotate') {
return { ok: true, data: event.context.rotate }
}
return { ok: true }
}, 'demo', 'protected')
This wrapper populates one of several action-specific fields on the H3 event context:
| Action | event.context field | Value |
|---|---|---|
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 } |
list-metadata is intentionally rejected on POST. Use getApiListsController
for the authenticated GET route instead.
defineByteLimiterHandler
Use this wrapper when you need a low-level raw-body size limit before JSON
parsing. It asserts the request method, checks Content-Length when present,
reads the raw body once, rejects bodies larger than your limit, and stores the
parsed JSON object on event.context.body.
defineApiManagementHandler uses this body parser. Use it
directly for other custom routes when you need a strict byte limit before any
schema validation runs.
export default defineByteLimiterHandler(async (event) => {
const body = event.context.body
return { ok: true, body }
}, 2048, 'POST')
If the request body is empty, event.context.body is left as undefined and
your handler still runs.
defineVerifiedMagicLinkGetHandler
Validates incoming magic link query parameters before running the handler. It checks that the request is a GET, that the canary_id, session, and __Secure-a cookies exist, and that the query string matches the VerificationLinkSchema (visitor, token, random, reason). After schema validation it calls the IAM service to verify the link.
On success, event.context.link and event.context.reason are populated from the IAM response:
export default defineVerifiedMagicLinkGetHandler(async (event) => {
const { link, reason } = event.context
// Link is verified; render the page or return data for the frontend
return { ok: true, reason }
})
defineMfaCodeVerifierHandler
Validates an MFA code submitted after a magic link is verified. It enforces POST method, verifies the CSRF cookie, limits the request body to 8 MB, validates the link query parameters, reads event.context.body.code (a 7-digit numeric string), and sends the code to the IAM service for verification. On success, tokens are rotated automatically and the new cookies are applied to the response before your handler runs.
export default defineMfaCodeVerifierHandler(async (event) => {
// Code verified, tokens are already rotated
return { ok: true }
})
The code must be present in the parsed request body as code. Read the body before this wrapper runs, or use event.context.body if a body-parsing middleware already ran upstream.
defineDeduplicatedEventHandler
Wraps a handler with request deduplication. Concurrent requests with the same identity are coalesced so that only one execution runs at a time. Use this for handlers that must not run concurrently per session, such as checkout or idempotent write operations.
export default defineDeduplicatedEventHandler(async (event) => {
await processCheckout(event)
return { ok: true }
})
The built-in controllers (loginHandler, logoutHandler, signUpHandler, restartPasswordHandler, sendMfaCodeHandler) already use this wrapper internally.
Event context fields added by wrappers
The wrapper layer extends the H3 event context so your handler can read the result of each verification step without repeating verification logic.
| Field | Set by | Meaning |
|---|---|---|
authorizedData | defineAuthenticatedEventHandler, defineOptionalAuthenticationEvent | Verified user session data |
accessToken | ensureValidCredentials | Current or rotated access token |
session | ensureValidCredentials | Current or rotated refresh token |
limitedMetaData | defineMfaCodeVerifierHandler | Verified MFA result after token rotation |
apiVerification | defineAuthenticatePublicApi | Verified API token metadata |
newApiToken | defineApiManagementHandler | Token creation result |
ipRestrictionUpdate | defineApiManagementHandler | IP restriction update result |
privilegeUpdate | defineApiManagementHandler | Privilege update result |
revoke | defineApiManagementHandler | Revocation result |
extensiveMetadata | defineApiManagementHandler | Single-token metadata result |
rotate | defineApiManagementHandler | Token rotation result |
body | defineByteLimiterHandler | Parsed JSON body |
Making a Custom wrapper
Make your own custom wrappers by simply following the pattern in this module, for example a wrapper that requires auth and csrf and its deduplicated:
import { defineEventHandler, type EventHandler, type EventHandlerRequest } from 'h3';
import { defineAuthenticatedEventPostHandlers, defineDeduplicatedEventHandler } from 'auth-h3client';
export const myCustomEventHandler = <T extends EventHandlerRequest, D>(
handler: EventHandler<T, D>
): EventHandler<T, Promise<D>> => {
return defineAuthenticatedEventPostHandlers(
defineDeduplicatedEventHandler(
defineEventHandler((event) => {
// Do stuff
return handler(event);
})
) as EventHandler<T, D>
);
};
// usage
export default myCustomEventHandler(async (event) => {
// stuff
})
Access token rotation inside wrappers
Both defineAuthenticatedEventHandler and defineOptionalAuthenticationEvent call ensureValidCredentials before passing control to your handler. This means token rotation is transparent: if the access token is missing or near expiry, a fresh token pair is fetched from the IAM service and written to the response cookies before your handler reads any session data.