Authentication Services


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.

Authentication Architecture

The authentication system in nself is designed with security and scalability in mind:

  • JWT-Based: Stateless authentication using JSON Web Tokens
  • OAuth Integration: Support for Google, GitHub, Facebook, and custom providers
  • Multi-Factor Auth: TOTP and SMS-based 2FA support
  • Role-Based Access: Hierarchical permissions system
  • Session Management: Secure session handling with refresh tokens

Security First

All authentication flows follow OWASP security guidelines with password hashing, rate limiting, and comprehensive audit logging.

Quick Start

Basic Email/Password Authentication

# 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

Frontend Integration

// 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>
  )
}

JWT Token Configuration

JWT Structure

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"
  }
}

Custom JWT Claims

// 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
        }
      }
    }
  }
}

OAuth Providers

Google OAuth

# 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

# 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

# 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"

Multi-Factor Authentication

TOTP (Time-based One-Time Password)

# 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-Based MFA

# 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>
  )
}

Role-Based Access Control

Defining Roles and Permissions

-- 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)
);

Seed Default Roles

-- 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

Permission Checking

// 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)
  }
}

Session Management

Refresh Token Flow

# 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 Security

Password Policies

# 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 Flow

// 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])
  }
}

Account Verification

Email Verification

# 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 & Security

Authentication Rate Limiting

# 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

Security Headers and CORS

# 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 Logging

Authentication Events

-- 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
  }
}

API Integration

Protected API Endpoints

// 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)
  }
}

Testing Authentication

Unit Tests

// 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')
  })
})

Integration Tests

// 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)
  })
})

Troubleshooting

Common Issues

# 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"

Debug Mode

# 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

Next Steps

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.