CSRF Protection
The module implements a double-submit cookie pattern with HMAC signing. The server mints a signed cookie on first contact and expects the client to echo the token value back in a request header on every state-changing request. The signature covers the token value, a session identifier, and an expiry timestamp, so the cookie cannot be forged or reused after it expires.
Cookie issuance
generateCsrfCookie mints the __Host-csrf cookie if it is not already present on the request. It generates a 32-byte random token, creates a signed value from the token with a 30-minute TTL, and sets the cookie with httpOnly: false so the browser can read it.
The signed cookie format is: base64(token).base64(session).expiry.hmac
The session segment in the signature is the string "csrf", which is the keyword verified on the server side. This binds the cookie to the CSRF context and prevents signed values from other contexts being submitted as CSRF tokens.
// Cookie attributes set by generateCsrfCookie:
// Name: __Host-csrf
// HttpOnly: false (client must read it)
// SameSite: Strict
// Secure: true
// MaxAge: 1800 (30 minutes)
// Path: / (enforced by __Host- prefix)
The __Host- prefix requires Secure: true, Path: /, and no Domain attribute, making the cookie scoped to the exact origin with no subdomain leakage.
generateCsrfCookie is part of the global middleware chain wired up in the H3 or Nitro setup. When using the Nuxt module with enableMiddleware: true, it runs automatically on every request and you do not need to call it manually.
Cookie verification
verifyCsrfCookie validates the CSRF cookie and the X-CSRF-Token request header. It throws HTTP 403 in three cases: the __Host-csrf cookie is missing, the cookie signature or expiry is invalid, or the X-CSRF-Token header value does not match the token stored in the cookie payload.
// Throws 403 with one of these codes:
// CSRF_MISSING: cookie not present
// CSRF_INVALID: signature check failed or cookie expired
// TOKEN_INVALID: header missing or does not match cookie payload
Both the cookie payload and the X-CSRF-Token header are Base64-encoded. The server decodes both with fromB64 and performs a timing‑safe comparison using isSame to avoid timing side‑channel attacks. On missing/invalid values the middleware throws 403 with codes CSRF_MISSING, CSRF_INVALID, or TOKEN_INVALID.
verifyCsrfCookie is called automatically by defineVerifiedCsrfHandler, defineAuthenticatedEventPostHandlers, and defineMfaCodeVerifierHandler. You only need to call it directly when composing a custom pipeline:
import { defineEventHandler } from 'h3'
export default defineEventHandler(async (event) => {
await verifyCsrfCookie(event)
// Proceed with the handler
})
Client-side token reading
getCsrfToken() is a browser-only helper exported from auth-h3client/client. It reads the __Host-csrf cookie from document.cookie and extracts the first segment (the raw token before the first .). It returns undefined if the cookie is absent. See Client-side: getCsrfToken for the full client-side surface.
const token = getCsrfToken()
fetch('/api/action', {
method: 'POST',
headers: {
'X-CSRF-Token': token ?? '',
'Content-Type': 'application/json'
},
body: JSON.stringify({ ... })
})
Using executeRequest
executeRequest is the recommended way to make authenticated requests from Vue components and pages. It is exported from auth-h3client/client. On the client it reads the CSRF token via getCsrfToken() and injects it as the X-CSRF-Token header automatically. On the server it proxies the incoming request headers so cookies are forwarded upstream. It also captures Set-Cookie headers from the response and forwards them to the browser, handling token rotation transparently. See Client-side: executeRequest for full parameter and return-type details.
const result = await executeRequest<{ name: string }>('/api/profile', 'GET')
if (result.ok) {
console.log(result.data.name)
}
For POST requests:
const result = await executeRequest<{ ok: boolean }>(
'/api/account/settings',
'POST',
{ theme: 'dark' }
)
executeRequest returns a Results<T> object:
type Results<T> =
| { ok: true; data: T; date: string }
| { ok: false; reason: string; date: string }
The function accepts optional customHeaders, customOptions, and an ApiContext object for server-side usage where you want to pass a specific event or fetcher:
export default defineAuthenticatedEventHandler(async (event) => {
const headers = useRequestHeaders()
const fetcher = useRequestFetch()
const result = await executeRequest<Data>(
'/api/downstream',
'GET',
{},
{},
{},
{ headers, event, fetcher }
)
return result
})
Summary
| Step | Function | Where it runs |
|---|---|---|
| Issue cookie | generateCsrfCookie | Server middleware (automatic) |
| Read token | getCsrfToken() | Browser (client composable) |
| Inject header | executeRequest | Automatic in client context |
| Verify cookie + header | verifyCsrfCookie | Server, inside protected wrappers |