Skip to main content

Token Management

nauth-toolkit manages JWT tokens throughout the authentication lifecycle --- from initial login to token refresh to protected API calls. This page covers delivery modes, validation, refresh, CSRF protection, and JWT configuration.

Token Delivery Modes

Your backend configuration determines how tokens travel between backend and frontend. Choose one mode per client type:

ModeBest ForTokens InFrontend SendsCSRF Required
jsonMobile apps, SPAsResponse bodyAuthorization: Bearer headerNo
cookiesWeb apps (most secure)HTTP-only cookiesAutomatic (cookies)Yes
hybridWeb + Mobile from same backendVaries by routeDepends on endpointYes (for cookie routes)
Quick Recommendation
  • Web app only? Use cookies (most secure for browsers)
  • Mobile app only? Use json (standard Bearer tokens)
  • Both web and mobile? Use hybrid (separate routes per client type)

JSON Mode (Bearer Tokens)

Tokens are returned in the response body. The frontend stores them and sends them in the Authorization header.

Configuration:

config/auth.config.ts
{
tokenDelivery: {
method: 'json',
},
}

Response:

{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"accessTokenExpiresAt": 1735000000,
"refreshTokenExpiresAt": 1735604800,
"user": { "sub": "...", "email": "..." }
}
Security

For web apps, avoid storing tokens in localStorage --- they're vulnerable to XSS attacks. Use cookies mode instead, or store tokens in memory only.

Cookies Mode (HTTP-Only Cookies)

The most secure mode for web browsers. Tokens are set as HTTP-only cookies that JavaScript cannot access.

Configuration:

config/auth.config.ts
{
tokenDelivery: {
method: 'cookies',
cookieOptions: {
secure: true,
sameSite: 'strict',
domain: 'yourdomain.com',
},
},
security: {
csrf: {
cookieName: 'nauth_csrf_token',
headerName: 'x-csrf-token',
},
},
}

Response:

HTTP/1.1 200 OK
Set-Cookie: nauth_access_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/
Set-Cookie: nauth_refresh_token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/
Set-Cookie: nauth_csrf_token=abc123; Secure; SameSite=Strict; Path=/

{
"user": { "sub": "...", "email": "..." }
}
info

The response body does not contain tokens --- they're in the Set-Cookie headers. The frontend never sees the actual token values.

Hybrid Mode (Flexible Routing)

Hybrid mode lets one backend serve both web (cookies) and mobile (JSON) clients using separate routes.

Configuration:

config/auth.config.ts
{
tokenDelivery: {
method: 'hybrid',
cookieOptions: {
secure: true,
sameSite: 'strict',
},
},
security: {
csrf: {
excludedPaths: ['/auth/*/mobile'],
},
},
}
Route Naming Convention

Use a clear pattern like /auth/* for cookie routes and /auth/*/mobile for JSON routes. This makes it obvious which client should call which endpoint.

Setting Up Per-Route Delivery

When using hybrid mode, each route must specify its delivery mode.

src/auth/auth.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { Public, TokenDelivery } from '@nauth-toolkit/nestjs';
import { AuthService, LoginDTO, AuthResponseDTO } from '@nauth-toolkit/core';

@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Public()
@Post('login')
@TokenDelivery('cookies')
async loginWeb(@Body() dto: LoginDTO): Promise<AuthResponseDTO> {
return await this.authService.login(dto);
}

@Public()
@Post('login/mobile')
@TokenDelivery('json')
async loginMobile(@Body() dto: LoginDTO): Promise<AuthResponseDTO> {
return await this.authService.login(dto);
}
}

Token Validation

When a client makes a request to a protected endpoint, nauth-toolkit automatically validates the access token.

What happens automatically:

  1. Token extracted from the correct source (cookie or Authorization header)
  2. JWT signature validated
  3. Token expiration checked
  4. Session loaded from storage
  5. User object attached to request context

Access the authenticated user:

import { Controller, Get } from '@nestjs/common';
import { CurrentUser } from '@nauth-toolkit/nestjs';
import { IUser } from '@nauth-toolkit/core';

@Controller('profile')
export class ProfileController {
@Get()
getProfile(@CurrentUser() user: IUser) {
return { user };
}
}
info

Routes are not protected by default. Use requireAuth() (Express/Fastify) or apply guards (NestJS) to enforce authentication.

Token Refresh

Access tokens have short lifetimes (typically 15 minutes). When they expire, use the refresh token to get new ones without re-login.

Refresh endpoint implementation (all frameworks and modes)

JSON mode:

@Public()
@Post('refresh')
async refresh(@Body() dto: RefreshTokenDTO): Promise<TokenResponse> {
return await this.authService.refreshToken(dto);
}

Cookies mode:

@Public()
@Post('refresh')
async refresh(@Req() req: any): Promise<TokenResponse> {
const token = req?.cookies?.['nauth_refresh_token'];
if (!token) {
throw new UnauthorizedException('Refresh token missing');
}

const dto = new RefreshTokenDTO();
dto.refreshToken = token;
return await this.authService.refreshToken(dto);
}

Hybrid mode:

@Public()
@Post('refresh')
@TokenDelivery('cookies')
async refreshWeb(@Req() req: any): Promise<TokenResponse> {
const token = req?.cookies?.['nauth_refresh_token'];
if (!token) {
throw new UnauthorizedException('Refresh token missing');
}
const dto = new RefreshTokenDTO();
dto.refreshToken = token;
return await this.authService.refreshToken(dto);
}

@Public()
@Post('refresh/mobile')
@TokenDelivery('json')
async refreshMobile(@Body() dto: RefreshTokenDTO): Promise<TokenResponse> {
return await this.authService.refreshToken(dto);
}
Frontend SDK

