Skip to content

Single Unified Authentication Flow

🎯 Overview

The Identity Server implements a sophisticated Single Unified Authentication Flow that elegantly handles both new user registration and existing user login through a single, streamlined process. This design eliminates the traditional complexity of separate registration and login flows while maintaining security and providing seamless user experience.


🏗️ Architecture Design

Core Philosophy

The unified flow operates on the principle of "authenticate first, determine user state second". This approach provides several advantages:

  • Simplified UX: Users don't need to know if they have an account
  • Reduced friction: Single flow for all authentication scenarios
  • Enhanced security: Email verification required for all access
  • Seamless onboarding: New users are automatically provisioned

Flow Diagram

mermaid
graph TD
    A[User Enters Email + Name] --> B[POST /auth/request-otp]
    B --> C[Generate & Store OTP]
    C --> D[Send OTP Email]
    D --> E[User Enters OTP + SessionID]
    E --> F[POST /auth/verify-otp]
    F --> G{User Exists?}
    G -->|No| H[Create New User - 'pending' status]
    G -->|Yes| I[Update Existing User - login tracking]
    H --> J[Return JWT Token + User Status]
    I --> J
    J --> K{Check Status}
    K -->|pending| L[Show Onboarding Flow]
    K -->|active| M[Show Main Application]

🔄 Step-by-Step Implementation

Step 1: Request OTP (POST /auth/request-otp)

Purpose

Initiate authentication flow for any user, regardless of account status.

Request Format

javascript
POST /auth/request-otp
Content-Type: application/json

{
  "email": "user@example.com",
  "fullName": "John Doe"
}

Implementation Logic

javascript
// No database lookup required - universal flow
export async function requestOTP(request, env) {
  const { email, fullName } = await request.json();
  
  // Generate secure OTP
  const otp = generateSecureOTP(); // 6-digit numeric code
  const sessionId = generateUUID();
  
  // Store temporarily (5-minute expiration)
  await env.OTP_STORE.put(sessionId, JSON.stringify({
    email,
    fullName,
    otp,
    createdAt: new Date().toISOString(),
    verified: false
  }), { expirationTtl: 300 }); // 5 minutes
  
  // Send OTP email
  await sendOTPEmail(email, fullName, otp, env);
  
  return new Response(JSON.stringify({
    success: true,
    sessionId,
    message: "OTP sent to your email address"
  }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  });
}

Key Features

  • No Pre-validation: System doesn't check if user exists
  • Universal Input: Same inputs for new and existing users
  • Secure Storage: OTP stored with automatic expiration
  • Email Delivery: Immediate OTP dispatch via email service

Security Measures

javascript
const generateSecureOTP = () => {
  // Cryptographically secure random 6-digit code
  const crypto = require('crypto');
  const buffer = crypto.randomBytes(4);
  const num = buffer.readUInt32BE(0);
  return String(num % 1000000).padStart(6, '0');
};

Step 2: Verify OTP (POST /auth/verify-otp)

Purpose

Verify the provided OTP and intelligently handle user creation or update.

Request Format

javascript
POST /auth/verify-otp
Content-Type: application/json

{
  "sessionId": "uuid-session-id",
  "otp": "123456"
}

Smart User Handling Implementation

