monacousa-portal/docs-archive/EMAIL_VERIFICATION_RELOAD_L...

8.7 KiB

Email Verification Reload Loop - Complete Fix Implementation

Problem Analysis

The email verification page was experiencing endless reload loops on mobile browsers (both Chrome and Safari iOS), caused by:

  1. Server-Side Token Consumption Bug: Tokens were consumed immediately on verification, even when Keycloak updates failed
  2. Client-Side Navigation Failures: Mobile browsers failing to navigate away from the verification page
  3. Component Lifecycle Issues: No circuit breaker to prevent repeated API calls
  4. Mobile Browser Quirks: Different timeout and retry behaviors on mobile

Root Cause (From System Logs)

[verify-email] Keycloak update failed: Failed to update user profile: 400 - {"field":"email","errorMessage":"error-user-attribute-required","params":["email"]}
[email-tokens] Token verification failed: Token not found or already used

The flow was:

  1. Email verification succeeds, token gets consumed
  2. Keycloak update fails (configuration issue)
  3. API returns error, but token is already consumed
  4. Mobile browser retries same URL
  5. Token now shows "already used" → endless loop

Complete Solution Implementation

Phase 1: Server-Side Token Management Fix

A. Enhanced Token Utilities (server/utils/email-tokens.ts)

Before: Tokens were consumed immediately during verification After: Separated verification from consumption

// NEW: Verify without consuming
export async function verifyEmailToken(token: string): Promise<{ userId: string; email: string }> {
  // Verify JWT and validate, but DON'T delete token yet
  return { userId: decoded.userId, email: decoded.email };
}

// NEW: Consume token only after successful operations
export async function consumeEmailToken(token: string): Promise<void> {
  activeTokens.delete(token);
}

B. Smart API Endpoint (server/api/auth/verify-email.get.ts)

Key improvements:

  • Only consumes tokens after successful Keycloak updates
  • Intelligent error classification (retryable vs permanent)
  • Enhanced response data with partial success indicators
try {
  // Verify token WITHOUT consuming
  const { userId, email } = await verifyEmailToken(token);

  // Attempt Keycloak update
  await keycloak.updateUserProfile(userId, { emailVerified: true });
  
  // ONLY consume on success
  await consumeEmailToken(token);

} catch (keycloakError) {
  if (keycloakError.message?.includes('error-user-attribute-required')) {
    // Configuration issue - don't consume token, allow retries
    partialSuccess = true;
  } else {
    // Other errors - consume to prevent infinite loops
    await consumeEmailToken(token);
    partialSuccess = true;
  }
}

Phase 2: Client-Side Circuit Breaker System

A. Verification State Management (utils/verification-state.ts)

Features:

  • Browser-persistent state: Uses sessionStorage with unique keys per token
  • Circuit breaker pattern: Max 3 attempts per 5-minute window
  • Progressive navigation: Multiple fallback methods for mobile compatibility
  • Mobile optimizations: Different delays for Safari iOS vs other browsers
export interface VerificationAttempt {
  token: string;
  attempts: number;
  lastAttempt: number;
  maxAttempts: number;
  status: 'pending' | 'success' | 'failed' | 'blocked';
  errors: string[];
}

// Progressive navigation with fallbacks
export async function navigateWithFallback(url: string): Promise<boolean> {
  try {
    // Method 1: Nuxt navigateTo
    await navigateTo(url, options);
  } catch {
    // Method 2: Vue Router
    await nuxtApp.$router.replace(url);
  } catch {
    // Method 3: Direct window.location (mobile fallback)
    window.location.replace(url);
  }
}

B. Mobile Browser Optimizations

Safari iOS specific:

  • 500ms navigation delay for stability
  • Static device detection to avoid reactive loops
  • Viewport meta optimization
  • Hardware acceleration management

General mobile:

  • 300ms navigation delay
  • Touch-friendly button sizing
  • Optimized scroll behavior

Phase 3: Enhanced Verification Page

A. Updated UI States (pages/auth/verify.vue)

New states:

  1. Circuit Breaker Blocked: Shows when max attempts exceeded
  2. Loading with Attempt Counter: Shows current attempt number
  3. Smart Retry Logic: Only shows retry if attempts remain
  4. Comprehensive Error Display: Different messages for different error types

