Route Protection

Event handler wrappers that enforce authentication, API-key verification, CSRF validation, body limits, and method requirements at the route definition level, with typed access to session data.

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

WrapperSession authAPI keyCSRFMethod
defineAuthenticatedEventHandlerYesNoNoAny
defineOptionalAuthenticationEventOptionalNoNoAny
defineVerifiedCsrfHandlerNoNoYesAny
defineAuthenticatedEventPostHandlersYesNoYesPOST only
defineAuthenticatePublicApiNoYesNoAny
defineApiManagementHandlerYesNoYesPOST only
defineByteLimiterHandlerNoNoNoPOST, PUT, or PATCH
defineVerifiedMagicLinkGetHandlerNoNoNoGET only
defineMfaCodeVerifierHandlerNoNoYesPOST only
defineDeduplicatedEventHandlerNoNoNoAny

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.

server/api/profile.get.ts
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.

server/api/posts/[id].get.ts
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.

server/api/contact.post.ts
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.

server/api/account/delete.post.ts
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.

Use 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.

server/api/public/reports.get.ts
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-token
  • revoke
  • metadata
  • rotate
  • ip-restriction-update
  • privilege-update

The wrapper accepts different request bodies depending on the action:

ActionRequest body accepted by the wrapperWhat 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.

server/api/auth/api-tokens/[action].post.ts
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:

Actionevent.context fieldValue
new-tokennewApiToken{ rawApiKey, expiresAt }
ip-restriction-updateipRestrictionUpdate{ msg }
privilege-updateprivilegeUpdate{ msg }
revokerevokestring or { msg, invalidedTokenId, userId }
metadataextensiveMetadata{ tokenMeta, counts }
rotaterotate{ 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.

server/api/upload-metadata.post.ts
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:

server/api/auth/verify-mfa.get.ts
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 }
})
This wrapper does not validate a CSRF token because it is designed for GET requests arriving from an email link, to let callers render pages for the right user.

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.

server/api/auth/verify-mfa.post.ts
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.

server/api/checkout.post.ts
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.

FieldSet byMeaning
authorizedDatadefineAuthenticatedEventHandler, defineOptionalAuthenticationEventVerified user session data
accessTokenensureValidCredentialsCurrent or rotated access token
sessionensureValidCredentialsCurrent or rotated refresh token
limitedMetaDatadefineMfaCodeVerifierHandlerVerified MFA result after token rotation
apiVerificationdefineAuthenticatePublicApiVerified API token metadata
newApiTokendefineApiManagementHandlerToken creation result
ipRestrictionUpdatedefineApiManagementHandlerIP restriction update result
privilegeUpdatedefineApiManagementHandlerPrivilege update result
revokedefineApiManagementHandlerRevocation result
extensiveMetadatadefineApiManagementHandlerSingle-token metadata result
rotatedefineApiManagementHandlerToken rotation result
bodydefineByteLimiterHandlerParsed 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:

myWrapper.ts
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.

Logo