javascript
export async function verifyOTP(request, env) {
  const { sessionId, otp } = await request.json();
  
  // Retrieve OTP data
  const otpDataStr = await env.OTP_STORE.get(sessionId);
  if (!otpDataStr) {
    return errorResponse('Invalid or expired session', 400);
  }
  
  const otpData = JSON.parse(otpDataStr);
  
  // Verify OTP
  if (otpData.otp !== otp) {
    return errorResponse('Invalid OTP', 400);
  }
  
  // 🧠 Smart User Decision Logic
  let user = await env.USERS_DB.prepare(
    'SELECT * FROM users WHERE email = ?'
  ).bind(otpData.email).first();
  
  if (!user) {
    // 🆕 NEW USER: Create with 'pending' status
    console.log(`Creating new user: ${otpData.email}`);
    
    const result = await env.USERS_DB.prepare(`
      INSERT INTO users (email, full_name, status, created_at, updated_at)
      VALUES (?, ?, 'pending', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
    `).bind(otpData.email, otpData.fullName).run();
    
    // Fetch newly created user
    user = await env.USERS_DB.prepare(
      'SELECT * FROM users WHERE id = ?'
    ).bind(result.meta.last_row_id).first();
    
  } else {
    // ✅ EXISTING USER: Update login tracking
    console.log(`Updating existing user: ${otpData.email}`);
    
    await env.USERS_DB.prepare(`
      UPDATE users SET 
        full_name = ?, 
        last_login_at = CURRENT_TIMESTAMP,
        login_count = COALESCE(login_count, 0) + 1, 
        updated_at = CURRENT_TIMESTAMP
      WHERE id = ?
    `).bind(otpData.fullName, user.id).run();
    
    // Refresh user data
    user = await env.USERS_DB.prepare(
      'SELECT * FROM users WHERE id = ?'
    ).bind(user.id).first();
  }
  
  // Generate JWT token
  const token = await generateJWT(user, env);
  
  // Clean up OTP
  await env.OTP_STORE.delete(sessionId);
  
  return new Response(JSON.stringify({
    success: true,
    token,
    user: {
      id: user.id,
      email: user.email,
      fullName: user.full_name,
      status: user.status,
      isNewUser: !user.last_login_at
    }
  }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  });
}

🚀 Benefits of This Design

✅ User Experience Advantages

1. Simplified Authentication

javascript
// Single flow for all users
const authenticate = async (email, fullName) => {
  // Step 1: Request OTP (same for everyone)
  const otpResponse = await fetch('/auth/request-otp', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, fullName })
  });
  
  // Step 2: User enters OTP (same for everyone)
  const sessionId = otpResponse.sessionId;
  // ... OTP verification ...
};

2. No Account Status Confusion

  • Users never see "Account doesn't exist" errors
  • No separate registration forms
  • No password complexity requirements
  • No "forgot password" flows needed

3. Seamless Experience

javascript
// Frontend handles response transparently
const handleAuthResponse = (response) => {
  const { user, token } = response;
  
  // Store authentication
  localStorage.setItem('authToken', token);
  
  // Route based on user status
  if (user.status === 'pending') {
    router.push('/onboarding');
  } else {
    router.push('/dashboard');
  }
};

✅ Technical Advantages

1. Reduced API Surface

javascript
// Traditional approach: Multiple endpoints
POST /auth/register      // New users
POST /auth/login         // Existing users
POST /auth/forgot        // Password reset
POST /auth/verify-email  // Email verification

// Unified approach: Single flow
POST /auth/request-otp   // Everyone
POST /auth/verify-otp    // Everyone

2. Simplified Client Logic

javascript
// No need to determine user existence
class AuthService {
  async authenticate(email, fullName) {
    // Always use the same flow
    const session = await this.requestOTP(email, fullName);
    return session; // Client handles OTP input
  }
  
  async verify(sessionId, otp) {
    // Always use the same verification
    const result = await this.verifyOTP(sessionId, otp);
    return result; // Server determines user state
  }
}

3. Enhanced Security

javascript
// Email verification required for ALL access
const securityBenefits = {
  emailVerification: 'Required for both new and existing users',
  bruteForceProtection: 'OTP expiration limits attack vectors',
  sessionSecurity: 'Temporary OTP storage with TTL',
  noPasswordStorage: 'Eliminates password-related vulnerabilities'
};

🔄 Integration with Onboarding System

Client-Side Integration Pattern

