Security
Auth H3 Client is built on the same Defense in Depth principle as the IAM service. Every layer assumes the layers around it can fail and adds its own protection. A missing CSRF token does not reach your handler. An unrecognized IP does not reach the session check. A replayed service request does not pass the HMAC signature verification. No single control stands alone.
This page documents every security mechanism in the BFF layer. Where a control extends or delegates to the IAM service, you will find a link to the corresponding IAM documentation.
Request lifecycle
Every request that enters the Auth H3 Client layer passes through a sequence of
checks before it reaches the final handler. When the Nuxt module runs with
enableMiddleware: true, its bundled global middleware applies three of these
checks to all non-skipped requests before route-level enforcement begins. When
enableMiddleware: false, you are responsible for registering any equivalent
browser middleware yourself.
Global middleware chain
The global middleware in server/middleware/auth.ts skips requests that do not need checking: HEAD requests, /api/health, /_nuxt static assets, /api/_mdc, and requests arriving from loopback addresses (127.0.0.1, ::1). All other requests pass through the following chain in order:
isIPValid
Extracts the client IP address from the request headers and validates it with net.isIP(). Rejects any request with an invalid or missing IP address before any network call is made. This check has no latency cost.
botDetectorMiddleware
Forwards the visitor fingerprint to the IAM /check endpoint. Returns HTTP 403 when the visitor score exceeds the configured ban threshold. When enableFireWallBans is true, calls banIp to block the IP at the firewall level. See Bot Detection for the full flow.
generateCsrfCookie
Mints a signed __Host-csrf cookie when one is not already present. The token is a 32-byte hex string signed with an expiring HMAC payload. See CSRF Protection below for how the cookie is constructed and verified.
Route-level enforcement
Inside your own route handlers, the event handler wrappers enforce authentication and CSRF requirements:
| Wrapper | Enforces |
|---|---|
defineAuthenticatedEventHandler | HMAC signature, token rotation, session check |
defineOptionalAuthenticationEvent | Same as above, continues as guest on failure |
defineVerifiedCsrfHandler | CSRF cookie and header match |
defineAuthenticatedEventPostHandlers | Authentication + CSRF + POST method |
defineAuthenticatePublicApi | X-API-KEY verification against IAM /api/public/verify |
defineApiManagementHandler | Authentication + CSRF + POST method + 2 KB JSON body limit + server-side API token identity mapping |
defineVerifiedMagicLinkGetHandler | GET method, required cookies, magic link schema, IAM link verification |
defineMfaCodeVerifierHandler | POST method, CSRF, body limit, link params, 7-digit code, token rotation |
See Route Protection for the full wrappers reference.
CSRF protection
The module implements the double-submit cookie pattern. The CSRF token is issued as a cookie and must also appear as a request header on every state-changing request.
Cookie issuance
generateCsrfCookie runs automatically in global middleware. It generates a 32-byte random token with crypto.randomBytes(32).toString('hex'), signs it into a base64(value).base64('csrf').expiry.hmac payload, and sets it as __Host-csrf:
| Attribute | Value |
|---|---|
HttpOnly | false (the client must be able to read it to inject it as a header) |
SameSite | Strict |
Secure | true |
MaxAge | 1800 seconds (30 minutes) |
Path | / (enforced by the __Host- prefix) |
The __Host- prefix tells the browser to enforce Secure: true, Path: /, and to strip the domain attribute. This prevents the cookie from being set by a subdomain and makes it impossible to replicate without HTTPS.
Verification
verifyCsrfCookie reads the __Host-csrf cookie, verifies the HMAC signature and expiry using verifySignedValue, and then checks that the X-CSRF-Token request header equals the token stored inside the cookie payload. The comparison uses isSameBuffer, a timing-safe HMAC comparison that prevents timing side-channel attacks.
Verification fails with the following HTTP 403 error codes:
| Code | Meaning |
|---|---|
CSRF_MISSING | The __Host-csrf cookie is absent |
CSRF_INVALID | The cookie signature or expiry check failed |
TOKEN_INVALID | The X-CSRF-Token header does not match the cookie payload |
On the client, executeRequest injects the CSRF token automatically from getCsrfToken(), which reads the raw value from document.cookie. On the server, the CSRF token is extracted from the forwarded request headers. See CSRF for the full flow and ApiContext usage.
Cookie security
Signed cookies
Cookies that carry sensitive state are signed using createSignedValue. The format is:
base64url(value) . base64url(keyword) . expiryTimestamp . hmacSha256Hex
The keyword parameter binds the signature to a specific purpose so that a valid CSRF cookie cannot be replayed as a different cookie type. The HMAC is computed with the cryptoCookiesSecret from the configuration.
verifySignedValue parses this format, recomputes the HMAC, and uses isSameBuffer for the comparison. A mismatched signature, expired timestamp, or malformed payload all return { valid: false, payload: null }. See Cookies for the full inventory and attributes.
Cookie prefixes
All sensitive cookies use either the __Host- or __Secure- prefix:
| Prefix | Enforced attributes |
|---|---|
__Host- | Secure: true, Path: /, domain attribute stripped |
__Secure- | Secure: true |
The __Host- prefix, used for the CSRF cookie, provides the strongest protection: the browser will not send it on plain HTTP, will only send it on requests to the exact origin, and will refuse to set it from a subdomain. The access token cookie __Secure-a requires HTTPS but allows domain scoping for cross-subdomain token sharing.
Inter-service authentication
Traffic between the module and the IAM service runs on a private Docker network. This isolation is not sufficient alone. The module supports two independent authentication layers for every outbound request.
HMAC request signing
When enableHmac: true, every request the module sends to the IAM service carries four signed headers:
| Header | Purpose |
|---|---|
X-Client-Id | Identifies the gateway instance |
X-Timestamp | Millisecond timestamp for clock-skew detection |
X-Request-Id | A unique request ID for replay detection |
X-Signature | HMAC-SHA256 of clientId:timestamp:method:path:requestId |
The IAM service recomputes the signature with crypto.timingSafeEqual and rejects requests outside the configured clock-skew window. It also caches recent request IDs to reject replays. A container on the same Docker network without the shared secret cannot forge a valid request.
hmacSignatureMiddleware, which runs inside every authenticated handler wrapper, generates these headers and stores them on event.context.authHeaders so that every serviceToService call to the IAM service carries a valid signature.
See the HMAC guide for configuration and the IAM HMAC documentation for the server-side verification flow.
Mutual TLS
getAuthAgent builds an Undici HTTP agent with optional client certificate and CA bundle. When configured, both the module and the IAM service present certificates and verify each other. This adds a transport-layer identity check on top of the application-layer HMAC signature.
See the mTLS guide for certificate generation and configuration.
Bot detection and IP validation
The module integrates with the Bot Detection service to screen incoming requests before any authentication logic runs.
IP validation
isIPValid extracts the client IP from the request headers and validates it with net.isIP(). This check runs first, before any network call, and adds no latency.
Score-based screening
botDetectorMiddleware forwards the visitor fingerprint to the IAM service /check endpoint. The IAM service returns a score for the visitor based on the Bot Detection pipeline. Visitors with a score at or above the configured ban threshold receive HTTP 403. The IAM service also updates the visitor record on each check.
Firewall banning
When enableFireWallBans: true, the module calls banIp on any visitor that exceeds the ban threshold. banIp executes sudo ufw insert 1 deny from <ip> to any to block the IP at the operating system firewall level, before the request even enters the application.
sudo privileges for the process user. Do not enable this option in environments where those conditions are not met.In custom handlers, checkForBots(event) calls /check directly so you can apply bot detection outside of the global middleware chain.
Input validation
Body size limits
limitBytes(maxBytes) reads the request body before any parsing and throws HTTP 403 with code INVALID_CONTENT_TYPE if the byte length exceeds the limit. Passing limitBytes(0) rejects any body entirely. This limit is applied before JSON parsing, so a large payload cannot consume memory during deserialization.
| Route type | Limit |
|---|---|
| Login, signup, MFA, password reset | 1 KB |
| Email change body | 1 MB |
| MFA code submission | 8 MB |
| Logout | 0 (no body allowed) |
Content-type enforcement
contentType(expected) validates the Content-Type request header before the body is parsed. Requests with an unexpected or missing content type are rejected with HTTP 403 and code INVALID_CONTENT_TYPE. This prevents multipart, URL-encoded, or XML payloads from bypassing JSON-level input validation.
XSS sanitization pipeline
sanitizeInputString runs a seven-stage pipeline on every user-supplied string before it reaches business logic:
Length guard
Rejects input exceeding htmlSanitizer.maxAllowedInputLength. Oversized input is rejected before entering the loop.
Unicode normalization
Normalizes to NFKC, collapses zero-width characters, strips soft hyphens and bidirectional overrides, and transliterates fullwidth ASCII back to standard ASCII.
URI decode
Applies decodeURIComponent once. Malformed percent-encoding (e.g. %ZZ) causes immediate rejection.
Iterative decoding loop
Alternates between decodeURIComponent and HTML entity decoding until the output stabilizes or the iteration limit is reached. Catches layered payloads like %253Cscript%253E.
Residual cleanup
Strips zero-width characters that decoders may have reintroduced.
Pattern detection
Tests for HTML tags, inline event handlers (onclick=, onerror=), and javascript: URIs. Any match sets htmlFound: true.
sanitize-html pass and entity encoding
Runs sanitize-html with zero allowed tags, then entity-encodes the output: &, <, >, ", ', backtick, and ${.
makeSafeString wraps this pipeline in a Zod transform so it applies automatically to any schema field:
const schema = z.object({
name: makeSafeString({ min: 1, max: 100 }),
bio: makeSafeString({ min: 0, max: 500 })
})
See the IAM XSS Protection page for the full pipeline specification.
Token management
Rotation deduplication
When two requests arrive simultaneously for the same session, both will detect that the access token is near expiry and attempt to call POST /auth/user/refresh-session on the IAM service. Sending two concurrent rotation requests would cause the first-returned token to be immediately invalidated by the second rotation, ending the session.
lockAsyncAction deduplicates this by key. The first rotation call proceeds normally. Any additional calls with the same session key wait for the first result and reuse it. Rotation outcomes are cached for 5 seconds to handle requests that arrive after the first call completes.
Session data caching
getCachedUserData caches the result of the IAM /secret/data response in the configured unstorage instance. The cache key is a SHA-256 hash of the canary_id, session, and __Secure-a cookie values. A change in any token invalidates the cache entry automatically.
Cached results are served for the duration of successTtl (default 30 days) without a network call to the IAM service. Rate-limited responses are cached separately for rateLimitTtl to prevent hammering the IAM service during a rate limit window.
Access token metadata cache
getAccessTokenMetaData keeps a local LRU cache of access token expiry and rotation state. This means that checking whether a token is still valid does not require a round trip to the IAM service on every request. The cache is populated after each successful rotation.
API token verification and management
The API token surface adds two distinct security patterns: public API key verification for machine-to-machine traffic, and authenticated management routes for token inventory and lifecycle operations.
Public API verification
defineAuthenticatePublicApi reads X-API-KEY from the incoming request and
forwards it to IAM /api/public/verify together with the privilege floor you
choose for that route. Your handler only runs after IAM verifies the key.
On success, the wrapper places the normalized verification result on
event.context.apiVerification. On failure, it returns a structured JSON
response and forwards Retry-After on rate limits. This keeps API key parsing,
privilege comparison, and abuse control out of your application handlers.
Authenticated management routes
defineApiManagementHandler protects browser-facing token management routes
behind the full authenticated POST chain: HMAC signing to IAM, token rotation,
session verification, CSRF enforcement, method enforcement, and a 2 KB raw body
limit before JSON parsing.
For revoke, metadata, rotate, IP restriction updates, and privilege updates,
the wrapper first fetches the token list from IAM and maps the submitted
tokenId to public_identifier in the server layer. Clients only need to send
tokenId, which keeps token identity details out of the public request body.
Threat model
What this layer defends against
| Threat | Defense |
|---|---|
| CSRF attacks | Double-submit cookie with HMAC-signed expiring token and timing-safe comparison |
| Cookie theft via XSS | CSRF cookie uses HttpOnly: false intentionally (client must read it); access and session cookies are httpOnly: true, unreachable from JavaScript |
| Subdomain cookie injection | __Host- prefix prevents any subdomain from setting or reading the guarded cookies |
| Unauthorized gateway requests | HMAC signature with clock-skew and replay detection; optional mTLS |
| Bot traffic and credential stuffing | IP validation, bot score screening, optional UFW firewall banning |
| Concurrent token rotation race | lockAsyncAction deduplication prevents competing rotation calls for the same session |
| Oversized or malformed payloads | limitBytes applied before JSON parsing; contentType enforcement before body reads |
| XSS injection in user input | Seven-stage sanitization pipeline before any string reaches business logic |
What this layer defers to the IAM service
The BFF layer does not handle these controls. They are enforced entirely within the IAM service:
- Password hashing: Argon2id with pepper. See IAM Security: Credential Storage.
- Refresh token single-use enforcement: Atomic
usage_countconsumption. See IAM Security: Token Lifecycle. - Anomaly detection: Nine-check session fingerprint validation. See Anomaly Detection.
- Adaptive MFA: Triggered by anomaly detection, code hashed with SHA-256. See MFA.
- Rate limiting: Per-IP and per-composite-key limits on all auth endpoints. See Rate Limiting.
- Session lifetime enforcement: Hard
MAX_SESSION_LIFEcap enforced during rotation