nself provides comprehensive authentication services built on modern standards including JWT tokens, OAuth providers, and multi-factor authentication. This guide covers all authentication features and integration patterns.
The authentication system in nself is designed with security and scalability in mind:
All authentication flows follow OWASP security guidelines with password hashing, rate limiting, and comprehensive audit logging.
# Configure authentication in .env.local
AUTH_JWT_SECRET=your-super-secure-jwt-secret
AUTH_JWT_EXPIRES_IN=24h
AUTH_REFRESH_TOKEN_EXPIRES_IN=30d
# Enable email/password authentication
AUTH_EMAIL_SIGNUP_ENABLED=true
AUTH_EMAIL_SIGNIN_ENABLED=true
# Email verification settings
AUTH_EMAIL_VERIFICATION_REQUIRED=true
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-app@gmail.com
SMTP_PASS=your-app-password
// 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>
)
}
nself 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
}
}
}
}
}
# Google OAuth configuration
AUTH_GOOGLE_ENABLED=true
AUTH_GOOGLE_CLIENT_ID=your-google-client-id
AUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret
AUTH_GOOGLE_CALLBACK_URL=https://yourapp.com/auth/callback/google
# Optional: Restrict to specific domains
AUTH_GOOGLE_ALLOWED_DOMAINS=yourcompany.com,partner.com
// Frontend Google OAuth integration
import { useAuth } from '@nself/auth-react'
function GoogleSignIn() {
const { signInWithProvider } = useAuth()
return (
<button onClick={() => signInWithProvider('google')}>
Sign in with Google
</button>
)
}
# GitHub OAuth configuration
AUTH_GITHUB_ENABLED=true
AUTH_GITHUB_CLIENT_ID=your-github-client-id
AUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
AUTH_GITHUB_CALLBACK_URL=https://yourapp.com/auth/callback/github
# Optional: Restrict to organization members
AUTH_GITHUB_ALLOWED_ORGS=your-org,partner-org
# Custom OAuth provider configuration
auth:
providers:
custom:
enabled: true
name: "Azure AD"
client_id: "azure-client-id"
client_secret: "azure-client-secret"
authorize_url: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize"
token_url: "https://login.microsoftonline.com/tenant/oauth2/v2.0/token"
user_info_url: "https://graph.microsoft.com/v1.0/me"
scopes: ["openid", "profile", "email"]
user_mapping:
id: "id"
email: "mail"
name: "displayName"
avatar: "photo"
callback_url: "https://yourapp.com/auth/callback/azure"
# 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 interactions
Now that you understand authentication in nself:
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.