javascript
// React example: Unified authentication handling
const AuthenticationFlow = () => {
  const [step, setStep] = useState('email'); // 'email' | 'otp' | 'complete'
  const [sessionId, setSessionId] = useState(null);
  
  const handleEmailSubmit = async (email, fullName) => {
    const response = await authService.requestOTP(email, fullName);
    setSessionId(response.sessionId);
    setStep('otp');
  };
  
  const handleOTPSubmit = async (otp) => {
    const response = await authService.verifyOTP(sessionId, otp);
    
    // Store authentication
    setAuthToken(response.token);
    setUser(response.user);
    
    // Route based on user status
    if (response.user.status === 'pending') {
      // New user - show onboarding
      router.push('/onboarding');
    } else {
      // Existing user - go to main app
      router.push('/dashboard');
    }
  };
  
  return (
    <div>
      {step === 'email' && <EmailForm onSubmit={handleEmailSubmit} />}
      {step === 'otp' && <OTPForm onSubmit={handleOTPSubmit} />}
    </div>
  );
};

Status-Based Routing

javascript
// Onboarding system integration
const AppRouter = () => {
  const { user } = useAuth();
  
  return (
    <Routes>
      <Route path="/auth" element={<AuthenticationFlow />} />
      
      {/* Protected routes with status checking */}
      <Route path="/onboarding" element={
        <ProtectedRoute requiredStatus="pending">
          <OnboardingFlow />
        </ProtectedRoute>
      } />
      
      <Route path="/dashboard" element={
        <ProtectedRoute requiredStatus="active">
          <Dashboard />
        </ProtectedRoute>
      } />
    </Routes>
  );
};

const ProtectedRoute = ({ children, requiredStatus }) => {
  const { user } = useAuth();
  
  if (!user) {
    return <Navigate to="/auth" />;
  }
  
  if (user.status !== requiredStatus) {
    // Redirect based on actual status
    const redirectPath = user.status === 'pending' ? '/onboarding' : '/dashboard';
    return <Navigate to={redirectPath} />;
  }
  
  return children;
};

📊 User Status Management

Status Lifecycle

mermaid
graph LR
    A[Email Verification] --> B[pending]
    B --> C[Onboarding Complete] 
    C --> D[active]
    D --> E[Admin Action] 
    E --> F[suspended]
    F --> G[Reactivation]
    G --> D

Database Schema

sql
CREATE TABLE users (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT UNIQUE NOT NULL,
  full_name TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending', -- 'pending', 'active', 'suspended'
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  last_login_at DATETIME,
  login_count INTEGER DEFAULT 0,
  
  -- Onboarding tracking
  onboarding_completed_at DATETIME,
  onboarding_step INTEGER DEFAULT 0,
  
  -- Profile information
  company_name TEXT,
  role TEXT,
  phone TEXT,
  
  -- Metadata
  metadata TEXT -- JSON for additional fields
);

-- Indexes for performance
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_created_at ON users(created_at);

Status Transition Management

javascript
class UserStatusManager {
  static async updateStatus(userId, newStatus, env) {
    const validTransitions = {
      pending: ['active', 'suspended'],
      active: ['suspended'],
      suspended: ['active']
    };
    
    const user = await env.USERS_DB.prepare(
      'SELECT status FROM users WHERE id = ?'
    ).bind(userId).first();
    
    if (!validTransitions[user.status]?.includes(newStatus)) {
      throw new Error(`Invalid status transition: ${user.status} -> ${newStatus}`);
    }
    
    await env.USERS_DB.prepare(`
      UPDATE users SET 
        status = ?, 
        updated_at = CURRENT_TIMESTAMP,
        ${newStatus === 'active' && user.status === 'pending' ? 'onboarding_completed_at = CURRENT_TIMESTAMP,' : ''}
      WHERE id = ?
    `).bind(newStatus, userId).run();
    
    return { success: true, newStatus };
  }
  
  static async completeOnboarding(userId, profileData, env) {
    await env.USERS_DB.prepare(`
      UPDATE users SET 
        status = 'active',
        onboarding_completed_at = CURRENT_TIMESTAMP,
        company_name = ?,
        role = ?,
        phone = ?,
        updated_at = CURRENT_TIMESTAMP
      WHERE id = ? AND status = 'pending'
    `).bind(
      profileData.companyName,
      profileData.role,
      profileData.phone,
      userId
    ).run();
  }
}

