ɳSelfɳSELFDOCS
  • Getting Started

    • Introduction
    • Quick Start
    • Installation
    • Your First Project
  • Core Concepts

    • Architecture Overview
    • Project Structure
    • Configuration
    • Environments
  • Services

    • PostgreSQL Database
    • Hasura GraphQL
    • Authentication
    • Real-Time Communication
    • Storage (MinIO)
    • Email Configuration
    • Redis Cache
    • Search Engines
    • Functions
    • MLflow (ML Tracking)
    • Monitoring & Metrics
    • Admin UI
    • Dashboard
  • Database Tools

    • Schema Management
    • Migrations
    • Seeding Data
    • Backup & Restore
    • dbdiagram.io Sync
  • Microservices

    • NestJS Services
    • BullMQ Workers
    • Go Services
    • Python Services
  • CLI Reference

    • Complete Command Reference
    • Core Commands
    • Database Commands
    • Service Management
    • Production Commands
  • Deployment

    • Local Development
    • Production Setup
    • SSL/TLS Configuration
    • Domain Configuration
    • Environment Variables
  • Advanced Topics

    • Multi-Tenancy & SaaS
    • Security & Hardening
    • Custom Actions
    • Webhooks
    • Performance Tuning
    • Troubleshooting
  • Migration Guides

    • From Supabase
    • From Nhost
    • From Firebase
  • Resources

    • Changelog
    • Licensing
    • FAQ
    • Contributing
    • Support

Authentication Services

v0.9.5Updated for ɳSelf v0.9.5 - OAuth 2.0 support

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

Service Overview

The authentication service is a required core service in every nself deployment. It provides:

  • JWT-Based: Stateless authentication using JSON Web Tokens with Hasura integration
  • OAuth 2.0 / OIDC: 13 providers with PKCE support, automatic token refresh, and account linking
  • Multi-Factor Auth: TOTP and SMS-based 2FA support
  • Role-Based Access: Hierarchical permissions system with Hasura claims
  • Session Management: Secure session handling with refresh tokens
  • Magic Links: Passwordless authentication via email
  • User Management API: Full REST API for user operations

Service Details

  • Version: 0.36.0 (default)
  • Port: 4000
  • Container: ${PROJECT_NAME}_auth
  • URL: https://auth.<domain>

Security First

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

Quick Start

Configuration

# 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.com

Development Mode

In development, ɳSelf automatically configures MailPit to capture all authentication emails (verification, password reset, etc.). View emails at https://mail.local.nself.org.

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

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

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

New in v0.9.5

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.

Supported OAuth Providers (13)

Social & Developer

  • ✓ Google (OAuth 2.0 + OIDC)
  • ✓ GitHub
  • ✓ Facebook
  • ✓ Twitter/X (with PKCE)
  • ✓ LinkedIn
  • ✓ Apple (with PKCE)

Enterprise & Platforms

  • ✓ Microsoft / Azure AD
  • ✓ Slack
  • ✓ Discord
  • ✓ Twitch
  • ✓ GitLab
  • ✓ Bitbucket
  • ✓ Spotify

Key Features

  • Automatic Token Refresh: Background service refreshes OAuth tokens before expiry
  • Account Linking: Users can link multiple OAuth providers to a single account
  • PKCE Support: Proof Key for Code Exchange for mobile apps (Apple, Twitter/X)
  • Token Rotation: Secure refresh token rotation on every use
  • Account Merging: Combine OAuth providers from different user accounts
  • Comprehensive Audit Logging: Track all OAuth events and token operations

Quick Start

# 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

Google OAuth Setup

# Configure Google OAuth
ɳSelf oauth config google \
  --client-id=123456789.apps.googleusercontent.com \
  --client-secret=GOCSPX-your-secret-here

# Test configuration
ɳSelf oauth test google

Where to Get Google Credentials:

  1. Go to Google Cloud Console
  2. Create a new project or select existing project
  3. Navigate to "APIs & Services" → "Credentials"
  4. Create "OAuth 2.0 Client ID" (Web application)
  5. Add authorized redirect URI: https://auth.yourdomain.com/oauth/google/callback
  6. Copy Client ID and Client Secret

GitHub OAuth Setup

# Configure GitHub OAuth
ɳSelf oauth config github \
  --client-id=Iv1.abc123def456 \
  --client-secret=your-github-secret-here

# Test configuration
ɳSelf oauth test github

Where to Get GitHub Credentials:

  1. Go to GitHub Developer Settings
  2. Click "New OAuth App"
  3. Set callback URL: https://auth.yourdomain.com/oauth/github/callback
  4. Copy Client ID and generate Client Secret

Microsoft OAuth Setup (Azure AD)

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

Where to Get Microsoft Credentials:

  1. Go to Azure Portal
  2. Navigate to "Azure Active Directory" → "App registrations"
  3. Register new application
  4. Add redirect URI: https://auth.yourdomain.com/oauth/microsoft/callback
  5. Copy Application (client) ID and Directory (tenant) ID
  6. Create client secret under "Certificates & secrets"

Frontend Integration

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

Automatic Token Refresh

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 once

How Token Refresh Works

  1. When OAuth tokens are stored, expiration time is calculated
  2. Refresh is automatically scheduled 5 minutes before token expiry
  3. Background service processes the refresh queue periodically
  4. New access and refresh tokens are stored securely
  5. Old refresh tokens are invalidated
  6. Failed refreshes are retried up to 3 times
  7. If all retries fail, user must re-authenticate

Account Linking (Multi-Provider Support)

Users 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

Account Linking Safety Rules

  • Cannot unlink last auth method: User must have password OR at least one OAuth provider
  • Cannot link same provider twice: Each user can have max 1 account per provider
  • Provider account cannot be shared: Each provider account can only link to one user
// 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>
  )
}

PKCE Support (Mobile Apps)

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

OAuth CLI Commands Reference

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

  • Use HTTPS Only: OAuth requires HTTPS in production (automatically enforced)
  • Secure Client Secrets: Never commit secrets to version control
  • Validate Redirect URIs: Whitelist exact callback URLs in provider dashboards
  • CSRF Protection: State parameter validation (automatic in ɳSelf)
  • Token Storage: Access and refresh tokens encrypted in database
  • Audit Logging: All OAuth events logged for security review
  • Rate Limiting: OAuth endpoints are rate-limited to prevent abuse
  • Rotate Secrets: Update client secrets every 90 days

Database Schema

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

Troubleshooting OAuth

Redirect URI Mismatch

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

Token Refresh Failing

# 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

OAuth Service Not Starting

# 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

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 ɳSelf:

  • Hasura Integration - Connect authentication to GraphQL
  • Custom Actions - Add custom authentication logic
  • Webhooks - Handle authentication events
  • Production Setup - Secure authentication in production

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.