If you're using @nauth-toolkit/client, token refresh is handled automatically. The SDK intercepts 401 responses, calls the refresh endpoint, and retries the original request.

Token Rotation

nauth-toolkit always issues a new refresh token on every use — the old token is immediately invalidated. This limits the damage if a refresh token is intercepted.

Enable reuse detection to catch stolen tokens:

config/auth.config.ts
{
jwt: {
refreshToken: {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d',
reuseDetection: true, // Revoke all sessions when reuse is detected
},
},
}

When reuseDetection: true is set and a previously-used refresh token is presented, nauth-toolkit:

  1. Revokes all active sessions for that user (prevents the attacker from using other tokens)
  2. Returns AUTH_TOKEN_REUSE_DETECTED — the user must log in again
When to Enable

Enable reuseDetection for security-sensitive applications. Without it, token theft may go undetected until the token expires naturally.

CSRF Protection

When using cookies or hybrid mode, CSRF protection is mandatory. nauth-toolkit uses the double-submit cookie pattern:

  1. Server sets a CSRF token as a readable cookie (not httpOnly)
  2. Frontend reads the cookie and sends the value in a custom header
  3. Server validates that the cookie and header match

Configuration:

config/auth.config.ts
{
security: {
csrf: {
cookieName: 'nauth_csrf_token',
headerName: 'x-csrf-token',
excludedPaths: ['/webhooks/*'],
},
},
}

Frontend implementation:

function getCsrfToken(): string {
const cookies = document.cookie.split(';');
const csrfCookie = cookies.find((c) => c.trim().startsWith('nauth_csrf_token='));
return csrfCookie ? csrfCookie.split('=')[1] : '';
}

fetch('/api/protected', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'x-csrf-token': getCsrfToken(),
},
body: JSON.stringify({ ... }),
});
Never Disable CSRF

CSRF protection is required for cookie-based authentication. Without it, malicious sites can perform actions on behalf of your users by exploiting automatic cookie sending.

Guard Enforcement

nauth-toolkit enforces your chosen delivery method to prevent security bypasses:

ModeBearer TokenCookie Token
jsonAcceptedRejected (COOKIES_NOT_ALLOWED)
cookiesRejected (BEARER_NOT_ALLOWED)Accepted
hybridAcceptedAccepted (checks cookies first)

If you configure cookies mode for security but a client sends a Bearer token from localStorage, the security benefit is lost. Enforcement ensures all clients use the intended method.

JWT Algorithm

AlgorithmTypeKey Requirement
HS256Symmetric (HMAC)Shared secret
HS384Symmetric (HMAC)Shared secret
HS512Symmetric (HMAC)Shared secret
RS256Asymmetric (RSA)Public/private key pair
RS384Asymmetric (RSA)Public/private key pair
RS512Asymmetric (RSA)Public/private key pair
config/auth.config.ts
{
jwt: {
algorithm: 'HS256',
accessToken: {
secret: process.env.JWT_SECRET,
expiresIn: '15m',
},
refreshToken: {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d',
},
},
}
Never Change Algorithm After Launch

Changing jwt.algorithm after users have active sessions will break all existing tokens. Plan your algorithm choice before going to production.

Fine-tune cookie behavior for your deployment:

config/auth.config.ts
{
tokenDelivery: {
method: 'cookies',
cookieNamePrefix: 'nauth_',
cookieOptions: {
secure: true,
sameSite: 'strict',
domain: 'example.com',
path: '/',
},
},
}
OptionTypeDefaultDescription
cookieNamePrefixstringnauth_Prefix for all cookie names
httpOnlyAlways trueHardcoded — not configurable
securebooleantrueHTTPS only
sameSitestringstrictstrict, lax, or none
domainstring---Share across subdomains
pathstring/Cookie path

Generated cookie names (with default prefix nauth_):

CookiePurposeHTTP-OnlyAccessible to JS
nauth_access_tokenAccess tokenYesNo
nauth_refresh_tokenRefresh tokenYesNo
nauth_csrf_tokenCSRF tokenNoYes (required)
nauth_device_idTrusted device tokenYesNo
Localhost Development

For local development, set secure: false because localhost uses HTTP:

cookieOptions: {
secure: process.env.NODE_ENV === 'production',
}

Remember Device

The "Remember Device" feature works with all delivery modes:

ModeDevice Token DeliveryFrontend Action
cookiesSet as nauth_device_id httpOnly cookieAutomatic --- no action needed
jsonReturned as deviceToken in response bodyStore securely, send in x-device-token header
hybridDepends on route's delivery modeWeb: automatic. Mobile: manual storage

See MFA for trusted device configuration.

Troubleshooting

"Token invalid" or "Token expired" errors
  1. Clock skew --- Server and client clocks out of sync. Use NTP to synchronize server time
  2. Algorithm mismatch --- Changed jwt.algorithm after tokens were issued. Clear all sessions or wait for natural expiration
  3. Wrong secret --- Using different secrets between environments. Verify environment variables
CSRF token errors
  1. Missing header --- Frontend not sending x-csrf-token header. Read cookie and include in request headers
  2. CORS issues --- Credentials not being sent cross-origin. Ensure credentials: 'include' and proper CORS config
  3. Cookie domain mismatch --- CSRF cookie not accessible. Check tokenDelivery.cookieOptions.domain
Refresh token not working
  1. Mode mismatch --- Using JSON refresh endpoint with cookies mode. Implement correct endpoint for your mode
  2. Cookie not sent --- Browser not sending refresh cookie. Ensure credentials: 'include' in refresh request
  3. Token already used --- Refresh token rotation means each token is single-use. Always use the new token from the refresh response

What's Next