🔒 Security Considerations

OTP Security

javascript
const OTP_CONFIG = {
  length: 6,                    // 6-digit codes
  expiration: 300,              // 5 minutes
  maxAttempts: 3,               // Rate limiting
  algorithm: 'crypto.randomBytes', // Cryptographically secure
  charSet: '0123456789'         // Numeric only for UX
};

const generateSecureOTP = () => {
  const crypto = require('crypto');
  let otp = '';
  
  for (let i = 0; i < OTP_CONFIG.length; i++) {
    const randomByte = crypto.randomBytes(1)[0];
    otp += OTP_CONFIG.charSet[randomByte % OTP_CONFIG.charSet.length];
  }
  
  return otp;
};

Session Management

javascript
const SESSION_CONFIG = {
  storage: 'KV Store',          // Cloudflare KV for scalability
  ttl: 300,                     // 5-minute expiration
  keyFormat: 'session:{uuid}',  // Namespaced keys
  encryption: true              // Encrypt stored data
};

const storeOTPSession = async (sessionId, data, env) => {
  const encryptedData = await encrypt(JSON.stringify(data));
  await env.OTP_STORE.put(
    `session:${sessionId}`, 
    encryptedData, 
    { expirationTtl: SESSION_CONFIG.ttl }
  );
};

Rate Limiting

javascript
class RateLimiter {
  static async checkRequestLimit(email, env) {
    const key = `rate_limit:${email}`;
    const current = await env.RATE_LIMIT_STORE.get(key);
    
    if (current && parseInt(current) >= 5) { // 5 requests per hour
      throw new Error('Rate limit exceeded. Please try again later.');
    }
    
    await env.RATE_LIMIT_STORE.put(
      key, 
      String((parseInt(current) || 0) + 1),
      { expirationTtl: 3600 } // 1 hour
    );
  }
  
  static async checkVerificationAttempts(sessionId, env) {
    const key = `verify_attempts:${sessionId}`;
    const attempts = await env.VERIFICATION_STORE.get(key);
    
    if (attempts && parseInt(attempts) >= 3) {
      // Invalidate session after 3 failed attempts
      await env.OTP_STORE.delete(sessionId);
      throw new Error('Maximum verification attempts exceeded');
    }
    
    await env.VERIFICATION_STORE.put(
      key,
      String((parseInt(attempts) || 0) + 1),
      { expirationTtl: 300 }
    );
  }
}

📈 Analytics and Monitoring

Authentication Metrics

javascript
const trackAuthenticationEvent = async (event, data, env) => {
  const metrics = {
    timestamp: new Date().toISOString(),
    event, // 'otp_requested', 'otp_verified', 'user_created', 'user_login'
    email: data.email,
    sessionId: data.sessionId,
    userAgent: data.userAgent,
    ipAddress: data.ipAddress,
    success: data.success
  };
  
  // Store in analytics database
  await env.ANALYTICS_DB.prepare(`
    INSERT INTO auth_events (timestamp, event, email, session_id, user_agent, ip_address, success, metadata)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  `).bind(
    metrics.timestamp,
    metrics.event,
    metrics.email,
    metrics.sessionId,
    metrics.userAgent,
    metrics.ipAddress,
    metrics.success,
    JSON.stringify(data.metadata || {})
  ).run();
};

Success Rate Monitoring

