Skip to main content

Configuration

This page provides a complete reference for configuring nauth-toolkit, including all available options and best practices.

Configuration File Approach​

Best Practice: Store your configuration in a separate file, not in your AppModule or main.ts.

This keeps your configuration organized, testable, and easy to maintain across environments.

Configuration Sources

Load sensitive values (secrets, credentials) from your preferred configuration source:

  • Environment variables (process.env)
  • NestJS ConfigService
  • AWS Secrets Manager / Azure Key Vault
  • Configuration files (.env, config.json)

The toolkit is agnostic to how you manage configuration - use what works best for your infrastructure.

1. Create configuration file:

src/config/auth.config.ts
import { NAuthModuleConfig, createRedisStorageAdapter } from '@nauth-toolkit/nestjs';
import { NodemailerEmailProvider } from '@nauth-toolkit/email-nodemailer';
import { Logger } from '@nestjs/common';

export const authConfig: NAuthModuleConfig = {
jwt: {
algorithm: 'HS256',
accessToken: {
secret: process.env.JWT_SECRET,
expiresIn: '15m',
},
refreshToken: {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d',
reuseDetection: true,
},
},

storageAdapter: createRedisStorageAdapter(process.env.REDIS_URL || 'redis://localhost:6379'),

emailProvider: new NodemailerEmailProvider({
transport: {
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
defaults: {
from: `My App <${process.env.EMAIL_FROM || 'noreply@example.com'}>`,
},
}),

email: {
globalVariables: {
appName: 'My App',
supportEmail: process.env.SUPPORT_EMAIL,
},
},

logger: {
instance: new Logger('NAuth'),
enablePiiRedaction: true,
},
};

2. Import in AppModule:

src/app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from '@nauth-toolkit/nestjs';
import { authConfig } from './config/auth.config';

@Module({
imports: [AuthModule.forRoot(authConfig)],
})
export class AppModule {}

Core Configuration​

Top-level options (NAuthConfig)​

These are the top-level keys you can provide in NAuthConfig / NAuthModuleConfig. Each links to its detailed section below.

OptionRequiredDescription
tablePrefixNoDatabase table prefix. Default: nauth_. Example: 'myapp_' → tables become myapp_users, myapp_sessions.
migrationsNoWhether nauth applies its own schema migrations at startup. Safe for parallel container starts by default.
jwtYesJWT signing algorithm, secrets, and token lifetimes.
storageAdapterNo*Transient storage for sessions, rate limits, and token reuse detection. Auto-detected if omitted and DB storage entities are registered.
signupNoSignup toggle, verification method (email/phone/both/none), and rate limits.
loginNoLogin identifier policy: email, username, phone, or email_or_username.
passwordNoPassword policy (length, complexity, history, expiry) and password reset flow.
lockoutNoIP-based failed-login lockout.
securityNoCSRF protection and sensitive data masking in challenge responses.
tokenDeliveryNoHow tokens reach clients: json (response body), cookies (httpOnly), or hybrid. Default: json.
routesNoMount the shipped auth endpoints instead of writing controllers. Omit to register none.
authorizationNoProvider deciding who may perform admin operations. Required to mount admin routes.
sessionNoSession concurrency limits and hard maximum lifetime.
emailProviderNo*Email provider instance. Required when email verification or MFA email is enabled.
emailNoEmail branding (globalVariables), custom HTML/text templates, and fallback date formatting (defaultTimezone, defaultLocale, timeZoneNameStyle).
emailNotificationsNoPer-event suppression controls. Optional lifecycle emails are disabled by default.
smsProviderNo*SMS provider instance. Required when phone verification or MFA SMS is enabled.
smsNoSMS template branding and custom message content.
socialNoSocial OAuth providers (Google, Apple, Facebook, Microsoft Entra ID) and post-OAuth redirect config.
mfaNoMFA methods, enforcement mode (OPTIONAL/REQUIRED/ADAPTIVE), and device trust.
geoLocationNoMaxMind IP geolocation database. Required for adaptive MFA.
auditLogsNoAudit trail for authentication and security events. Default: enabled.
recaptchaNoGoogle reCAPTCHA bot protection (v2, v3, Enterprise).
challengeNoMax verification attempts before a challenge session is invalidated. Default: 3.
loggerNoLogger instance and PII redaction controls. Silent by default.

JWT Configuration​

Controls JWT token generation and validation. JWTs are used for stateless authentication - the access token authorizes API requests, while the refresh token allows obtaining new access tokens without re-authentication.

jwt: {
algorithm: 'HS256', // HS256/HS384/HS512 (symmetric) or RS256/RS384/RS512 (asymmetric)
issuer: 'com.myapp',
audience: ['web', 'mobile'],

accessToken: {
secret: process.env.JWT_SECRET, // Required for HS* algorithms
// privateKey: process.env.PRIVATE_KEY, // Required for RS* algorithms
// publicKey: process.env.PUBLIC_KEY, // Required for RS* algorithms
expiresIn: '15m', // 15 minutes
},

refreshToken: {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d', // 7 days
reuseDetection: true, // Detect and block token reuse attacks
},
}

Configuration Options:

OptionDescriptionDefaultRecommended
algorithmSigning algorithm. HS256 uses symmetric key (same secret for sign/verify). RS256 uses asymmetric keys (private key signs, public key verifies) - better for microservices where multiple services verify tokens.HS256HS256 for monoliths, RS256 for microservices
issuerIdentifies who issued the token. Used for validation - tokens from other issuers are rejected.noneYour app identifier (e.g., com.myapp)
audienceWho the token is intended for. Can be a string or array. Tokens are rejected if audience doesn't match.noneArray of client types (e.g., web, mobile)
accessToken.secretREQUIRED (HS* algorithms). Secret key for HS* algorithms. Must be strong and kept secure. Minimum 32 characters (256 bits).N/A256-bit random string from secure config
accessToken.privateKeyREQUIRED (RS* algorithms). PEM-encoded private key for RS* algorithms. Used to sign tokens.N/ARSA private key from secure config
accessToken.publicKeyREQUIRED (RS* algorithms). PEM-encoded public key for RS* algorithms. Used to verify tokens. Can be shared publicly.N/ARSA public key from secure config
accessToken.expiresInREQUIRED. How long access tokens are valid. Short-lived for security (if stolen, limited damage). Format: 15m, 1h, or seconds.N/A15m (15 minutes)
refreshToken.secretREQUIRED. Secret for signing refresh tokens. Should be different from access token secret. Minimum 32 characters (256 bits).N/A256-bit random string from secure config
refreshToken.expiresInREQUIRED. How long refresh tokens are valid. Longer-lived for better UX (users stay logged in). Format: 7d, 30d, or seconds.N/A7d to 30d
refreshToken.rotationDeprecated. Token rotation is always enabled and cannot be disabled. A new refresh token is issued on every use regardless of this flag. Accepted for backward compatibility but ignored at runtime.N/AOmit (always on)
refreshToken.reuseDetectionDetect when an old refresh token is reused (sign of theft). When detected, invalidates entire token family and forces re-authentication.falsetrue

When to use RS256 vs HS256:

  • HS256 (Symmetric): Single application, simpler setup, faster
  • RS256 (Asymmetric): Microservices, multiple services verify tokens, public key can be shared

Key Points:

  • algorithm: Use HS256 for simplicity, RS256 for microservices
  • accessToken.expiresIn: Short-lived (15m recommended)
  • refreshToken.expiresIn: Long-lived (7d-30d)
  • reuseDetection: Recommended for production

Migrations​

nauth-toolkit applies its own schema migrations at startup. Parallel container starts are safe with no configuration — each run is serialized behind a database-level lock (a PostgreSQL advisory lock, or a MySQL named lock), so ECS tasks or Kubernetes pods that boot together cannot race to create the same tables. Instances that lose the race wait for the winner and then find nothing to do.

The lock is held on a dedicated connection and released by the database if an instance dies, so a crashed task can never block a deploy.

// Apply migrations from a one-off release task instead of at boot
migrations: {
autoRun: false, // default: true
},
OptionTypeDefaultDescription
autoRunbooleantrueApply pending nauth migrations during startup. Set to false when a release task or Kubernetes Job runs them first.

Locking is always on and has no settings.

See TypeORM PostgreSQL or TypeORM MySQL for details.

Storage Adapter​

Storage adapter is REQUIRED. Choose between Redis or Database for transient storage.

// Redis (Recommended for production, multi-server)
import { createRedisStorageAdapter } from '@nauth-toolkit/nestjs';
storageAdapter: createRedisStorageAdapter('redis://localhost:6379'),

// Database (No Redis needed) - NestJS
import { createDatabaseStorageAdapter } from '@nauth-toolkit/nestjs';
storageAdapter: createDatabaseStorageAdapter(),

// Database (No Redis needed) - Express/Fastify
// import { DatabaseStorageAdapter } from '@nauth-toolkit/storage-database';
// storageAdapter: new DatabaseStorageAdapter(),
tip

If you don't provide a storageAdapter explicitly, DatabaseStorageAdapter will be auto-created if storage entities (getNAuthTransientStorageEntities()) are included in your TypeORM configuration.

See Storage for detailed comparison and auto-detection behavior.

Login Configuration​

Controls which identifier types are accepted during login.

identifierTypeAccepts
undefined (default)All types: email, username, phone
'email'Email addresses only
'username'Usernames only
'phone'Phone numbers only
'email_or_username'Email or username (phone excluded)
login: {
identifierType: 'email_or_username',
},

Email Provider​

Date formatting in emails​

Notification emails render dates in the recipient's own timezone and locale, taken from user.timezone / user.locale. These two keys are the fallback when a user has never set them — useful when your servers run in UTC but your users are concentrated in one region.

email: {
defaultTimezone: 'Europe/Dublin', // IANA name; falls back to the host's zone, then UTC
defaultLocale: 'en-GB', // BCP 47 tag; falls back to the host's locale, then en-US
},

Templates receive the result as {{timestampFormatted}}. This controls date formatting only — it does not translate email body text. See Notifications.

By default, {{timestampFormatted}} names the timezone in full — e.g. 14 Sept 2026, 04:22 Pacific Daylight Time — rather than an abbreviation like PDT. email.timeZoneNameStyle picks a different style instead:

email: {
timeZoneNameStyle: 'short', // default: 'long'
},
StyleExample (Australia/Sydney, recipient locale en-AU)
'long' (default)Australian Eastern Standard Time
'short'AEST
'shortOffset'GMT+10
'longOffset'GMT+10:00
'shortGeneric'AET
'longGeneric'Australian Eastern Time

'short'/'shortGeneric' only render a colloquial abbreviation when the recipient's own locale "owns" their timezone — the pairing above (en-AU + Australia/Sydney) does. A mismatched pairing (say, an en-US locale with an Australia/Sydney timezone) falls back to a generic offset instead — GMT+10, not AEST — because Intl.DateTimeFormat has no way to force a specific abbreviation regardless of locale, and most abbreviations aren't unambiguous across regions anyway (CST alone means three different things). 'long', 'longOffset', and 'longGeneric' render the same text regardless of locale, which is why 'long' is the default: it's the predictable choice for a mixed audience.

Configure email delivery for verification codes and notifications.

import { NodemailerEmailProvider } from '@nauth-toolkit/email-nodemailer';

emailProvider: new NodemailerEmailProvider({
transport: {
host: process.env.SMTP_HOST,
port: 587,
secure: false, // false = STARTTLS on port 587; true = direct TLS on port 465
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
defaults: {
from: 'My App <noreply@myapp.com>',
},
}),

email: {
globalVariables: {
appName: 'My App',
companyName: 'My Company Inc.',
supportEmail: 'support@myapp.com',
logoUrl: 'https://myapp.com/logo.png',
brandColor: '#4f46e5',
},
},

Email Notifications (Optional)​

Optional notification emails are opt-in and controlled via emailNotifications.suppress. This is separate from template customization (see Email Templates).

emailNotifications: {
enabled: true,
suppress: {
// Optional notifications — default: true (suppressed/disabled). Set false to enable.
welcome: false,
passwordChanged: false,
mfaDeviceRemoved: false,
mfaFirstEnabled: false, // Uses the mfaEnabled email template
mfaMethodAdded: false,
adaptiveMfaRiskDetected: false, // Uses the adaptiveMfaRiskAlert template
sessionsRevoked: false,
accountDisabled: false,
accountEnabled: false,
emailChangedOld: false, // Alert sent to the old email address
emailChangedNew: false, // Confirmation sent to the new email address
phoneChanged: false, // Alert sent to the account email when the phone changes
// Note: Code emails (emailVerification, passwordReset, adminPasswordReset)
// cannot be suppressed and are always sent when enabled: true
},
},
Naming tip

Some notification keys map to a differently-named template type:

  • mfaFirstEnabled notification uses the mfaEnabled email template (TemplateType.MFA_ENABLED)
  • adaptiveMfaRiskDetected notification uses the adaptiveMfaRiskAlert email template (TemplateType.ADAPTIVE_MFA_RISK_ALERT)

SMS Provider​

Configure SMS delivery for phone verification, MFA, and password reset.

import { TwilioSMSProvider } from '@nauth-toolkit/sms-twilio';

smsProvider: new TwilioSMSProvider({
accountSid: process.env.TWILIO_ACCOUNT_SID!,
authToken: process.env.TWILIO_AUTH_TOKEN!,
fromNumber: process.env.TWILIO_FROM_NUMBER!, // E.164 format, e.g. '+15551234567'
// Or use a Messaging Service instead:
// messagingServiceSid: process.env.TWILIO_MESSAGING_SERVICE_SID!,
}),

SMS Templates​

Customize SMS message content globally (branding + custom templates).

import { ConsoleSMSProvider } from '@nauth-toolkit/sms-console';

smsProvider: new ConsoleSMSProvider(),

sms: {
templates: {
globalVariables: {
appName: 'My App',
supportPhone: '+1-800-123-4567',
},
customTemplates: {
verification: {
content:
'{{#if appName}}{{appName}}: {{/if}}Your verification code is {{code}}. Valid for {{expiryMinutes}} minutes. Need help? {{supportPhone}}',
},
mfa: {
content: '{{#if appName}}{{appName}}: {{/if}}Your MFA code is {{code}}. Valid for {{expiryMinutes}} minutes.',
},
passwordReset: {
content:
'{{#if appName}}{{appName}}: {{/if}}Your password reset code is {{code}}. Valid for {{expiryMinutes}} minutes.',
},
},
},
},

See SMS Templates Feature Guide and SMS Templates Configuration.

Logger​

Configure logging with PII redaction.

import { Logger } from '@nestjs/common';

logger: {
instance: new Logger('NAuth'),
enablePiiRedaction: true, // Redact emails, IPs, tokens
logLevel: 'debug',
},

Signup Configuration​

Controls user registration behavior, verification requirements, and anti-abuse measures.

signup: {
enabled: true,
verificationMethod: 'email', // 'none' | 'email' | 'phone' | 'both'
allowDuplicatePhones: false,

emailVerification: {
expiresIn: 3600, // 1 hour
maxAttempts: 3, // Max attempts per code
resendDelay: 60, // 60 seconds
rateLimitMax: 3, // 3 emails per window
rateLimitWindow: 3600, // 1 hour
maxAttemptsPerUser: 10,
maxAttemptsPerIP: 20,
attemptWindow: 3600,
baseUrl: 'https://myapp.com', // Optional: include verification link in emails
},

phoneVerification: {
codeLength: 6,
expiresIn: 300, // 5 minutes
maxAttempts: 3,
resendDelay: 60,
rateLimitMax: 3,
rateLimitWindow: 3600,
maxAttemptsPerUser: 10,
maxAttemptsPerIP: 20,
attemptWindow: 3600,
},
},

Verification Methods:

MethodDescriptionUse Case
'none'No verification required. Users can login immediately after signup.Internal tools, trusted environments, development
'email'Email verification required before login. User receives verification code/link. No phone is collected; SMS MFA can still be offered, the number is collected during MFA setup (see SMS MFA).Standard web apps, most common choice
'phone'Phone verification required before login. User receives SMS code.Mobile-first apps, regions where phone is primary contact
'both'Both email AND phone verification required. Higher security but more friction.High-security apps, financial services

Email Verification Options:

OptionDescriptionDefaultRecommended
expiresInHow long verification codes/links are valid (seconds). Balance security vs UX.36003600 (1 hour)
maxAttemptsMax attempts to verify a single code. Prevents brute force.33
resendDelayMinimum time between resend requests (seconds). Prevents spam.6060 (1 minute)
rateLimitMaxMaximum verification emails per time window. Prevents email bombing.33
rateLimitWindowTime window for rate limiting (seconds).36003600 (1 hour)
maxAttemptsPerUserMax verification attempts per user per window. Prevents brute force.1010
maxAttemptsPerIPMax verification attempts per IP per window. Prevents distributed attacks.2020
attemptWindowTime window for attempt limits (seconds).36003600 (1 hour)
baseUrlBase URL for verification links. If provided, emails include clickable link with code. Format: ${baseUrl}?code=${code}. The consumer app handles routing. Supports localhost (e.g., http://localhost:4200).nonehttps://myapp.com or http://localhost:4200 for development

Phone Verification Options:

OptionDescriptionDefaultRecommended
codeLengthLength of SMS verification code. Longer = more secure but harder to type.66
expiresInHow long SMS codes are valid (seconds). Shorter for security (SMS can be intercepted).300300 (5 minutes)
maxAttemptsMax attempts to verify a single code. Prevents brute force.33
resendDelayMinimum time between SMS resends (seconds). Prevents SMS spam and cost abuse.6060 (1 minute)
rateLimitMaxMaximum SMS per time window. Important: SMS costs money, prevent abuse!33
rateLimitWindowTime window for SMS rate limiting (seconds).36003600 (1 hour)
maxAttemptsPerUserMax verification attempts per user per window.1010
maxAttemptsPerIPMax verification attempts per IP per window.2020
attemptWindowTime window for attempt limits (seconds).36003600 (1 hour)

Other Options:

OptionDescriptionUse Case
enabledEnable/disable user signups globally.Set to false to close registration (invite-only apps)
allowDuplicatePhonesAllow multiple users with same phone number.Family accounts, shared devices

Security Considerations:

  • Rate Limiting: Prevents attackers from sending thousands of verification emails/SMS
  • Attempt Limits: Prevents brute-forcing verification codes
  • IP-based Limits: Stops distributed attacks from multiple accounts
  • Expiration: Short expiration windows reduce attack surface

Password & Security​

Configure password policies, account lockout, and CSRF protection.

password: {
minLength: 8,
maxLength: 128,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
preventCommon: true, // Block common passwords
preventUserInfo: true, // Block passwords containing email/username
historyCount: 5, // Prevent reusing last 5 passwords
expiryDays: 90, // Force password change after 90 days (0 = disabled)
},

lockout: {
enabled: true,
maxAttempts: 5, // Lock after 5 failed attempts
duration: 900, // 15 minutes lockout
resetOnSuccess: true, // Reset counter on successful login
},

security: {
maskSensitiveData: true, // Mask email/phone in challenge responses
csrf: {
cookieName: 'csrf-token',
headerName: 'x-csrf-token',
tokenLength: 32,
excludedPaths: ['/webhook'], // Paths that don't need CSRF
cookieOptions: {
secure: true,
sameSite: 'strict',
domain: '.myapp.com', // Optional: share across subdomains
},
},
},

maskSensitiveData:

When enabled (default), email addresses and phone numbers in challenge responses are masked for security:

  • Email: john.doe@example.com → j***@example.com
  • Phone: +1234567890 → ***-***-7890

Set to false to return full email/phone in challenge responses. Useful for development or internal apps where security is less critical.

security: {
maskSensitiveData: false, // Return full email/phone
},

Password Policy Options:

OptionDescriptionDefaultRecommended
minLengthMinimum password length. NIST recommends at least 8 characters.88 or higher
maxLengthMaximum password length. Prevents DoS attacks from Argon2 hashing very long strings.128128
requireUppercaseRequire at least one uppercase letter (A-Z).falsetrue for moderate security
requireLowercaseRequire at least one lowercase letter (a-z).falsetrue for moderate security
requireNumbersRequire at least one number (0-9).falsetrue for moderate security
requireSpecialCharsRequire at least one special character. Defaults to !@#$%^&* if specialChars is not set.falsetrue for high security
specialCharsCustom set of characters that satisfy requireSpecialChars. Overrides the default !@#$%^&* set.none'$#!@' or domain-appropriate set
preventCommonBlock common passwords (e.g., password123, qwerty). Uses built-in list of 10,000+ common passwords.falsetrue (strongly recommended)
preventUserInfoBlock passwords containing user's email or username. Prevents john@example.com using john123.falsetrue
historyCountNumber of previous passwords to remember. Prevents password reuse. 0 = disabled.05 for compliance, 0 for simplicity
expiryDaysForce password change after N days. 0 = never expires. Note: NIST no longer recommends forced expiration.00 (disabled) or 90 for compliance

Password Reset Options​

Controls the forgot-password flow (code delivery is handled by your configured email/SMS provider).

OptionDescriptionDefault
codeLengthVerification code length sent to the user.6
expiresInCode expiry in seconds.900 (15 minutes)
rateLimitMaxMaximum reset requests per time window. Prevents abuse.3
rateLimitWindowTime window for rate limiting (seconds).3600 (1 hour)
maxAttemptsMaximum code verification attempts per code. Prevents brute force.3
baseUrlServer-controlled base URL for the reset link (${baseUrl}?code=…). The shipped forgot-password route uses only this — never a request-body value — so a reset link can't be pointed at an attacker host. Omit to send the code with no link.(none)
password: {
// ... policy options ...
passwordReset: {
codeLength: 6,
expiresIn: 900, // 15 minutes
rateLimitMax: 3,
rateLimitWindow: 3600,
maxAttempts: 3,
baseUrl: 'https://myapp.com/auth/reset-password', // optional; your frontend route
},
},

:::warning Reset-link base URL is server-controlled The public POST /auth/forgot-password route intentionally ignores any baseUrl in the request body and builds the reset link from password.passwordReset.baseUrl. If you need a dynamic, per-user link (e.g. embedding the email), write your own controller that narrows the request body to identifier and passes a server-computed baseUrl to AuthService.forgotPassword — never forward a client-supplied one. :::

Admin Password Reset Options​

Controls admin-initiated password resets. Longer expiry by default; no rate limiting (admin-initiated).

OptionDescriptionDefault
codeLengthVerification code length.6
expiresInCode expiry in seconds. Longer than user-initiated (admin context).3600 (1 hour)
maxAttemptsMaximum code verification attempts per code.3

Account Lockout Options:

OptionDescriptionDefaultRecommended
enabledEnable IP-based account lockout after failed login attempts.falsetrue
maxAttemptsNumber of failed login attempts before lockout. Too low = UX issues, too high = security risk.55
attemptWindowTime window in seconds over which maxAttempts is counted. Prevents indefinite accumulation.36003600 (1 hour)
durationLockout duration in seconds. After this time, the lockout lifts.900900 (15 minutes)
resetOnSuccessReset failed attempt counter on successful login. Recommended for better UX.truetrue

Why IP-based lockout? Uses IP addresses instead of user identifiers. This prevents attackers from locking out legitimate users by repeatedly trying their email/username.

CSRF Protection Options:

OptionDescriptionDefaultRecommended
cookieNameName of the CSRF token cookie.nauth_csrf_tokennauth_csrf_token
headerNameHTTP header name where frontend sends CSRF token.x-csrf-tokenx-csrf-token
tokenLengthLength of CSRF token in bytes. Longer = more secure.3232 (256 bits)
excludedPathsPaths that don't require CSRF tokens (e.g., webhooks, public APIs).empty array/webhook, /api/public as needed
cookieOptions.secureRequire HTTPS for CSRF cookie. Set to false for localhost development.truetrue (false for localhost)
cookieOptions.sameSiteSameSite cookie attribute. strict = most secure, lax = allows top-level navigation.strictstrict
cookieOptions.domainCookie domain for subdomain sharing.none.myapp.com for subdomains

When is CSRF protection needed?

  • Required when using tokenDelivery.method = 'cookies' or 'hybrid'
  • Not needed when using tokenDelivery.method = 'json' (Bearer tokens are CSRF-safe)

Token Delivery​

Control how tokens are delivered to clients.

tokenDelivery: {
method: 'cookies', // 'json' | 'cookies' | 'hybrid'
cookieNamePrefix: 'nauth_',

cookieOptions: {
secure: true, // HTTPS only (set to false for localhost)
sameSite: 'strict', // 'strict' | 'lax' | 'none'
domain: '.myapp.com', // Optional: for subdomain sharing
path: '/',
},
},

Delivery Methods:

MethodDescriptionBest ForCSRF Required?
'json'Tokens returned in response body only. Frontend stores in memory/localStorage. Sent via Authorization: Bearer header.Mobile apps, SPAs, API-first appsNo
'cookies'Tokens set as httpOnly cookies. Browser automatically sends with requests. More secure (XSS-resistant).Traditional web apps, server-rendered appsYes
'hybrid'Cookies for web origins, JSON for mobile origins. Best of both worlds.Apps with both web and mobile clientsYes (for web)

Cookie Options:

OptionDescriptionDefaultRecommended
cookieNamePrefixPrefix for all auth cookies. Prevents conflicts with other cookies.nauth_nauth_
secureRequire HTTPS. Cookies won't be sent over HTTP. Set to false for localhost development.truetrue (false for localhost)
sameSiteControls when cookies are sent. strict = same-site only, lax = allows top-level navigation, none = all requests (requires secure: true).strictstrict (or lax for better UX)
domainCookie domain. Set to .myapp.com to share cookies across app.myapp.com and api.myapp.com.none.myapp.com for subdomain sharing
pathCookie path. Usually / for all routes.//

Security Comparison:

AspectJSON (localStorage)Cookies (httpOnly)
XSS Protection Vulnerable - XSS can steal tokens Protected - httpOnly prevents JS access
CSRF Protection Not vulnerable Requires CSRF tokens
Mobile Support Easy Requires cookie handling
Subdomain Sharing Manual implementation Built-in via domain option
Auto-send Must add header manually Browser sends automatically

Recommendation:

  • Web apps: Use 'cookies' for better security
  • Mobile apps: Use 'json' for simplicity
  • Both: Use 'hybrid' mode

Hybrid policy (tokenDelivery.method = 'hybrid')​

In hybrid mode, nauth-toolkit chooses between cookies vs JSON delivery for each request. Delivery is resolved in this order:

  1. Route-level override — @TokenDelivery('json' | 'cookies') (NestJS) or nauth.helpers.tokenDelivery(...) (Express/Fastify). Typically used for dedicated /mobile endpoints.
  2. Origin-based classification — webOrigins → cookies, nativeOrigins → json.
tokenDelivery: {
method: 'hybrid',
hybridPolicy: {
webOrigins: ['https://app.myapp.com'],
nativeOrigins: ['capacitor://localhost', 'ionic://localhost'],

// Optional: per-delivery refresh token TTLs
// When set, the resolved delivery mode selects which TTL is used.
// Fall back to jwt.refreshToken.expiresIn when unset.
cookieRefreshExpiresIn: '7d', // Shorter lifetime for browsers (cookies)
jsonRefreshExpiresIn: '90d', // Longer lifetime for mobile / workers (json)
},
},
PropertyTypePurpose
webOriginsstring[]Origins that resolve to cookies delivery when no route-level override is set.
nativeOriginsstring[]Origins that resolve to json delivery when no route-level override is set.
cookieRefreshExpiresInstring | numberRefresh token TTL applied when a request resolves to cookies delivery. Falls back to jwt.refreshToken.expiresIn.
jsonRefreshExpiresInstring | numberRefresh token TTL applied when a request resolves to json delivery. Falls back to jwt.refreshToken.expiresIn.
Hybrid-only

cookieRefreshExpiresIn and jsonRefreshExpiresIn are read only when tokenDelivery.method === 'hybrid'. They're ignored in 'json' or 'cookies' mode.

Common pairing

Short cookie TTL (7d) for web and long json TTL (90d) for mobile gives browsers tight blast-radius while letting native apps stay signed in for months without re-authentication.

Shipped Routes​

Mount the toolkit's auth endpoints rather than hand-writing controllers. Omit routes entirely and no endpoints are registered — the behaviour of every release before this option existed.

routes: [
{ prefix: 'auth', delivery: 'cookies' },
{ prefix: 'mobile/auth', delivery: 'json', groups: ['core', 'social'] },
],
OptionTypeDefaultDescription
adminGuardsGuardLike[][]Guards applied only to access: 'admin' routes.
delivery'json' | 'cookies'inheritForces one transport. Requires tokenDelivery.method: 'hybrid' unless it matches the configured method.
enabledbooleantrueSet false to mount nothing.
excludeNAuthRouteKey[][]Routes to leave out. An unknown key throws at startup.
groupsNAuthRouteGroup[]all but admin, apiKeysAdminWhich bundles to mount.
guardsGuardLike[][]Guards applied to every route in the bundle.
prefixstring'auth'Path prefix. On Fastify the prefix comes from register() instead.
routeGuardsPartial<Record<NAuthRouteKey, GuardLike[]>>{}Guards for named routes.

Passing two bundles with different delivery values requires tokenDelivery.method: 'hybrid'; the toolkit throws at startup rather than failing on the first request. The message names the offending bundle by its prefix when you supplied one — on Fastify the prefix belongs to register(), so it identifies the bundle by its delivery mode instead.

See Route Groups for what each group contains, Shipped Routes for the full route table, and Authentication Routes for the guide.

Authorization​

Decides who may perform administrative operations. The toolkit authenticates but ships no role model, so without a provider AdminAuthService trusts its caller and the admin route group refuses to mount.

// NestJS — a class, so it can inject its own dependencies
AuthModule.forRoot({ ...authConfig, authorization: RoleAuthorizer })

// Express / Fastify — an instance, alongside dataSource
NAuth.create({ config, dataSource, adapter, authorization: new RoleAuthorizer() })

Enforcement lives in the services, so the same policy covers a shipped route, a controller you write, and a script. See Authorization.

Session Configuration​

Manage user sessions and concurrency.

session: {
maxConcurrent: 5, // Max active sessions per user
disallowMultipleSessions: false, // If true, only 1 session allowed
maxLifetime: '30d', // Hard limit: force re-auth after 30 days
},

Social Login​

Configure OAuth providers.

social: {
// social.redirect is required when any provider is enabled — see Social Redirect below
redirect: {
frontendBaseUrl: process.env.FRONTEND_BASE_URL || 'https://app.myapp.com',
},

google: {
enabled: !!process.env.GOOGLE_CLIENT_ID,
clientId: process.env.GOOGLE_CLIENT_ID,
// Or array for multi-platform: [webClientId, iosClientId, androidClientId]
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackUrl: `${process.env.API_BASE_URL}/auth/social/google/callback`,
scopes: ['openid', 'email', 'profile'],
autoLink: true, // Auto-link to existing account by email match
allowSignup: true, // Create new account if no match found
},

apple: {
enabled: !!process.env.APPLE_CLIENT_ID,
clientId: process.env.APPLE_CLIENT_ID, // Apple Services ID (e.g., com.myapp.services)
// Apple requires a JWT client secret for web OAuth. The toolkit generates and refreshes
// it automatically from your Apple Developer credentials (stored in DB, refreshed ~30 days before expiry).
teamId: process.env.APPLE_TEAM_ID, // Apple Developer Team ID
keyId: process.env.APPLE_KEY_ID, // Key ID (kid) from your .p8 key file
privateKeyPem: process.env.APPLE_PRIVATE_KEY_PEM, // Contents of your .p8 file as PEM string
callbackUrl: `${process.env.API_BASE_URL}/auth/social/apple/callback`,
scopes: ['name', 'email'],
autoLink: true,
allowSignup: true,
},

facebook: {
enabled: !!process.env.FACEBOOK_CLIENT_ID,
clientId: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
callbackUrl: `${process.env.API_BASE_URL}/auth/social/facebook/callback`,
scopes: ['email', 'public_profile'],
autoLink: true,
allowSignup: true,
},

microsoft: {
enabled: !!process.env.MS_CLIENT_ID,
clientId: process.env.MS_CLIENT_ID,
clientSecret: process.env.MS_CLIENT_SECRET,
callbackUrl: `${process.env.API_BASE_URL}/auth/social/microsoft/callback`,
// 'common' (work + personal) | 'organizations' | 'consumers' | a directory GUID.
// Pin a GUID to restrict sign-in to one Microsoft 365 tenant.
tenant: process.env.MS_TENANT_ID || 'common',
scopes: ['openid', 'email', 'profile'],
autoLink: true,
allowSignup: true,
// Optional access gate, checked on every sign-in:
// allowedTenants: ['<directory-guid>'], // closed allowlist for 'common'/'organizations'
// requiredRoles: ['nauth.access'], // Entra app roles from the `roles` claim
// requiredGroups: ['<group-guid>'], // security group object ids
},
},
Microsoft tenant

tenant decides who can sign in, and must match the app registration's Supported account types. 'common' admits work accounts from any directory as well as personal accounts — pin a GUID when the app serves one organisation. See the Microsoft Entra ID guide.

Social Redirect (social.redirect)​

Required when social login is enabled

social.redirect.frontendBaseUrl is required whenever any social provider is enabled. The Zod schema will throw a validation error at startup if it is missing.

After OAuth completes, the backend redirects the user back to your frontend. Configure where that redirect lands:

social: {
redirect: {
frontendBaseUrl: process.env.FRONTEND_BASE_URL || 'https://app.myapp.com',
// Allow returnTo query param to specify a post-login destination URL.
// false = relative paths only (safest); true = absolute URLs allowed (requires allowedReturnToOrigins)
allowAbsoluteReturnTo: false,
// Required when allowAbsoluteReturnTo is true — prevents open-redirect attacks
allowedReturnToOrigins: ['https://app.myapp.com', 'https://admin.myapp.com'],
},
google: { enabled: true, ... },
},
OptionDescriptionDefault
frontendBaseUrlRequired. Base URL of your frontend app. Used to resolve relative returnTo redirect paths after OAuth.—
allowAbsoluteReturnToAllow absolute URLs in returnTo query param. Enable only for multi-frontend setups.false
allowedReturnToOriginsOrigin allowlist enforced when allowAbsoluteReturnTo is true. Prevents open redirects.—

Multi-Factor Authentication (MFA)​

Configure MFA methods and enforcement.

import { MFAMethod } from '@nauth-toolkit/core';

mfa: {
enabled: true,
enforcement: 'OPTIONAL', // 'OPTIONAL' | 'REQUIRED' | 'ADAPTIVE'
gracePeriod: 7, // Days before REQUIRED enforcement kicks in
grace: {
skipForSignup: false, // Skip the MFA setup challenge during signup only
},
requireForSocialLogin: false,

allowedMethods: [
MFAMethod.TOTP, // Authenticator apps
MFAMethod.SMS, // SMS codes (phone collected during setup if the account has none)
MFAMethod.EMAIL, // Email codes
MFAMethod.PASSKEY, // WebAuthn/biometric
],

issuer: 'My App',

totp: {
window: 1, // Time steps to check (±1)
stepSeconds: 30, // Standard TOTP interval
digits: 6,
algorithm: 'sha1',
},

passkey: {
rpName: 'My App',
rpId: 'myapp.com',
origin: ['https://myapp.com', 'https://app.myapp.com'],
timeout: 60000,
userVerification: 'preferred',
},

adaptive: {
triggers: ['new_device', 'new_ip', 'new_country', 'impossible_travel', 'suspicious_activity', 'recent_password_reset'],
riskLevels: {
low: { maxScore: 20, action: 'allow', notifyUser: false },
medium: { maxScore: 50, action: 'require_mfa', notifyUser: true },
high: { maxScore: 100, action: 'require_mfa', notifyUser: true },
},
blockedSignIn: {
// Block scope controls the blast radius of `block_signin`.
// - 'ip': block the suspicious IP
// - 'device': block the device token
// - 'user': block the whole user (strongest, highest DoS risk)
//
// 'ip' and 'device' are identified by values the client supplies, so a caller
// presenting a *different* IP or device token is not covered by an existing
// block — that request is risk-scored from scratch instead. Presenting none
// does not evade it: a request with no device token (or no IP) stays blocked.
// Use 'user' where the block must hold regardless of what the client sends.
scope: 'ip',
blockDuration: 15, // minutes
message: 'Sign-in blocked due to suspicious activity. Please try again shortly or contact support.',
},
},

rememberDevices: 'user_opt_in', // 'never' | 'always' | 'user_opt_in'
rememberDeviceDays: 30,
bypassMFAForTrustedDevices: true,

backup: {
enabled: true,
codeCount: 10,
codeLength: 8,
},
},

Grace period​

Under REQUIRED and ADAPTIVE enforcement, users who have not enrolled MFA are challenged with MFA_SETUP_REQUIRED. These options control when that challenge starts.

OptionDescriptionDefault
gracePeriodDays from account creation during which an un-enrolled user can still authenticate. 0 enforces setup immediately.7
grace.skipForSignupSkip the MFA_SETUP_REQUIRED challenge for the signup flow only, so signup stays frictionless. Enforcement resumes on the user's next login. Honoured even when gracePeriod is 0.false

While setup is pending but not yet enforced, every auth response — challenges included — carries mfaGracePeriod, so a client can offer an optional setup flow right after signup instead of waiting for a later login to force it.

// No day-based grace, but signup is never blocked by an MFA wall
mfa: {
enabled: true,
enforcement: 'REQUIRED',
gracePeriod: 0,
grace: { skipForSignup: true },
},
note

grace.skipForSignup covers the signup request and the email/phone verification challenges it issues — the whole onboarding flow, including social signup. It does not affect login.

Social logins are only subject to any of this when requireForSocialLogin is true. With the default false, social users are never asked to set up MFA and their responses carry no mfaGracePeriod.

Geolocation​

Configure IP geolocation for adaptive MFA.

geoLocation: {
maxMind: {
dbPath: '/app/data/maxmind', // Optional: defaults to system temp directory
editions: ['GeoLite2-City', 'GeoLite2-Country'],
download: {
from: 'maxmind',
licenseKey: process.env.MAXMIND_LICENSE_KEY,
accountId: Number(process.env.MAXMIND_ACCOUNT_ID),
onStartup: true, // default: fetch during startup
},
},
},

Omit download entirely to load .mmdb files already present in dbPath and never fetch anything --- the mode for a sidecar, init container or geoipupdate.

Download from your own mirror instead of MaxMind's rate-limited API. No licence key is involved:

geoLocation: {
maxMind: {
dbPath: '/app/data/maxmind',
download: {
from: 'url',
url: 'https://cdn.example.com/geoip/{edition}.tar.gz', // or a per-edition object
auth: { username: '...', password: '...' }, // optional HTTP Basic
},
requireDatabaseOnStartup: true, // default for from: 'url' --- abort startup if nothing loads
},
},

Accepts .tar.gz in MaxMind's layout or a bare .mmdb, detected from the response bytes. HTTPS only --- s3:// is rejected, since the toolkit ships no AWS SDK; use a presigned URL or the bucket's HTTPS endpoint. See Geolocation.

Audit Logs​

Configure audit trail.

auditLogs: {
enabled: true,
fireAndForget: false, // Set true for performance (no await)
},

Telemetry​

Anonymous usage telemetry (config shape only — no PII, IPs, or secrets). See Telemetry for exactly what is sent.

telemetry: {
enabled: false, // Opt out (default: true)
},
OptionDescriptionDefault
enabledEnable anonymous usage telemetrytrue
Opt-out without code changes

Set NAUTH_TELEMETRY_DISABLED=1 (or DO_NOT_TRACK=1) in the environment. Telemetry is always disabled in CI and tests.

reCAPTCHA​

Protect authentication endpoints from bot attacks using Google reCAPTCHA v2, v3, or Enterprise.

note

Social OAuth endpoints (/auth/social/*) are not protected by reCAPTCHA — OAuth providers handle their own bot protection.

npm install @nauth-toolkit/recaptcha
import { RecaptchaV3Provider } from '@nauth-toolkit/recaptcha';

recaptcha: {
enabled: true,
provider: new RecaptchaV3Provider({
secretKey: process.env.RECAPTCHA_SECRET_KEY,
}),
minimumScore: 0.5, // v3/Enterprise only. 0.0 = bot, 1.0 = human
actionScores: { // Optional: per-action overrides
login: 0.3, // More permissive for returning users
signup: 0.7, // Stricter for new registrations
},
validateOnStartup: 'warn', // 'warn' | 'error' | false
},

reCAPTCHA Options:

OptionDescriptionDefault
enabledEnable reCAPTCHA validation on auth endpoints.false
providerProvider instance. Choose RecaptchaV2Provider, RecaptchaV3Provider, or RecaptchaEnterpriseProvider.—
minimumScoreDefault minimum score (0.0–1.0). Used when no per-action override exists in actionScores. v3/Enterprise only.0.5
actionScoresPer-action score overrides (Record<string, number>). Keys: login, signup, password_reset, etc. Falls back to minimumScore.—
validateOnStartupValidate credentials at startup. 'warn': log warning on failure. 'error': halt startup. false: skip.'warn'

Choosing a provider:

ProviderInteractionBest For
RecaptchaV3ProviderInvisible, score-basedMost web apps — no user friction
RecaptchaV2ProviderCheckbox ("I'm not a robot")Highest-risk actions, legacy setups
RecaptchaEnterpriseProviderScore-based with advanced signalsEnterprise — requires GCP project

Challenge Configuration​

Challenge session limits for flows like verification, MFA, and step-up challenges.

challenge: {
maxAttempts: 3, // Default: 3
},

API Keys​

Long-lived keys that authenticate as their owning user. Disabled by default. See the API Keys guide for routes and decorators.

apiKeys: {
enabled: true, // Default: false
allowUserCreation: false, // Default: false (admin-only creation). true = users self-serve
header: 'X-API-Key', // Default: 'X-API-Key' (case-insensitive)
maxKeysPerUser: 10, // Default: 10
maxExpiryDays: 365, // Caps finite expiries; omit for no cap
allowIndefinite: true, // Default: true (allow never-expiring keys)
trackUsageIp: true, // Default: true (record last-used IP per key)
lastUsedThrottleSeconds: 60, // Default: 60 (throttle last-used writes)
globalAllowlist: false, // Default: false (opt-in per route via @AllowApiKey())
ipRestrictions: {
enabled: true, // Default: true (allow per-key IP allowlists)
requireForNewKeys: false, // Default: false
maxIpsPerKey: 20, // Default: 20
},
},
OptionDefaultDescription
enabledfalseEnable API key authentication
allowUserCreationfalseAllow end users (not just admins) to create keys
header'X-API-Key'Header carrying the key; when present it is the only credential used
maxKeysPerUser10Maximum active keys per user
maxExpiryDays—Caps finite expiry; unset means no cap
allowIndefinitetrueAllow never-expiring keys
trackUsageIptrueRecord the source IP of each key's last use
globalAllowlistfalseAccept keys on all protected routes (opt-out via @DenyApiKey())
ipRestrictions.enabledtrueAllow per-key IP allowlists (IPs / IPv4 CIDR)
ipRestrictions.requireForNewKeysfalseRequire an allowlist on every new key
ipRestrictions.maxIpsPerKey20Max allowlist entries per key

OpenID Connect Provider​

The OpenID Connect provider is not configured through NAuthConfig. It is a separate package with its own options object, passed to OIDCProviderModule.forRoot() alongside AuthModule.forRoot():

src/config/oidc.config.ts
import type { OIDCProviderModuleOptions } from '@nauth-toolkit/oidc-provider/nestjs';

export const oidcConfig: OIDCProviderModuleOptions = {
issuer: 'https://auth.example.com', // an origin, never a path
pathPrefix: '/oidc',
interactionUrl: (uid) => `https://auth.example.com/interaction/${uid}`,
cookieKeys: [process.env.OIDC_COOKIE_SECRET!],
clients: [/* ... */],
};

It reuses your existing StorageAdapter and user repository, so there is nothing to add to NAuthConfig and no new tables. See NAuthOIDCOptions for the full reference.

Complete Example​

Here's a production-ready configuration:

src/config/auth.config.ts
import { NAuthModuleConfig, MFAMethod, createRedisStorageAdapter } from '@nauth-toolkit/nestjs';
import { NodemailerEmailProvider } from '@nauth-toolkit/email-nodemailer';
import { AWSSMSProvider } from '@nauth-toolkit/sms-aws-sns';
import { Logger } from '@nestjs/common';

export const authConfig: NAuthModuleConfig = {
tablePrefix: 'nauth_',

storageAdapter: createRedisStorageAdapter(process.env.REDIS_URL || 'redis://localhost:6379'),

jwt: {
algorithm: 'HS256',
issuer: 'com.myapp',
audience: ['web', 'mobile'],
accessToken: { secret: process.env.JWT_SECRET, expiresIn: '15m' },
refreshToken: {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d',
reuseDetection: true,
},
},

logger: {
instance: new Logger('NAuth'),
enablePiiRedaction: true,
logLevel: 'log', // 'error' | 'warn' | 'log' | 'debug' | 'verbose'
},

signup: {
enabled: true,
verificationMethod: 'email',
emailVerification: {
expiresIn: 3600,
resendDelay: 60,
rateLimitMax: 3,
rateLimitWindow: 3600,
baseUrl: 'https://myapp.com', // Optional: include verification link in emails
},
},

password: {
minLength: 8,
requireUppercase: true,
requireNumbers: true,
requireSpecialChars: true,
preventCommon: true,
},

lockout: {
enabled: true,
maxAttempts: 5,
duration: 900,
},

tokenDelivery: {
method: 'cookies',
cookieOptions: {
secure: true,
sameSite: 'strict',
},
},

security: {
csrf: {
cookieName: 'csrf-token',
headerName: 'x-csrf-token',
},
},

// Option 1: SMTP Transport
emailProvider: new NodemailerEmailProvider({
transport: {
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
defaults: {
from: `My App <${process.env.EMAIL_FROM || 'noreply@myapp.com'}>`,
},
}),

// Option 2: AWS SES SDK Transport (with IAM roles)
// import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
// emailProvider: new NodemailerEmailProvider({
// transport: {
// SES: {
// sesClient: new SESv2Client({ region: process.env.AWS_REGION || 'us-east-1' }),
// SendEmailCommand,
// },
// },
// defaults: {
// from: 'My App <noreply@myapp.com>',
// },
// }),

email: {
globalVariables: {
appName: 'My App',
companyName: 'My Company Inc.',
supportEmail: 'support@myapp.com',
logoUrl: 'https://myapp.com/logo.png',
brandColor: '#4f46e5',
},
},

smsProvider: new AWSSMSProvider({
region: process.env.AWS_REGION as string,
originationNumber: process.env.AWS_SMS_ORIGINATION as string,
accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
// apiMode: 'end-user-messaging-sms',
// configurationSetName: 'default',
}),

sms: {
templates: {
globalVariables: {
appName: 'My App',
},
},
},

social: {
redirect: {
frontendBaseUrl: process.env.FRONTEND_BASE_URL || 'https://app.myapp.com',
},
google: {
enabled: true,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackUrl: `${process.env.API_BASE_URL || 'https://api.myapp.com'}/auth/social/google/callback`,
autoLink: true,
allowSignup: true,
},
},

mfa: {
enabled: true,
enforcement: 'OPTIONAL',
allowedMethods: [MFAMethod.TOTP, MFAMethod.EMAIL],
issuer: 'My App',
},

session: {
maxConcurrent: 5,
maxLifetime: '30d',
},

auditLogs: { enabled: true },
};

Next Steps​