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.
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.
- NestJS
- Express / Fastify
1. Create configuration file:
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:
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 {}
1. Create configuration file:
import { NAuthConfig } from '@nauth-toolkit/core';
import { RedisStorageAdapter } from '@nauth-toolkit/storage-redis';
import { NodemailerEmailProvider } from '@nauth-toolkit/email-nodemailer';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' });
await redisClient.connect();
export const authConfig: NAuthConfig = {
jwt: {
algorithm: 'HS256',
accessToken: {
secret: process.env.JWT_SECRET,
expiresIn: '15m',
},
refreshToken: {
secret: process.env.JWT_REFRESH_SECRET,
expiresIn: '7d',
reuseDetection: true,
},
},
storageAdapter: new RedisStorageAdapter(redisClient),
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: console, // Any logger implementing LoggerService (log/error/warn/debug)
enablePiiRedaction: true,
},
};
2. Register in main:
import { NAuth } from '@nauth-toolkit/core';
import { ExpressAdapter } from '@nauth-toolkit/core'; // or FastifyAdapter
import { authConfig } from './config/auth.config';
const nauth = await NAuth.create({
config: authConfig,
dataSource,
adapter: new ExpressAdapter(), // or new FastifyAdapter()
});
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.
| Option | Required | Description |
|---|---|---|
tablePrefix | No | Database table prefix. Default: nauth_. Example: 'myapp_' → tables become myapp_users, myapp_sessions. |
migrations | No | Whether nauth applies its own schema migrations at startup. Safe for parallel container starts by default. |
jwt | Yes | JWT signing algorithm, secrets, and token lifetimes. |
storageAdapter | No* | Transient storage for sessions, rate limits, and token reuse detection. Auto-detected if omitted and DB storage entities are registered. |
signup | No | Signup toggle, verification method (email/phone/both/none), and rate limits. |
login | No | Login identifier policy: email, username, phone, or email_or_username. |
password | No | Password policy (length, complexity, history, expiry) and password reset flow. |
lockout | No | IP-based failed-login lockout. |
security | No | CSRF protection and sensitive data masking in challenge responses. |
tokenDelivery | No | How tokens reach clients: json (response body), cookies (httpOnly), or hybrid. Default: json. |
routes | No | Mount the shipped auth endpoints instead of writing controllers. Omit to register none. |
authorization | No | Provider deciding who may perform admin operations. Required to mount admin routes. |
session | No | Session concurrency limits and hard maximum lifetime. |
emailProvider | No* | Email provider instance. Required when email verification or MFA email is enabled. |
email | No | Email branding (globalVariables), custom HTML/text templates, and fallback date formatting (defaultTimezone, defaultLocale, timeZoneNameStyle). |
emailNotifications | No | Per-event suppression controls. Optional lifecycle emails are disabled by default. |
smsProvider | No* | SMS provider instance. Required when phone verification or MFA SMS is enabled. |
sms | No | SMS template branding and custom message content. |
social | No | Social OAuth providers (Google, Apple, Facebook, Microsoft Entra ID) and post-OAuth redirect config. |
mfa | No | MFA methods, enforcement mode (OPTIONAL/REQUIRED/ADAPTIVE), and device trust. |
geoLocation | No | MaxMind IP geolocation database. Required for adaptive MFA. |
auditLogs | No | Audit trail for authentication and security events. Default: enabled. |
recaptcha | No | Google reCAPTCHA bot protection (v2, v3, Enterprise). |
challenge | No | Max verification attempts before a challenge session is invalidated. Default: 3. |
logger | No | Logger 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:
| Option | Description | Default | Recommended |
|---|---|---|---|
algorithm | Signing 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. | HS256 | HS256 for monoliths, RS256 for microservices |
issuer | Identifies who issued the token. Used for validation - tokens from other issuers are rejected. | none | Your app identifier (e.g., com.myapp) |
audience | Who the token is intended for. Can be a string or array. Tokens are rejected if audience doesn't match. | none | Array of client types (e.g., web, mobile) |
accessToken.secret | REQUIRED (HS* algorithms). Secret key for HS* algorithms. Must be strong and kept secure. Minimum 32 characters (256 bits). | N/A | 256-bit random string from secure config |
accessToken.privateKey | REQUIRED (RS* algorithms). PEM-encoded private key for RS* algorithms. Used to sign tokens. | N/A | RSA private key from secure config |
accessToken.publicKey | REQUIRED (RS* algorithms). PEM-encoded public key for RS* algorithms. Used to verify tokens. Can be shared publicly. | N/A | RSA public key from secure config |
accessToken.expiresIn | REQUIRED. How long access tokens are valid. Short-lived for security (if stolen, limited damage). Format: 15m, 1h, or seconds. | N/A | 15m (15 minutes) |
refreshToken.secret | REQUIRED. Secret for signing refresh tokens. Should be different from access token secret. Minimum 32 characters (256 bits). | N/A | 256-bit random string from secure config |
refreshToken.expiresIn | REQUIRED. How long refresh tokens are valid. Longer-lived for better UX (users stay logged in). Format: 7d, 30d, or seconds. | N/A | 7d to 30d |
refreshToken.rotation | Deprecated. 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/A | Omit (always on) |
refreshToken.reuseDetection | Detect when an old refresh token is reused (sign of theft). When detected, invalidates entire token family and forces re-authentication. | false | true |
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: UseHS256for simplicity,RS256for microservicesaccessToken.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
},
| Option | Type | Default | Description |
|---|---|---|---|
autoRun | boolean | true | Apply 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(),
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.
identifierType | Accepts |
|---|---|
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'
},
| Style | Example (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.
- Nodemailer (SMTP)
- AWS SES (SDK with IAM)
- Console (Development)
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',
},
},
Uses AWS SDK v3 with automatic IAM role discovery. Perfect for EC2/ECS/containers.
- npm
- Yarn
- pnpm
- Bun
npm install @aws-sdk/client-sesv2
yarn add @aws-sdk/client-sesv2
pnpm add @aws-sdk/client-sesv2
bun add @aws-sdk/client-sesv2
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
import { NodemailerEmailProvider } from '@nauth-toolkit/email-nodemailer';
emailProvider: new NodemailerEmailProvider({
transport: {
SES: {
sesClient: new SESv2Client({
region: process.env.AWS_REGION || 'us-east-1',
// Credentials automatically discovered from:
// 1. IAM role (when running on EC2/ECS/containers) - RECOMMENDED
// 2. Environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
// 3. Shared credentials file (~/.aws/credentials)
}),
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',
},
},
The AWS SES SDK transport automatically uses IAM roles when running on AWS infrastructure (EC2, ECS, Lambda), eliminating the need to manage credentials manually.
import { ConsoleEmailProvider } from '@nauth-toolkit/email-console';
emailProvider: new ConsoleEmailProvider(),
email: {
globalVariables: {
appName: 'My App',
supportEmail: 'support@myapp.com',
},
},
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
},
},
Some notification keys map to a differently-named template type:
mfaFirstEnablednotification uses themfaEnabledemail template (TemplateType.MFA_ENABLED)adaptiveMfaRiskDetectednotification uses theadaptiveMfaRiskAlertemail template (TemplateType.ADAPTIVE_MFA_RISK_ALERT)
SMS Provider
Configure SMS delivery for phone verification, MFA, and password reset.
- Twilio (Production)
- AWS SNS
- Console (Development)
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!,
}),
import { AWSSMSProvider } from '@nauth-toolkit/sms-aws-sns';
smsProvider: new AWSSMSProvider({
region: process.env.AWS_REGION as string,
originationNumber: process.env.AWS_SMS_ORIGINATION as string, // e.g. '+12345678901' or 'MyApp'
accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
// apiMode: 'end-user-messaging-sms', // optional; use for configuration sets
// configurationSetName: 'default',
}),
import { ConsoleSMSProvider } from '@nauth-toolkit/sms-console';
smsProvider: new ConsoleSMSProvider(),
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.
- NestJS
- Express
import { Logger } from '@nestjs/common';
logger: {
instance: new Logger('NAuth'),
enablePiiRedaction: true, // Redact emails, IPs, tokens
logLevel: 'debug',
},
// Optional: Any logger implementing LoggerService interface
logger: {
instance: yourLoggerInstance, // console, Winston, Pino, etc.
enablePiiRedaction: true,
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:
| Method | Description | Use 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:
| Option | Description | Default | Recommended |
|---|---|---|---|
expiresIn | How long verification codes/links are valid (seconds). Balance security vs UX. | 3600 | 3600 (1 hour) |
maxAttempts | Max attempts to verify a single code. Prevents brute force. | 3 | 3 |
resendDelay | Minimum time between resend requests (seconds). Prevents spam. | 60 | 60 (1 minute) |
rateLimitMax | Maximum verification emails per time window. Prevents email bombing. | 3 | 3 |
rateLimitWindow | Time window for rate limiting (seconds). | 3600 | 3600 (1 hour) |
maxAttemptsPerUser | Max verification attempts per user per window. Prevents brute force. | 10 | 10 |
maxAttemptsPerIP | Max verification attempts per IP per window. Prevents distributed attacks. | 20 | 20 |
attemptWindow | Time window for attempt limits (seconds). | 3600 | 3600 (1 hour) |
baseUrl | Base 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). | none | https://myapp.com or http://localhost:4200 for development |
Phone Verification Options:
| Option | Description | Default | Recommended |
|---|---|---|---|
codeLength | Length of SMS verification code. Longer = more secure but harder to type. | 6 | 6 |
expiresIn | How long SMS codes are valid (seconds). Shorter for security (SMS can be intercepted). | 300 | 300 (5 minutes) |
maxAttempts | Max attempts to verify a single code. Prevents brute force. | 3 | 3 |
resendDelay | Minimum time between SMS resends (seconds). Prevents SMS spam and cost abuse. | 60 | 60 (1 minute) |
rateLimitMax | Maximum SMS per time window. Important: SMS costs money, prevent abuse! | 3 | 3 |
rateLimitWindow | Time window for SMS rate limiting (seconds). | 3600 | 3600 (1 hour) |
maxAttemptsPerUser | Max verification attempts per user per window. | 10 | 10 |
maxAttemptsPerIP | Max verification attempts per IP per window. | 20 | 20 |
attemptWindow | Time window for attempt limits (seconds). | 3600 | 3600 (1 hour) |
Other Options:
| Option | Description | Use Case |
|---|---|---|
enabled | Enable/disable user signups globally. | Set to false to close registration (invite-only apps) |
allowDuplicatePhones | Allow 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:
| Option | Description | Default | Recommended |
|---|---|---|---|
minLength | Minimum password length. NIST recommends at least 8 characters. | 8 | 8 or higher |
maxLength | Maximum password length. Prevents DoS attacks from Argon2 hashing very long strings. | 128 | 128 |
requireUppercase | Require at least one uppercase letter (A-Z). | false | true for moderate security |
requireLowercase | Require at least one lowercase letter (a-z). | false | true for moderate security |
requireNumbers | Require at least one number (0-9). | false | true for moderate security |
requireSpecialChars | Require at least one special character. Defaults to !@#$%^&* if specialChars is not set. | false | true for high security |
specialChars | Custom set of characters that satisfy requireSpecialChars. Overrides the default !@#$%^&* set. | none | '$#!@' or domain-appropriate set |
preventCommon | Block common passwords (e.g., password123, qwerty). Uses built-in list of 10,000+ common passwords. | false | true (strongly recommended) |
preventUserInfo | Block passwords containing user's email or username. Prevents john@example.com using john123. | false | true |
historyCount | Number of previous passwords to remember. Prevents password reuse. 0 = disabled. | 0 | 5 for compliance, 0 for simplicity |
expiryDays | Force password change after N days. 0 = never expires. Note: NIST no longer recommends forced expiration. | 0 | 0 (disabled) or 90 for compliance |
Password Reset Options
Controls the forgot-password flow (code delivery is handled by your configured email/SMS provider).
| Option | Description | Default |
|---|---|---|
codeLength | Verification code length sent to the user. | 6 |
expiresIn | Code expiry in seconds. | 900 (15 minutes) |
rateLimitMax | Maximum reset requests per time window. Prevents abuse. | 3 |
rateLimitWindow | Time window for rate limiting (seconds). | 3600 (1 hour) |
maxAttempts | Maximum code verification attempts per code. Prevents brute force. | 3 |
baseUrl | Server-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).
| Option | Description | Default |
|---|---|---|
codeLength | Verification code length. | 6 |
expiresIn | Code expiry in seconds. Longer than user-initiated (admin context). | 3600 (1 hour) |
maxAttempts | Maximum code verification attempts per code. | 3 |
Account Lockout Options:
| Option | Description | Default | Recommended |
|---|---|---|---|
enabled | Enable IP-based account lockout after failed login attempts. | false | true |
maxAttempts | Number of failed login attempts before lockout. Too low = UX issues, too high = security risk. | 5 | 5 |
attemptWindow | Time window in seconds over which maxAttempts is counted. Prevents indefinite accumulation. | 3600 | 3600 (1 hour) |
duration | Lockout duration in seconds. After this time, the lockout lifts. | 900 | 900 (15 minutes) |
resetOnSuccess | Reset failed attempt counter on successful login. Recommended for better UX. | true | true |
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:
| Option | Description | Default | Recommended |
|---|---|---|---|
cookieName | Name of the CSRF token cookie. | nauth_csrf_token | nauth_csrf_token |
headerName | HTTP header name where frontend sends CSRF token. | x-csrf-token | x-csrf-token |
tokenLength | Length of CSRF token in bytes. Longer = more secure. | 32 | 32 (256 bits) |
excludedPaths | Paths that don't require CSRF tokens (e.g., webhooks, public APIs). | empty array | /webhook, /api/public as needed |
cookieOptions.secure | Require HTTPS for CSRF cookie. Set to false for localhost development. | true | true (false for localhost) |
cookieOptions.sameSite | SameSite cookie attribute. strict = most secure, lax = allows top-level navigation. | strict | strict |
cookieOptions.domain | Cookie 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:
| Method | Description | Best For | CSRF Required? |
|---|---|---|---|
'json' | Tokens returned in response body only. Frontend stores in memory/localStorage. Sent via Authorization: Bearer header. | Mobile apps, SPAs, API-first apps | No |
'cookies' | Tokens set as httpOnly cookies. Browser automatically sends with requests. More secure (XSS-resistant). | Traditional web apps, server-rendered apps | Yes |
'hybrid' | Cookies for web origins, JSON for mobile origins. Best of both worlds. | Apps with both web and mobile clients | Yes (for web) |
Cookie Options:
| Option | Description | Default | Recommended |
|---|---|---|---|
cookieNamePrefix | Prefix for all auth cookies. Prevents conflicts with other cookies. | nauth_ | nauth_ |
secure | Require HTTPS. Cookies won't be sent over HTTP. Set to false for localhost development. | true | true (false for localhost) |
sameSite | Controls when cookies are sent. strict = same-site only, lax = allows top-level navigation, none = all requests (requires secure: true). | strict | strict (or lax for better UX) |
domain | Cookie domain. Set to .myapp.com to share cookies across app.myapp.com and api.myapp.com. | none | .myapp.com for subdomain sharing |
path | Cookie path. Usually / for all routes. | / | / |
Security Comparison:
| Aspect | JSON (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:
- Route-level override —
@TokenDelivery('json' | 'cookies')(NestJS) ornauth.helpers.tokenDelivery(...)(Express/Fastify). Typically used for dedicated/mobileendpoints. - 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)
},
},
| Property | Type | Purpose |
|---|---|---|
webOrigins | string[] | Origins that resolve to cookies delivery when no route-level override is set. |
nativeOrigins | string[] | Origins that resolve to json delivery when no route-level override is set. |
cookieRefreshExpiresIn | string | number | Refresh token TTL applied when a request resolves to cookies delivery. Falls back to jwt.refreshToken.expiresIn. |
jsonRefreshExpiresIn | string | number | Refresh token TTL applied when a request resolves to json delivery. Falls back to jwt.refreshToken.expiresIn. |
cookieRefreshExpiresIn and jsonRefreshExpiresIn are read only when tokenDelivery.method === 'hybrid'. They're ignored in 'json' or 'cookies' mode.
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'] },
],
| Option | Type | Default | Description |
|---|---|---|---|
adminGuards | GuardLike[] | [] | Guards applied only to access: 'admin' routes. |
delivery | 'json' | 'cookies' | inherit | Forces one transport. Requires tokenDelivery.method: 'hybrid' unless it matches the configured method. |
enabled | boolean | true | Set false to mount nothing. |
exclude | NAuthRouteKey[] | [] | Routes to leave out. An unknown key throws at startup. |
groups | NAuthRouteGroup[] | all but admin, apiKeysAdmin | Which bundles to mount. |
guards | GuardLike[] | [] | Guards applied to every route in the bundle. |
prefix | string | 'auth' | Path prefix. On Fastify the prefix comes from register() instead. |
routeGuards | Partial<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
},
},
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)
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, ... },
},
| Option | Description | Default |
|---|---|---|
frontendBaseUrl | Required. Base URL of your frontend app. Used to resolve relative returnTo redirect paths after OAuth. | — |
allowAbsoluteReturnTo | Allow absolute URLs in returnTo query param. Enable only for multi-frontend setups. | false |
allowedReturnToOrigins | Origin 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.
| Option | Description | Default |
|---|---|---|
gracePeriod | Days from account creation during which an un-enrolled user can still authenticate. 0 enforces setup immediately. | 7 |
grace.skipForSignup | Skip 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 },
},
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)
},
| Option | Description | Default |
|---|---|---|
enabled | Enable anonymous usage telemetry | true |
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.
Social OAuth endpoints (/auth/social/*) are not protected by reCAPTCHA — OAuth providers handle their own bot protection.
- npm
- Yarn
- pnpm
- Bun
npm install @nauth-toolkit/recaptcha
yarn add @nauth-toolkit/recaptcha
pnpm add @nauth-toolkit/recaptcha
bun add @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:
| Option | Description | Default |
|---|---|---|
enabled | Enable reCAPTCHA validation on auth endpoints. | false |
provider | Provider instance. Choose RecaptchaV2Provider, RecaptchaV3Provider, or RecaptchaEnterpriseProvider. | — |
minimumScore | Default minimum score (0.0–1.0). Used when no per-action override exists in actionScores. v3/Enterprise only. | 0.5 |
actionScores | Per-action score overrides (Record<string, number>). Keys: login, signup, password_reset, etc. Falls back to minimumScore. | — |
validateOnStartup | Validate credentials at startup. 'warn': log warning on failure. 'error': halt startup. false: skip. | 'warn' |
Choosing a provider:
| Provider | Interaction | Best For |
|---|---|---|
RecaptchaV3Provider | Invisible, score-based | Most web apps — no user friction |
RecaptchaV2Provider | Checkbox ("I'm not a robot") | Highest-risk actions, legacy setups |
RecaptchaEnterpriseProvider | Score-based with advanced signals | Enterprise — 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
},
},
| Option | Default | Description |
|---|---|---|
enabled | false | Enable API key authentication |
allowUserCreation | false | Allow end users (not just admins) to create keys |
header | 'X-API-Key' | Header carrying the key; when present it is the only credential used |
maxKeysPerUser | 10 | Maximum active keys per user |
maxExpiryDays | — | Caps finite expiry; unset means no cap |
allowIndefinite | true | Allow never-expiring keys |
trackUsageIp | true | Record the source IP of each key's last use |
globalAllowlist | false | Accept keys on all protected routes (opt-out via @DenyApiKey()) |
ipRestrictions.enabled | true | Allow per-key IP allowlists (IPs / IPv4 CIDR) |
ipRestrictions.requireForNewKeys | false | Require an allowlist on every new key |
ipRestrictions.maxIpsPerKey | 20 | Max 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():
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:
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
- Challenge System - Understanding verification flows
- Storage - Database and transient storage
- Error Handling - Exception handling patterns
- OIDC Provider - Configuring the identity provider