B. Integration with Circuit Breaker

// Initialize verification state on mount
verificationState.value = initVerificationState(token, 3);

// Check if blocked before attempting
if (shouldBlockVerification(token)) {
  console.log('[auth/verify] Verification blocked by circuit breaker');
  return;
}

// Record attempts and update UI
verificationState.value = recordAttempt(token, success, error);
updateUIState();

Fix Benefits

🚫 Prevents Reload Loops

  • Server: Tokens preserved for retryable failures
  • Client: Circuit breaker prevents excessive API calls
  • Mobile: Progressive navigation with fallbacks

📱 Mobile Browser Compatibility

  • Safari iOS: Specific delay and navigation optimizations
  • Chrome Mobile: Standard mobile optimizations
  • Progressive Fallbacks: Multiple navigation methods

🔄 Smart Retry Logic

  • Automatic Retries: Up to 3 attempts per 5-minute window
  • Intelligent Blocking: Prevents spam while allowing legitimate retries
  • User Feedback: Clear status messages and attempt counters

🛡️ Error Resilience

  • Partial Success Handling: Works even with Keycloak configuration issues
  • Graceful Degradation: Always provides user feedback and alternatives
  • Self-Healing: Circuit breaker automatically resets after timeout

Testing Scenarios Covered

Server Configuration Issues

  • Keycloak misconfiguration: Shows partial success, preserves token
  • Database connectivity: Proper error handling with retry options
  • Network timeouts: Circuit breaker prevents endless attempts

Mobile Browser Edge Cases

  • Navigation failures: Multiple fallback methods
  • Component remounting: Persistent state prevents restart loops
  • Memory constraints: Automatic cleanup of expired states
  • Network switching: Handles connection changes gracefully

User Experience Scenarios

  • Expired links: Clear error messages with alternatives
  • Used links: Proper detection and user guidance
  • Multiple tabs: Each instance has independent circuit breaker
  • Back button: Replace navigation prevents loops

Implementation Files

Server Files Modified

  • server/utils/email-tokens.ts - Token management overhaul
  • server/api/auth/verify-email.get.ts - Smart verification endpoint

Client Files Created/Modified

  • utils/verification-state.ts - Circuit breaker and state management (NEW)
  • pages/auth/verify.vue - Enhanced verification page with circuit breaker

Dependencies

  • Existing static device detection (utils/static-device-detection.ts)
  • Existing mobile Safari optimizations (utils/mobile-safari-utils.ts)

Monitoring and Debugging

Server-Side Logging

[email-tokens] Token consumed successfully
[verify-email] Keycloak configuration error - token preserved for retry
[verify-email] Consuming token despite Keycloak error to prevent loops

Client-Side Logging

[verification-state] Maximum attempts (3) reached, blocking further attempts
[verification-state] Verification blocked for 8 more minutes
[verification-state] Using window.location fallback

Configuration

Circuit Breaker Settings

const MAX_ATTEMPTS_DEFAULT = 3;
const ATTEMPT_WINDOW = 5 * 60 * 1000; // 5 minutes
const CIRCUIT_BREAKER_TIMEOUT = 10 * 60 * 1000; // 10 minutes

Mobile Navigation Delays

// Safari iOS: 500ms delay
// Other mobile: 300ms delay  
// Desktop: 100ms delay

Deployment Notes

Immediate Benefits

  • Existing verification links will work better
  • No database migrations required
  • Backward compatible with existing tokens

Long-term Improvements

  • Reduced server load from repeated failed attempts
  • Better user experience with clear status messages
  • Automatic recovery from temporary configuration issues

Success Metrics

Before Fix

  • Endless reload loops on mobile browsers
  • Token consumption on partial failures
  • No retry mechanism for temporary issues
  • Poor mobile browser navigation compatibility

After Fix

  • Circuit breaker prevents reload loops
  • Smart token consumption based on actual success
  • Intelligent retry with user feedback
  • Progressive navigation with mobile fallbacks
  • Comprehensive error handling and user guidance

This fix addresses the root cause while providing comprehensive resilience for all edge cases and browser combinations.