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
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
POST /auth/request-otp
Content-Type: application/json
{
"email": "user@example.com",
"fullName": "John Doe"
}Implementation Logic
// 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
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
POST /auth/verify-otp
Content-Type: application/json
{
"sessionId": "uuid-session-id",
"otp": "123456"
}Smart User Handling Implementation
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
// 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
// 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
// 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 // Everyone2. Simplified Client Logic
// 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
// 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
// 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
// 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
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 --> DDatabase Schema
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
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
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
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
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
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
-- 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
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
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
// 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
// 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