Token Management
next-api-layer handles JWT token management automatically, including validation, refresh, single-flight coalescing, and optional local verification.
Overview#
The library manages these token operations:
- Validation: Checking if a token is valid (backend per-request, or local JWT verification)
- Refresh: Automatically refreshing expired tokens, coalescing concurrent refreshes
- Reuse detection: Classifying refresh failures and failing closed on token theft (RFC 9700)
Token Validation#
In the default backend mode, every request triggers token validation through
your backend's validate endpoint. Switch to validate.mode: 'local' to verify
the JWT in the proxy instead and skip that call — see
Local Validation (Stateless) below.
Expected Response Format#
Your backend's /auth/me endpoint should return:
Custom Response Parsing#
If your backend uses a different format, use responseMappers:
Automatic Token Refresh#
When a token is invalid or expired, the library automatically attempts to refresh it:
Refresh Flow#
- Token validation fails (expired/invalid)
- Library calls
/auth/refreshwith current token - If successful, updates cookie with new token
- Sets
x-refreshed-tokenheader for the request - Continues with the original request
Expected Refresh Response#
Custom Refresh Parsing#
Concurrent Refresh (Single-Flight)#
When several requests arrive at once with the same expired token (navigation +
SWR polling + RSC fetches), the proxy coalesces their refreshes into a single
auth/refresh call and shares the result:
This matters for backends that rotate jti server-side: without coalescing, the
second refresh would replay a rotated token and bounce the user to login (or
orphan the session). Enabled by default (refresh.singleFlight), effective
within a single runtime instance.
Sharing refresh results across instances#
The single-flight map lives in memory, so it only covers one runtime instance.
PM2 cluster workers, Passenger, Docker replicas and serverless/edge isolates each
keep their own. Supply refresh.store so one instance can reuse a refresh
another just performed:
Keys are SHA-256 hashes of the old token, never the token itself. Values hold
freshly issued tokens for storeTtlMs, so back the store with a secured service
and keep the TTL small. Only a refreshed token that passes validation is stored.
Stale entries fall through to a normal refresh, and store errors are reported via
onError without failing the request.
refresh.storeis best-effort, not a distributed mutex —getandsetare not atomic, so two instances that miss the store at the same moment still refresh independently. For full coverage, pair either mechanism with idempotent refresh handling on the backend: a rotation grace window, and acceptance of the previousjtifor that window, chosen larger than your refresh latency.
Auth API Routes#
By default the proxy skips /api/auth/login, /api/auth/logout,
/api/auth/me, /api/auth/refresh and /api/auth/register entirely, so none of
the refresh machinery above applies to them.
/api/auth/me is the route AuthProvider polls, and
while it is bypassed an expired token simply returns 401 there. Remove it from
the list to route it through the normal pipeline:
Now an expired token on /api/auth/me triggers a single refresh, the new token
is written to the cookie and forwarded downstream via x-refreshed-token,
and the route returns 200.
Keep the login and register routes listed — they carry no token yet — and keep the refresh route listed, since validating it would rotate the token before the route itself runs.
Inside afterAuth, skipped requests are marked so you can tell them apart from
genuinely anonymous ones:
Proactive Refresh#
By default, refresh is reactive — triggered only after a 401. Enable
proactive refresh to renew a still-valid token shortly before it expires,
avoiding a guaranteed failed roundtrip on the next cycle:
Proactive refresh is best-effort: if it fails, the still-valid token keeps working. A detected token reuse remains terminal.
Reuse Detection & Fail-Closed (RFC 9700)#
A failed refresh is classified so you can react to token theft:
| reason | Meaning | Default trigger |
|---|---|---|
expired | Token past its lifetime | 401 |
revoked | Server rejected the token | 403 |
reuse | A rotated (old) token was replayed | 409 or body { code: 'token_reuse' } |
network | Transport error | fetch threw |
unknown | Anything else | — |
On reuse — or when access.guestFallbackOnUserRefreshFail: false — the proxy
clears every auth cookie and redirects to login (or 401 for API routes),
emitting auth:reuse / auth:refresh:fail audit events. It never downgrades
to a guest session.
Local Validation (Stateless)#
By default the proxy calls auth/me on every request. Switch to local
verification to validate the JWT in-process and remove the per-request backend
roundtrip:
revalidateInterval preserves revocation / allowlist enforcement: the backend
is consulted at most once every N seconds (tracked via the non-sensitive
__nal_rv cookie). Use validate.verify for RS256 / JWKS.
Dual-Token Mode#
For the OAuth2 access/refresh split — a short-lived access token plus a separate,
long-lived refresh token — name a refresh cookie:
The access token is validated on each request; only the refresh cookie is sent
to auth/refresh, which rotates both tokens. Even if the access token leaks
in a log or proxy, it is short-lived.
Token Types#
The library supports different token types:
Type Checking#
Guest vs User Tokens#
| Feature | Guest Token | User Token |
|---|---|---|
| Created automatically | Yes | No (login required) |
| Cookie name | guestToken | userToken |
| Typical TTL | 1 hour | 7 days |
| Can access protected routes | No | Yes |
Cookie Configuration#
Control how tokens are stored:
Security Best Practices#
- Always use
httpOnly: trueto prevent XSS token theft - Use
secure: truein production for HTTPS-only sameSite: 'lax'provides good CSRF protection while allowing normal navigation- Set appropriate
maxAgebased on your security requirements
Accessing Tokens#
In Route Handlers#
With API Client#
The API client handles token retrieval automatically:
Token Expiration Handling#
Proxy Level#
The proxy handles expiration automatically:
- Detects expired token from validation response
- Attempts refresh
- If refresh fails, clears cookies and redirects to login (for protected routes)
Client Level#
The AuthProvider keeps client state in sync:
Debugging#
Enable audit logging to track token operations:
This helps identify:
- Frequent refresh attempts (might indicate short token TTL)
- Failed validations (might indicate backend issues)
- Unauthorized access attempts