8.7 KiB
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:
- Server-Side Token Consumption Bug: Tokens were consumed immediately on verification, even when Keycloak updates failed
- Client-Side Navigation Failures: Mobile browsers failing to navigate away from the verification page
- Component Lifecycle Issues: No circuit breaker to prevent repeated API calls
- 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:
- Email verification succeeds, token gets consumed
- Keycloak update fails (configuration issue)
- API returns error, but token is already consumed
- Mobile browser retries same URL
- 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:
- Circuit Breaker Blocked: Shows when max attempts exceeded
- Loading with Attempt Counter: Shows current attempt number
- Smart Retry Logic: Only shows retry if attempts remain
- 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 overhaulserver/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.