Robust Authentication & Error Handling in Production Systems
Introduction
Building security-critical systems requires meticulous attention to authentication flows and error handling patterns. Recent production incidents have highlighted the importance of robust authentication mechanisms and graceful error recovery. This article explores key patterns for building trustworthy systems through proper authentication handling, role-based access control, and defensive programming practices.
Authentication State Management
Authentication in distributed systems requires careful state management to prevent unauthorized access while maintaining system usability.
OTP Authentication Implementation
interface AuthState {
step: 'initial' | 'otp-sent' | 'otp-verified' | 'authenticated';
userId?: string;
sessionToken?: string;
expiresAt?: Date;
}
class AuthenticationManager {
private state: AuthState = { step: 'initial' };
async initiateOTPLogin(email: string): Promise<void> {
try {
await this.sendOTP(email);
this.state = { step: 'otp-sent' };
} catch (error) {
this.handleAuthError(error);
throw new AuthenticationError('Failed to send OTP');
}
}
async verifyOTP(code: string): Promise<void> {
if (this.state.step !== 'otp-sent') {
throw new InvalidStateError('OTP verification not available');
}
const isValid = await this.validateOTP(code);
if (!isValid) {
throw new AuthenticationError('Invalid OTP code');
}
this.state = { step: 'otp-verified' };
}
}Graceful API Authentication Recovery
When dealing with authentication failures, systems should implement fallback mechanisms:
class APIClient {
async callWithFallback<T>(endpoint: string, data: any): Promise<T> {
try {
// Primary: Use Edge Function
return await this.callEdgeFunction(endpoint, data);
} catch (error) {
if (this.isAuthError(error)) {
// Fallback: Direct API call with fresh credentials
const freshToken = await this.refreshAuthentication();
return await this.callDirectAPI(endpoint, data, freshToken);
}
throw error;
}
}
private isAuthError(error: any): boolean {
return error.status === 401 || error.status === 403;
}
}Role-Based Access Control
Proper role management prevents privilege escalation and ensures users only access authorized resources.
Dynamic Role Level Checking
interface Role {
id: string;
name: string;
level: number; // Higher numbers = more privileges
permissions: Permission[];
}
class RoleManager {
async checkAccess(userId: string, requiredLevel: number): Promise<boolean> {
try {
const userRole = await this.getUserRole(userId);
return userRole.level >= requiredLevel;
} catch (error) {
// Fail closed - deny access on error
this.logger.error('Role check failed', { userId, error });
return false;
}
}
async saveRolePermissions(roleId: string, permissions: Permission[]): Promise<void> {
const transaction = await this.db.transaction();
try {
// Use maybeSingle() for queries that might return no results
const existingRole = await transaction
.from('roles')
.select('*')
.eq('id', roleId)
.maybeSingle();
if (!existingRole) {
throw new NotFoundError(`Role ${roleId} not found`);
}
await transaction
.from('role_permissions')
.delete()
.eq('role_id', roleId);
await transaction
.from('role_permissions')
.insert(permissions.map(p => ({ role_id: roleId, permission_id: p.id })));
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
}Database Query Safety
Proper query handling prevents runtime errors and ensures data consistency.
Safe Query Patterns
class DatabaseService {
// Use .maybeSingle() when record might not exist
async findUserRole(userId: string): Promise<Role | null> {
const result = await this.db
.from('user_roles')
.select('*')
.eq('user_id', userId)
.maybeSingle(); // Returns null if no record found
return result;
}
// Use .single() only when record MUST exist
async getUserById(userId: string): Promise<User> {
const result = await this.db
.from('users')
.select('*')
.eq('id', userId)
.single(); // Throws if no record found
if (!result) {
throw new NotFoundError(`User ${userId} not found`);
}
return result;
}
}Initialization Safety
Systems must handle startup conditions gracefully, especially in distributed environments.
Safe Database Initialization
class DatabaseInitializer {
private isInitialized = false;
private initializationPromise?: Promise<void>;
async ensureInitialized(): Promise<void> {
if (this.isInitialized) {
return;
}
if (this.initializationPromise) {
return this.initializationPromise;
}
this.initializationPromise = this.initialize();
return this.initializationPromise;
}
private async initialize(): Promise<void> {
try {
await this.runMigrations();
await this.seedRequiredData();
await this.validateSystemHealth();
this.isInitialized = true;
} catch (error) {
this.initializationPromise = undefined;
throw new InitializationError('Database initialization failed', error);
}
}
private async validateSystemHealth(): Promise<void> {
const healthCheck = await this.db.raw('SELECT 1 as health');
if (!healthCheck) {
throw new Error('Database health check failed');
}
}
}Payment Security Patterns
Critical operations like payments require additional safety measures.
State-Driven Payment Flow
interface PaymentState {
cardComplete: boolean;
processing: boolean;
validated: boolean;
}
class PaymentManager {
private state: PaymentState = {
cardComplete: false,
processing: false,
validated: false
};
updateCardInformation(cardData: Partial<CardInfo>): void {
const isComplete = this.validateCardCompleteness(cardData);
this.state.cardComplete = isComplete;
this.state.validated = isComplete && this.validateCardData(cardData);
}
canProcessPayment(): boolean {
return this.state.cardComplete &&
this.state.validated &&
!this.state.processing;
}
async processPayment(): Promise<void> {
if (!this.canProcessPayment()) {
throw new InvalidStateError('Payment cannot be processed');
}
this.state.processing = true;
try {
await this.chargeCard();
} finally {
this.state.processing = false;
}
}
}Error Classification and Recovery
Different error types require different handling strategies:
class ErrorHandler {
handleAPIError(error: APIError): void {
switch (error.status) {
case 401:
// Authentication failed - trigger re-auth
this.triggerReAuthentication();
break;
case 403:
// Authorization failed - log security event
this.logSecurityEvent('unauthorized_access_attempt', error);
break;
case 406:
// Not acceptable - likely query issue, use fallback
this.useFallbackQuery();
break;
default:
// Unknown error - fail safe
this.escalateError(error);
}
}
private logSecurityEvent(event: string, context: any): void {
// Audit logging for security events
this.auditLogger.log({
event,
timestamp: new Date().toISOString(),
context,
severity: 'high'
});
}
}Summary
Building trustworthy systems requires:
- Defensive Authentication: Multiple fallback mechanisms and proper state management
- Role-Based Security: Dynamic level checking and fail-closed access control
- Safe Database Operations: Using appropriate query methods and transaction safety
- Initialization Safety: Graceful startup handling and health validation
- Payment Security: State-driven flows and validation gates
- Error Recovery: Classified error handling with appropriate fallbacks
These patterns ensure systems remain secure and reliable even when facing unexpected conditions or failures.