ɳSelf provides comprehensive authentication services using an integrated auth system, supporting JWT tokens, 13 OAuth providers with automatic token refresh, and multi-factor authentication. This guide covers all authentication features, CLI commands, and integration patterns.
The authentication service is a required core service in every nself deployment. It provides:
${PROJECT_NAME}_authhttps://auth.<domain>All authentication flows follow OWASP security guidelines with password hashing, rate limiting, and comprehensive audit logging.
# Core authentication settings in .env
AUTH_ENABLED=true
AUTH_VERSION=0.36.0
AUTH_JWT_SECRET=jwt-secret-key-minimum-32-characters
AUTH_REFRESH_TOKEN_SECRET=refresh-secret-key-minimum-32-chars
AUTH_ACCESS_TOKEN_EXPIRY=15m
AUTH_REFRESH_TOKEN_EXPIRY=7d
AUTH_CLIENT_URL=http://localhost:3000
# Email configuration (for verification, password reset)
AUTH_SMTP_HOST=mailpit # Development (MailPit)
AUTH_SMTP_PORT=1025
AUTH_SMTP_USER=""
AUTH_SMTP_PASS=""
AUTH_SMTP_SECURE=false
AUTH_SMTP_SENDER=noreply@yourdomain.comIn development, ɳSelf automatically configures MailPit to capture all authentication emails (verification, password reset, etc.). View emails at https://mail.local.nself.org.
// React/Next.js authentication hook
import { useAuth } from '@nself/auth-react'
function LoginForm() {
const { signIn, signUp, user, loading } = useAuth()
const handleSignIn = async (email: string, password: string) => {
try {
await signIn({ email, password })
} catch (error) {
console.error('Authentication failed:', error)
}
}
const handleSignUp = async (email: string, password: string) => {
try {
await signUp({
email,
password,
metadata: {
displayName: 'John Doe'
}
})
} catch (error) {
console.error('Registration failed:', error)
}
}
if (loading) return <div>Loading...</div>
if (user) return <div>Welcome, {user.email}!</div>
return (
<form onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.target)
handleSignIn(formData.get('email'), formData.get('password'))
}}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
<button type="submit">Sign In</button>
</form>
)
}ɳSelf generates JWT tokens with the following claims structure:
{
"sub": "user-uuid-here",
"email": "user@example.com",
"iat": 1640995200,
"exp": 1641081600,
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": ["user", "moderator"],
"x-hasura-default-role": "user",
"x-hasura-user-id": "user-uuid-here",
"x-hasura-org-id": "org-uuid-here"
},
"metadata": {
"displayName": "John Doe",
"avatar": "https://example.com/avatar.jpg"
}
}// Add custom claims in your auth service
export class CustomClaimsService {
async generateClaims(user: User): Promise<JWTClaims> {
const roles = await this.getUserRoles(user.id)
const permissions = await this.getUserPermissions(user.id)
return {
'https://hasura.io/jwt/claims': {
'x-hasura-allowed-roles': roles,
'x-hasura-default-role': user.defaultRole,
'x-hasura-user-id': user.id,
'x-hasura-org-id': user.organizationId,
'x-hasura-permissions': permissions
},
'custom-app-claims': {
subscription: user.subscriptionPlan,
features: user.enabledFeatures,
quotas: {
apiCalls: user.apiCallLimit,
storage: user.storageLimit
}
}
}
}
}Complete OAuth 2.0 / OpenID Connect implementation with 13 providers, automatic token refresh, PKCE support for mobile apps, and multi-provider account linking.
ɳSelf provides production-ready OAuth 2.0 and OpenID Connect authentication with comprehensive provider support, enterprise-grade security, and automatic token management.
# 1. Install OAuth handlers service (one-time setup)
ɳSelf oauth install
# 2. Enable providers
ɳSelf oauth enable --providers google,github,slack
# 3. Configure Google
ɳSelf oauth config google \
--client-id=123456789.apps.googleusercontent.com \
--client-secret=GOCSPX-your-secret-here
# 4. Configure GitHub
ɳSelf oauth config github \
--client-id=Iv1.abc123def456 \
--client-secret=your-github-secret-here
# 5. Build and start
nself build
nself start
# 6. Start automatic token refresh service
ɳSelf oauth refresh start
# 7. Test OAuth flow
ɳSelf oauth test google# Configure Google OAuth
ɳSelf oauth config google \
--client-id=123456789.apps.googleusercontent.com \
--client-secret=GOCSPX-your-secret-here
# Test configuration
ɳSelf oauth test googleWhere to Get Google Credentials:
https://auth.yourdomain.com/oauth/google/callback# Configure GitHub OAuth
ɳSelf oauth config github \
--client-id=Iv1.abc123def456 \
--client-secret=your-github-secret-here
# Test configuration
ɳSelf oauth test githubWhere to Get GitHub Credentials:
https://auth.yourdomain.com/oauth/github/callback# Configure Microsoft OAuth with tenant ID
ɳSelf oauth config microsoft \
--client-id=abc123-def456-ghi789 \
--client-secret=your-secret-here \
--tenant-id=your-tenant-id
# Test configuration
ɳSelf oauth test microsoftWhere to Get Microsoft Credentials:
https://auth.yourdomain.com/oauth/microsoft/callback// React/Next.js OAuth login
import { useAuth } from '@nself/auth-react'
function OAuthLoginButtons() {
const { signInWithProvider } = useAuth()
return (
<div className="space-y-3">
<button
onClick={() => signInWithProvider('google')}
className="oauth-button"
>
<GoogleIcon />
Sign in with Google
</button>
<button
onClick={() => signInWithProvider('github')}
className="oauth-button"
>
<GitHubIcon />
Sign in with GitHub
</button>
<button
onClick={() => signInWithProvider('microsoft')}
className="oauth-button"
>
<MicrosoftIcon />
Sign in with Microsoft
</button>
</div>
)
}
// Handle OAuth callback
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const token = params.get('token')
if (token) {
// Token is stored in httpOnly cookie by the server — no client-side storage needed
// The /api/auth/callback route sets the cookie automatically
window.location.href = '/dashboard'
}
}, [])OAuth access tokens typically expire after 1 hour. ɳSelf automatically refreshes tokens before they expire using the refresh token.
# Start automatic token refresh service (daemon mode)
ɳSelf oauth refresh start
# Check refresh service status
ɳSelf oauth refresh status
# Output shows:
# - Status: RUNNING
# - Last run: 3 minutes ago
# - Tokens refreshed: 12
# - Errors: 0
# Run refresh once manually (for cron jobs)
ɳSelf oauth refresh once
# Add to crontab for scheduled refresh
*/5 * * * * /usr/local/bin/nself oauth refresh onceUsers can link multiple OAuth providers to a single account. For example, a user can sign in with Google initially, then link their GitHub and Slack accounts.
# List user's linked OAuth accounts
ɳSelf oauth accounts <user_id>
# Output:
# Linked OAuth Providers:
#
# google
# Email: user@gmail.com
# Linked: 2026-01-15 10:30:00
# Token expires: 2026-01-15 11:30:00
#
# github
# Email: user@users.noreply.github.com
# Linked: 2026-01-20 14:00:00
# Link additional provider to user account
ɳSelf oauth link <user_id> github
# Unlink provider from user account
ɳSelf oauth unlink <user_id> github// Frontend: Link additional OAuth provider
function LinkAccountButton() {
const { user } = useAuth()
const handleLinkGitHub = () => {
// Redirect to OAuth flow with link parameter
window.location.href = `https://auth.yourdomain.com/oauth/github?link_to=${user.id}`
}
return (
<button onClick={handleLinkGitHub}>
Link GitHub Account
</button>
)
}Apple and Twitter/X OAuth providers support PKCE (Proof Key for Code Exchange) for enhanced security in mobile applications where client secrets cannot be securely stored.
// React Native: OAuth with PKCE
import { AuthSession } from 'expo-auth-session'
import * as Crypto from 'expo-crypto'
async function signInWithApple() {
// Generate code verifier and challenge
const codeVerifier = Crypto.randomUUID()
const codeChallenge = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
codeVerifier,
{ encoding: Crypto.CryptoEncoding.BASE64 }
)
// Start OAuth flow with PKCE
const result = await AuthSession.startAsync({
authUrl: `https://auth.yourdomain.com/oauth/apple?code_challenge=${codeChallenge}&code_challenge_method=S256`,
returnUrl: 'myapp://oauth-callback'
})
if (result.type === 'success') {
// Exchange code for token with code verifier
const response = await fetch('https://auth.yourdomain.com/oauth/apple/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: result.params.code,
code_verifier: codeVerifier
})
})
const { token } = await response.json()
// Store token and navigate to app
}
}# Installation and Setup
ɳSelf oauth install # Install OAuth handlers service
ɳSelf oauth enable --providers google,github,slack # Enable providers
ɳSelf oauth disable --providers facebook # Disable providers
ɳSelf oauth config <provider> --client-id=... --client-secret=... # Configure credentials
ɳSelf oauth test <provider> # Test configuration
ɳSelf oauth list # List all providers
ɳSelf oauth status # Show service status
# Account Management
ɳSelf oauth accounts <user_id> # List user's OAuth accounts
ɳSelf oauth link <user_id> <provider> # Link provider to account
ɳSelf oauth unlink <user_id> <provider> # Unlink provider from account
# Token Refresh Service
ɳSelf oauth refresh start # Start refresh daemon
ɳSelf oauth refresh stop # Stop refresh daemon
ɳSelf oauth refresh status # Check status
ɳSelf oauth refresh once # Run refresh once (for cron)-- OAuth provider accounts
CREATE TABLE auth.oauth_provider_accounts (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
provider_user_id VARCHAR(255) NOT NULL,
provider_account_email VARCHAR(255),
access_token TEXT,
refresh_token TEXT,
token_expires_at TIMESTAMPTZ,
id_token TEXT,
scopes TEXT[],
raw_profile JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(provider, provider_user_id),
UNIQUE(user_id, provider)
);
-- OAuth states (CSRF protection)
CREATE TABLE auth.oauth_states (
id UUID PRIMARY KEY,
state VARCHAR(64) UNIQUE NOT NULL,
provider VARCHAR(50) NOT NULL,
redirect_url TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '10 minutes')
);
-- Token refresh queue
CREATE TABLE auth.oauth_token_refresh_queue (
id UUID PRIMARY KEY,
oauth_account_id UUID NOT NULL REFERENCES auth.oauth_provider_accounts(id) ON DELETE CASCADE,
scheduled_at TIMESTAMPTZ NOT NULL,
last_attempt_at TIMESTAMPTZ,
attempts INT NOT NULL DEFAULT 0,
max_attempts INT NOT NULL DEFAULT 3,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- OAuth audit log
CREATE TABLE auth.oauth_audit_log (
id UUID PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
provider VARCHAR(50) NOT NULL,
event_type VARCHAR(50) NOT NULL, -- 'login', 'link', 'unlink', 'refresh', 'revoke'
ip_address INET,
user_agent TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);# Check configured callback URL
ɳSelf oauth test google
# Ensure exact match in provider dashboard:
# ✓ https://auth.yourdomain.com/oauth/google/callback
# ✗ https://auth.yourdomain.com/* (wildcard not allowed)# Check refresh status
ɳSelf oauth refresh status
# View failed refresh attempts
nself logs oauth-handlers --filter "refresh failed"
# Reset failed attempts and retry
psql $DATABASE_URL -c "
UPDATE auth.oauth_token_refresh_queue
SET attempts = 0, error_message = NULL
WHERE attempts >= max_attempts;
"
# Force refresh
ɳSelf oauth refresh once# Check service logs
nself logs oauth-handlers
# Verify environment variables
nself config get | grep OAUTH
# Check for port conflicts (default 3100)
docker ps | grep 3100
# Verify database connection
nself db status# Enable TOTP in configuration
AUTH_MFA_ENABLED=true
AUTH_MFA_TOTP_ENABLED=true
AUTH_MFA_TOTP_ISSUER="Your App Name"
AUTH_MFA_REQUIRED_FOR_ROLES="admin,moderator"// TOTP setup and verification
import { useAuth } from '@nself/auth-react'
import QRCode from 'qrcode.react'
function TOTPSetup() {
const { enableTOTP, verifyTOTP, user } = useAuth()
const [secret, setSecret] = useState('')
const [qrCodeUrl, setQrCodeUrl] = useState('')
const [verificationCode, setVerificationCode] = useState('')
const initializeTOTP = async () => {
try {
const { secret, qr_code_url } = await enableTOTP()
setSecret(secret)
setQrCodeUrl(qr_code_url)
} catch (error) {
console.error('Failed to initialize TOTP:', error)
}
}
const verifyAndActivate = async () => {
try {
await verifyTOTP(verificationCode)
alert('TOTP activated successfully!')
} catch (error) {
console.error('TOTP verification failed:', error)
}
}
return (
<div>
<h3>Setup Two-Factor Authentication</h3>
<button onClick={initializeTOTP}>Generate QR Code</button>
{qrCodeUrl && (
<div>
<QRCode value={qrCodeUrl} />
<p>Scan with your authenticator app</p>
<p>Manual entry key: {secret}</p>
<input
type="text"
placeholder="Enter verification code"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
/>
<button onClick={verifyAndActivate}>Verify & Activate</button>
</div>
)}
</div>
)
}# SMS MFA configuration
AUTH_MFA_SMS_ENABLED=true
SMS_PROVIDER=twilio
TWILIO_ACCOUNT_SID=your-twilio-sid
TWILIO_AUTH_TOKEN=your-twilio-token
TWILIO_PHONE_NUMBER=+1234567890
# Alternative: AWS SNS
SMS_PROVIDER=aws_sns
AWS_SNS_ACCESS_KEY=your-aws-key
AWS_SNS_SECRET_KEY=your-aws-secret
AWS_SNS_REGION=us-east-1// SMS MFA implementation
function SMSMFASetup() {
const { enableSMSMFA, verifySMSMFA } = useAuth()
const [phoneNumber, setPhoneNumber] = useState('')
const [verificationCode, setVerificationCode] = useState('')
const [codeSent, setCodeSent] = useState(false)
const sendVerificationCode = async () => {
try {
await enableSMSMFA(phoneNumber)
setCodeSent(true)
} catch (error) {
console.error('Failed to send SMS:', error)
}
}
const verifyCode = async () => {
try {
await verifySMSMFA(verificationCode)
alert('SMS MFA activated!')
} catch (error) {
console.error('SMS verification failed:', error)
}
}
return (
<div>
{!codeSent ? (
<div>
<input
type="tel"
placeholder="Phone number"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
/>
<button onClick={sendVerificationCode}>Send Code</button>
</div>
) : (
<div>
<input
type="text"
placeholder="Verification code"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
/>
<button onClick={verifyCode}>Verify</button>
</div>
)}
</div>
)
}-- Create roles table
CREATE TABLE auth_roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
-- Create permissions table
CREATE TABLE auth_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
resource VARCHAR(100) NOT NULL,
action VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Create role-permission mapping
CREATE TABLE auth_role_permissions (
role_id UUID REFERENCES auth_roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES auth_permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
-- Create user-role mapping
CREATE TABLE auth_user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES auth_roles(id) ON DELETE CASCADE,
granted_at TIMESTAMP DEFAULT NOW(),
granted_by UUID REFERENCES users(id),
PRIMARY KEY (user_id, role_id)
);-- Insert default roles
INSERT INTO auth_roles (name, description, is_default) VALUES
('user', 'Standard user with basic permissions', true),
('moderator', 'Moderator with content management permissions', false),
('admin', 'Administrator with full system access', false),
('super_admin', 'Super administrator with unrestricted access', false);
-- Insert permissions
INSERT INTO auth_permissions (name, description, resource, action) VALUES
('read:profile', 'Read user profile', 'profile', 'read'),
('update:profile', 'Update user profile', 'profile', 'update'),
('read:posts', 'Read posts', 'posts', 'read'),
('create:posts', 'Create posts', 'posts', 'create'),
('update:posts', 'Update posts', 'posts', 'update'),
('delete:posts', 'Delete posts', 'posts', 'delete'),
('moderate:posts', 'Moderate posts', 'posts', 'moderate'),
('manage:users', 'Manage users', 'users', 'manage'),
('system:admin', 'System administration', 'system', 'admin');
-- Assign permissions to roles
INSERT INTO auth_role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM auth_roles r, auth_permissions p
WHERE (r.name = 'user' AND p.name IN ('read:profile', 'update:profile', 'read:posts', 'create:posts'))
OR (r.name = 'moderator' AND p.name IN ('read:profile', 'update:profile', 'read:posts', 'create:posts', 'update:posts', 'moderate:posts'))
OR (r.name = 'admin' AND p.name IN ('read:profile', 'update:profile', 'read:posts', 'create:posts', 'update:posts', 'delete:posts', 'moderate:posts', 'manage:users'))
OR (r.name = 'super_admin'); -- Super admin gets all permissions// Backend permission checking
import { Injectable } from '@nestjs/common'
@Injectable()
export class AuthorizationService {
async checkPermission(
userId: string,
resource: string,
action: string
): Promise<boolean> {
const result = await this.db.query(`
SELECT COUNT(*) as count
FROM auth_user_roles ur
JOIN auth_role_permissions rp ON ur.role_id = rp.role_id
JOIN auth_permissions p ON rp.permission_id = p.id
WHERE ur.user_id = $1
AND p.resource = $2
AND p.action = $3
`, [userId, resource, action])
return result[0].count > 0
}
async getUserPermissions(userId: string): Promise<string[]> {
const result = await this.db.query(`
SELECT DISTINCT p.name
FROM auth_user_roles ur
JOIN auth_role_permissions rp ON ur.role_id = rp.role_id
JOIN auth_permissions p ON rp.permission_id = p.id
WHERE ur.user_id = $1
`, [userId])
return result.map(row => row.name)
}
}
// Usage in controller
@Controller('posts')
export class PostsController {
@Post()
@RequirePermissions('create:posts')
async createPost(@CurrentUser() user: User, @Body() postData: CreatePostDto) {
return this.postsService.create(user.id, postData)
}
@Delete(':id')
@RequirePermissions('delete:posts')
async deletePost(@Param('id') id: string) {
return this.postsService.delete(id)
}
}# Refresh token configuration
AUTH_REFRESH_TOKEN_ENABLED=true
AUTH_REFRESH_TOKEN_EXPIRES_IN=30d
AUTH_REFRESH_TOKEN_ROTATION=true
# Session security
AUTH_SESSION_SECURE_COOKIES=true
AUTH_SESSION_SAME_SITE=strict
AUTH_MAX_ACTIVE_SESSIONS=5// Automatic token refresh
import { useAuth } from '@nself/auth-react'
function AuthProvider({ children }) {
const { refreshToken, logout, token } = useAuth()
useEffect(() => {
// Set up automatic token refresh
const refreshInterval = setInterval(async () => {
if (token && isTokenExpiringSoon(token)) {
try {
await refreshToken()
} catch (error) {
console.error('Token refresh failed:', error)
logout()
}
}
}, 60000) // Check every minute
return () => clearInterval(refreshInterval)
}, [token, refreshToken, logout])
return <>{children}</>
}
function isTokenExpiringSoon(token: string): boolean {
try {
const payload = JSON.parse(atob(token.split('.')[1]))
const expiryTime = payload.exp * 1000
const currentTime = Date.now()
const timeUntilExpiry = expiryTime - currentTime
// Refresh if token expires in less than 5 minutes
return timeUntilExpiry < 5 * 60 * 1000
} catch {
return true
}
}# Password policy configuration
AUTH_PASSWORD_MIN_LENGTH=8
AUTH_PASSWORD_REQUIRE_UPPERCASE=true
AUTH_PASSWORD_REQUIRE_LOWERCASE=true
AUTH_PASSWORD_REQUIRE_NUMBERS=true
AUTH_PASSWORD_REQUIRE_SYMBOLS=true
AUTH_PASSWORD_PREVENT_COMMON=true
AUTH_PASSWORD_PREVENT_USER_INFO=true
# Password history
AUTH_PASSWORD_HISTORY_COUNT=5
AUTH_PASSWORD_CHANGE_REQUIRED_DAYS=90// Password reset implementation
export class PasswordResetService {
async requestPasswordReset(email: string): Promise<void> {
const user = await this.userService.findByEmail(email)
if (!user) {
// Don't reveal if user exists
return
}
const resetToken = crypto.randomUUID()
const expiryTime = new Date(Date.now() + 60 * 60 * 1000) // 1 hour
await this.db.query(`
INSERT INTO password_reset_tokens (user_id, token, expires_at)
VALUES ($1, $2, $3)
ON CONFLICT (user_id)
DO UPDATE SET token = $2, expires_at = $3, created_at = NOW()
`, [user.id, resetToken, expiryTime])
await this.emailService.sendPasswordResetEmail(
user.email,
resetToken
)
}
async resetPassword(
token: string,
newPassword: string
): Promise<void> {
const resetRecord = await this.db.query(`
SELECT user_id
FROM password_reset_tokens
WHERE token = $1 AND expires_at > NOW()
`, [token])
if (!resetRecord[0]) {
throw new Error('Invalid or expired reset token')
}
const hashedPassword = await bcrypt.hash(newPassword, 12)
await this.db.query(`
UPDATE users
SET password_hash = $1, updated_at = NOW()
WHERE id = $2
`, [hashedPassword, resetRecord[0].user_id])
// Clean up reset token
await this.db.query(`
DELETE FROM password_reset_tokens WHERE token = $1
`, [token])
}
}# Email verification settings
AUTH_EMAIL_VERIFICATION_REQUIRED=true
AUTH_EMAIL_VERIFICATION_EXPIRES_IN=24h
AUTH_EMAIL_VERIFICATION_RESEND_LIMIT=3
AUTH_EMAIL_VERIFICATION_RESEND_INTERVAL=60// Email verification flow
export class EmailVerificationService {
async sendVerificationEmail(userId: string): Promise<void> {
const user = await this.userService.findById(userId)
if (!user || user.emailVerified) {
return
}
const verificationToken = crypto.randomUUID()
const expiryTime = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
await this.db.query(`
INSERT INTO email_verification_tokens (user_id, token, expires_at)
VALUES ($1, $2, $3)
ON CONFLICT (user_id)
DO UPDATE SET token = $2, expires_at = $3, created_at = NOW()
`, [userId, verificationToken, expiryTime])
const verificationUrl = `${process.env.APP_URL}/verify-email?token=${verificationToken}`
await this.emailService.sendVerificationEmail(
user.email,
verificationUrl
)
}
async verifyEmail(token: string): Promise<void> {
const result = await this.db.query(`
SELECT user_id
FROM email_verification_tokens
WHERE token = $1 AND expires_at > NOW()
`, [token])
if (!result[0]) {
throw new Error('Invalid or expired verification token')
}
await this.db.query(`
UPDATE users
SET email_verified = true, email_verified_at = NOW()
WHERE id = $1
`, [result[0].user_id])
await this.db.query(`
DELETE FROM email_verification_tokens WHERE token = $1
`, [token])
}
}# Rate limiting configuration
AUTH_RATE_LIMIT_ENABLED=true
AUTH_RATE_LIMIT_LOGIN_ATTEMPTS=5
AUTH_RATE_LIMIT_LOGIN_WINDOW=900 # 15 minutes
AUTH_RATE_LIMIT_SIGNUP_ATTEMPTS=3
AUTH_RATE_LIMIT_SIGNUP_WINDOW=3600 # 1 hour
# Account lockout
AUTH_ACCOUNT_LOCKOUT_ENABLED=true
AUTH_ACCOUNT_LOCKOUT_ATTEMPTS=5
AUTH_ACCOUNT_LOCKOUT_DURATION=1800 # 30 minutes# CORS configuration
AUTH_CORS_ENABLED=true
AUTH_CORS_ORIGINS=https://yourapp.com,https://admin.yourapp.com
AUTH_CORS_CREDENTIALS=true
# Security headers
AUTH_SECURITY_HEADERS_ENABLED=true
AUTH_CSP_ENABLED=true
AUTH_HSTS_ENABLED=true-- Audit log table
CREATE TABLE auth_audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
event_type VARCHAR(50) NOT NULL,
event_data JSONB,
ip_address INET,
user_agent TEXT,
success BOOLEAN NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Index for performance
CREATE INDEX idx_auth_audit_logs_user_id ON auth_audit_logs(user_id);
CREATE INDEX idx_auth_audit_logs_event_type ON auth_audit_logs(event_type);
CREATE INDEX idx_auth_audit_logs_created_at ON auth_audit_logs(created_at);// Audit logging service
export class AuthAuditService {
async logEvent(
userId: string | null,
eventType: string,
eventData: any,
request: Request,
success: boolean
): Promise<void> {
const ipAddress = this.extractIpAddress(request)
const userAgent = request.headers['user-agent']
await this.db.query(`
INSERT INTO auth_audit_logs
(user_id, event_type, event_data, ip_address, user_agent, success)
VALUES ($1, $2, $3, $4, $5, $6)
`, [userId, eventType, eventData, ipAddress, userAgent, success])
}
private extractIpAddress(request: Request): string {
return request.headers['x-forwarded-for']?.split(',')[0] ||
request.headers['x-real-ip'] ||
request.connection.remoteAddress ||
'unknown'
}
}
// Usage in authentication controller
@Post('login')
async login(@Body() loginDto: LoginDto, @Req() request: Request) {
try {
const result = await this.authService.login(loginDto.email, loginDto.password)
await this.auditService.logEvent(
result.user.id,
'login_success',
{ email: loginDto.email },
request,
true
)
return result
} catch (error) {
await this.auditService.logEvent(
null,
'login_failure',
{ email: loginDto.email, error: error.message },
request,
false
)
throw error
}
}// JWT guard for protecting routes
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest()
const token = this.extractTokenFromHeader(request)
if (!token) {
return false
}
try {
const payload = await this.jwtService.verifyAsync(token)
request.user = payload
return true
} catch {
return false
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? []
return type === 'Bearer' ? token : undefined
}
}
// Usage in controllers
@Controller('api/protected')
@UseGuards(JwtAuthGuard)
export class ProtectedController {
@Get('profile')
getProfile(@CurrentUser() user: User) {
return { user }
}
@Post('data')
@RequirePermissions('create:data')
createData(@CurrentUser() user: User, @Body() data: any) {
return this.dataService.create(user.id, data)
}
}// Authentication service tests
import { Test, TestingModule } from '@nestjs/testing'
import { AuthService } from './auth.service'
describe('AuthService', () => {
let service: AuthService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile()
service = module.get<AuthService>(AuthService)
})
it('should create JWT token for valid user', async () => {
const user = { id: 'test-id', email: 'test@example.com' }
const token = await service.generateToken(user)
expect(token).toBeDefined()
expect(typeof token).toBe('string')
})
it('should throw error for invalid credentials', async () => {
await expect(
service.login('invalid@example.com', 'wrongpassword')
).rejects.toThrow('Invalid credentials')
})
})// E2E authentication tests
import { Test } from '@nestjs/testing'
import * as request from 'supertest'
import { AppModule } from '../src/app.module'
describe('Authentication (e2e)', () => {
let app
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleFixture.createNestApplication()
await app.init()
})
it('/auth/login (POST)', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
})
.expect(200)
.expect((res) => {
expect(res.body.accessToken).toBeDefined()
expect(res.body.refreshToken).toBeDefined()
expect(res.body.user).toBeDefined()
})
})
it('/auth/protected (GET) should require authentication', () => {
return request(app.getHttpServer())
.get('/auth/protected')
.expect(401)
})
it('/auth/protected (GET) with valid token', async () => {
// First login to get token
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
})
const token = loginResponse.body.accessToken
return request(app.getHttpServer())
.get('/auth/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200)
})
})# Check authentication service logs
nself logs auth-service
# Verify JWT configuration
nself config check AUTH_JWT_SECRET
# Test token generation
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "test@example.com", "password": "password123"}'
# Verify token
curl -X GET http://localhost:3000/auth/verify \
-H "Authorization: Bearer your-jwt-token-here"# Enable authentication debug logging
AUTH_DEBUG_MODE=true
AUTH_LOG_LEVEL=debug
# This will log:
# - All authentication attempts
# - JWT token generation and verification
# - Permission checks
# - OAuth provider interactionsNow that you understand authentication in ɳSelf:
The authentication system provides enterprise-grade security with flexible configuration options. Start with basic email/password authentication and gradually add OAuth providers and multi-factor authentication as needed.