sql
-- Authentication success rates by time period
SELECT 
  DATE(timestamp) as date,
  event,
  COUNT(*) as total_attempts,
  SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful_attempts,
  ROUND(SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as success_rate
FROM auth_events 
WHERE timestamp >= datetime('now', '-7 days')
GROUP BY DATE(timestamp), event
ORDER BY date DESC, event;

-- New vs returning user ratios
SELECT 
  DATE(timestamp) as date,
  SUM(CASE WHEN event = 'user_created' THEN 1 ELSE 0 END) as new_users,
  SUM(CASE WHEN event = 'user_login' THEN 1 ELSE 0 END) as returning_users
FROM auth_events
WHERE timestamp >= datetime('now', '-30 days')
GROUP BY DATE(timestamp)
ORDER BY date DESC;

🎯 Best Practices Implementation

Error Handling

javascript
const handleAuthenticationError = (error, context) => {
  const errorMap = {
    'Invalid OTP': { status: 400, code: 'INVALID_OTP', retryable: true },
    'Expired session': { status: 400, code: 'SESSION_EXPIRED', retryable: false },
    'Rate limit exceeded': { status: 429, code: 'RATE_LIMITED', retryable: false },
    'Email delivery failed': { status: 500, code: 'EMAIL_FAILED', retryable: true }
  };
  
  const errorInfo = errorMap[error.message] || { 
    status: 500, 
    code: 'UNKNOWN_ERROR', 
    retryable: false 
  };
  
  return new Response(JSON.stringify({
    success: false,
    error: {
      code: errorInfo.code,
      message: error.message,
      retryable: errorInfo.retryable,
      context
    }
  }), {
    status: errorInfo.status,
    headers: { 'Content-Type': 'application/json' }
  });
};

Email Templates

javascript
const generateOTPEmail = (fullName, otp) => ({
  subject: 'Your verification code',
  html: `
    <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
      <h2>Welcome ${fullName}!</h2>
      <p>Your verification code is:</p>
      <div style="background: #f5f5f5; padding: 20px; text-align: center; margin: 20px 0;">
        <span style="font-size: 24px; font-weight: bold; letter-spacing: 3px;">${otp}</span>
      </div>
      <p>This code will expire in 5 minutes.</p>
      <p>If you didn't request this code, please ignore this email.</p>
    </div>
  `,
  text: `Welcome ${fullName}! Your verification code is: ${otp}. This code will expire in 5 minutes.`
});

🏆 Industry Comparison

Traditional Multi-Flow Approach

javascript
// Traditional: Complex flow management
class TraditionalAuth {
  async checkUserExists(email) { /* API call */ }
  async register(email, password, fullName) { /* API call */ }
  async login(email, password) { /* API call */ }
  async forgotPassword(email) { /* API call */ }
  async resetPassword(token, newPassword) { /* API call */ }
  async verifyEmail(token) { /* API call */ }
}

// Client must handle multiple flows
if (await auth.checkUserExists(email)) {
  await auth.login(email, password);
} else {
  await auth.register(email, password, fullName);
  await auth.verifyEmail(verificationToken);
}

Our Unified Approach

javascript
// Unified: Single flow for all scenarios
class UnifiedAuth {
  async requestOTP(email, fullName) { /* Single entry point */ }
  async verifyOTP(sessionId, otp) { /* Single verification */ }
}

// Client uses same flow always
await auth.requestOTP(email, fullName);
await auth.verifyOTP(sessionId, otp); // Server handles user state

🎉 Conclusion

The Single Unified Authentication Flow represents a paradigm shift from traditional authentication patterns. By implementing this approach, we've achieved:

Technical Excellence

  • Reduced complexity: Single flow eliminates branching logic
  • Enhanced security: Email verification required for all access
  • Improved scalability: Simplified API surface area
  • Better maintainability: Less code to test and debug

User Experience Innovation

  • Friction elimination: No account existence confusion
  • Seamless onboarding: Automatic user provisioning
  • Universal accessibility: Same experience for all users
  • Mobile-friendly: OTP-based auth works excellently on mobile

Business Value

  • Higher conversion rates: Reduced authentication abandonment
  • Lower support costs: Fewer password-related issues
  • Faster development: Simplified client implementation
  • Enhanced analytics: Unified tracking and monitoring

This implementation demonstrates how thoughtful architectural decisions can simultaneously improve security, user experience, and development efficiency - a true win-win-win scenario.


Document Version: 1.0
Last Updated: August 2025
Prepared for: StratiqX Platform Documentation

Strategic Intelligence Hub Documentation