🔧 Troubleshooting Tuesday: Authentication & Authorization Debugging - Los 5 Errores Más Comunes

Hoy analizamos los errores más frustrantes en sistemas de autenticación y autorización que pueden comprometer la seguridad y crear experiencias de usuario terribles.

:ticket: Problema #1: JWT Token Management Hell - Expiración y Refresh

:cross_mark: Síntomas:

  • Usuarios deslogueados inesperadamente
  • “401 Unauthorized” intermitentes
  • Loops infinitos de refresh tokens
  • Sessions que persisten después de logout

:magnifying_glass_tilted_left: Código Problemático:

// ❌ Token management básico sin estrategia
localStorage.setItem('token', response.data.token);

// En cada request
const token = localStorage.getItem('token');
fetch('/api/data', {
    headers: { 'Authorization': `Bearer ${token}` }
});

:white_check_mark: Token Manager Robusto:

class AuthTokenManager {
    constructor() {
        this.accessToken = null;
        this.refreshToken = null;
        this.tokenExpiry = null;
        this.refreshPromise = null;
        this.subscribers = new Set();
    }
    
    setTokens(accessToken, refreshToken, expiresIn) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        // Buffer de 60 segundos antes de expiración
        this.tokenExpiry = Date.now() + (expiresIn * 1000) - 60000;
        
        // Guardar en storage seguro
        this.saveToStorage();
        this.notifySubscribers('TOKEN_UPDATED');
    }
    
    async getValidToken() {
        // Token válido y no cercano a expirar
        if (this.accessToken && Date.now() < this.tokenExpiry) {
            return this.accessToken;
        }
        
        // Si hay refresh en progreso, esperar
        if (this.refreshPromise) {
            try {
                return await this.refreshPromise;
            } catch (error) {
                this.refreshPromise = null;
                throw error;
            }
        }
        
        // Intentar refresh
        if (this.refreshToken) {
            this.refreshPromise = this.performRefresh();
            
            try {
                const newToken = await this.refreshPromise;
                return newToken;
            } finally {
                this.refreshPromise = null;
            }
        }
        
        // No hay tokens válidos
        this.clearTokens();
        throw new Error('NO_VALID_TOKEN');
    }
    
    async performRefresh() {
        console.log('🔄 Refreshing authentication token...');
        
        try {
            const response = await fetch('/api/auth/refresh', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ 
                    refresh_token: this.refreshToken 
                })
            });
            
            if (!response.ok) {
                throw new Error(`Refresh failed: ${response.status}`);
            }
            
            const tokenData = await response.json();
            
            this.setTokens(
                tokenData.access_token,
                tokenData.refresh_token || this.refreshToken,
                tokenData.expires_in
            );
            
            console.log('✅ Token refreshed successfully');
            return this.accessToken;
            
        } catch (error) {
            console.error('❌ Token refresh failed:', error.message);
            this.clearTokens();
            this.notifySubscribers('REFRESH_FAILED');
            throw error;
        }
    }
    
    clearTokens() {
        this.accessToken = null;
        this.refreshToken = null;
        this.tokenExpiry = null;
        this.clearStorage();
        this.notifySubscribers('TOKENS_CLEARED');
    }
    
    // Suscripción para componentes React
    subscribe(callback) {
        this.subscribers.add(callback);
        return () => this.subscribers.delete(callback);
    }
    
    notifySubscribers(event) {
        this.subscribers.forEach(callback => callback(event));
    }
    
    saveToStorage() {
        // Usar sessionStorage para mayor seguridad
        sessionStorage.setItem('auth_tokens', JSON.stringify({
            accessToken: this.accessToken,
            refreshToken: this.refreshToken,
            tokenExpiry: this.tokenExpiry
        }));
    }
    
    loadFromStorage() {
        const stored = sessionStorage.getItem('auth_tokens');
        if (stored) {
            const { accessToken, refreshToken, tokenExpiry } = JSON.parse(stored);
            
            // Verificar si los tokens siguen siendo válidos
            if (tokenExpiry > Date.now()) {
                this.accessToken = accessToken;
                this.refreshToken = refreshToken;
                this.tokenExpiry = tokenExpiry;
                return true;
            }
        }
        return false;
    }
    
    clearStorage() {
        sessionStorage.removeItem('auth_tokens');
        localStorage.removeItem('auth_tokens'); // Cleanup legacy
    }
}

// Uso global
const authManager = new AuthTokenManager();

// HTTP interceptor
async function authenticatedFetch(url, options = {}) {
    try {
        const token = await authManager.getValidToken();
        
        return fetch(url, {
            ...options,
            headers: {
                ...options.headers,
                'Authorization': `Bearer ${token}`
            }
        });
        
    } catch (error) {
        if (error.message === 'NO_VALID_TOKEN') {
            // Redirect a login
            window.location.href = '/login';
            return;
        }
        throw error;
    }
}

:door: Problema #2: CORS en Authentication - Preflight y Cookies

:cross_mark: Problemas Típicos:

  • Cookies que no se envían en requests cross-origin
  • CORS preflight failures con custom headers
  • Authentication headers bloqueados

:magnifying_glass_tilted_left: Debugging CORS Issues:

// ❌ Configuración que no funciona cross-origin
fetch('https://api.myapp.com/login', {
    method: 'POST',
    credentials: 'include', // Cookies incluidas
    headers: {
        'Content-Type': 'application/json',
        'X-Custom-Auth': 'value' // Causa preflight
    },
    body: JSON.stringify({ username, password })
});

:white_check_mark: Solución CORS Completa:

Backend (Express.js):

// Configuración CORS específica para auth
const corsConfig = {
    origin: (origin, callback) => {
        const allowedOrigins = [
            'https://myapp.com',
            'https://www.myapp.com',
            'http://localhost:3000' // Development
        ];
        
        // Permitir requests sin origin (mobile apps, Postman)
        if (!origin) return callback(null, true);
        
        if (allowedOrigins.includes(origin)) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    credentials: true, // Permitir cookies
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    allowedHeaders: [
        'Content-Type',
        'Authorization',
        'X-Requested-With',
        'X-CSRF-Token'
    ],
    exposedHeaders: ['X-CSRF-Token'] // Headers que el cliente puede leer
};

app.use(cors(corsConfig));

// Middleware para debugging CORS
app.use((req, res, next) => {
    console.log('🌍 CORS Debug:', {
        origin: req.headers.origin,
        method: req.method,
        credentials: req.headers.cookie ? 'present' : 'none',
        customHeaders: Object.keys(req.headers).filter(h => h.startsWith('x-'))
    });
    next();
});

Frontend (Strategy Pattern):

class AuthStrategy {
    static async tokenBased(credentials) {
        // Para SPAs - sin cookies
        const response = await fetch('/api/auth/login', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(credentials)
        });
        
        if (response.ok) {
            const { access_token, refresh_token, expires_in } = await response.json();
            authManager.setTokens(access_token, refresh_token, expires_in);
        }
        
        return response;
    }
    
    static async cookieBased(credentials) {
        // Para aplicaciones tradicionales
        const response = await fetch('/api/auth/login', {
            method: 'POST',
            credentials: 'include', // Enviar/recibir cookies
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(credentials)
        });
        
        return response;
    }
    
    static async hybridBased(credentials) {
        // Combinación: CSRF token + JWT
        const csrfResponse = await fetch('/api/csrf-token', {
            credentials: 'include'
        });
        
        const csrfToken = csrfResponse.headers.get('X-CSRF-Token');
        
        return fetch('/api/auth/login', {
            method: 'POST',
            credentials: 'include',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': csrfToken
            },
            body: JSON.stringify(credentials)
        });
    }
}

:locked_with_key: Problema #3: Role-Based Access Control (RBAC) Inconsistente

:cross_mark: Síntomas:

  • Usuarios viendo contenido no autorizado
  • Checks de permisos inconsistentes entre frontend/backend
  • UI que cambia inesperadamente basada en roles

:magnifying_glass_tilted_left: Debugging Authorization:

// ❌ Authorization checks scattered y inconsistentes
function canEditPost(user, post) {
    return user.role === 'admin' || user.id === post.authorId;
}

function canDeletePost(user, post) {
    return user.role === 'admin'; // ¿Y el owner?
}

// En componente
{user.role === 'admin' && <DeleteButton />} // Inconsistente

:white_check_mark: Sistema RBAC Robusto:

// Definición centralizada de permisos
const PERMISSIONS = {
    POST_READ: 'post:read',
    POST_CREATE: 'post:create',
    POST_UPDATE: 'post:update',
    POST_DELETE: 'post:delete',
    USER_MANAGE: 'user:manage',
    ADMIN_PANEL: 'admin:access'
};

const ROLES = {
    GUEST: {
        permissions: [PERMISSIONS.POST_READ]
    },
    USER: {
        permissions: [
            PERMISSIONS.POST_READ,
            PERMISSIONS.POST_CREATE,
            PERMISSIONS.POST_UPDATE // Solo sus propios posts
        ]
    },
    MODERATOR: {
        inherits: ['USER'],
        permissions: [
            PERMISSIONS.POST_DELETE // Posts de cualquier usuario
        ]
    },
    ADMIN: {
        inherits: ['MODERATOR'],
        permissions: [
            PERMISSIONS.USER_MANAGE,
            PERMISSIONS.ADMIN_PANEL
        ]
    }
};

class AuthorizationManager {
    constructor() {
        this.currentUser = null;
        this.resolvedPermissions = new Map();
    }
    
    setUser(user) {
        this.currentUser = user;
        this.resolvedPermissions.clear();
        
        if (user) {
            this.resolvePermissions(user.roles || [user.role]);
        }
    }
    
    resolvePermissions(userRoles) {
        const allPermissions = new Set();
        
        for (const roleName of userRoles) {
            const role = ROLES[roleName.toUpperCase()];
            if (!role) continue;
            
            // Agregar permisos directos
            role.permissions?.forEach(p => allPermissions.add(p));
            
            // Resolver herencia
            if (role.inherits) {
                this.resolvePermissions(role.inherits)
                    .forEach(p => allPermissions.add(p));
            }
        }
        
        return allPermissions;
    }
    
    can(permission, resource = null) {
        if (!this.currentUser) return false;
        
        // Cache key para performance
        const cacheKey = `${permission}:${resource?.id || 'global'}`;
        
        if (this.resolvedPermissions.has(cacheKey)) {
            return this.resolvedPermissions.get(cacheKey);
        }
        
        let result = false;
        
        // Check permission global
        if (this.hasPermission(permission)) {
            result = true;
            
            // Check ownership si es necesario
            if (resource && this.requiresOwnership(permission)) {
                result = this.isOwner(resource);
            }
        }
        
        this.resolvedPermissions.set(cacheKey, result);
        return result;
    }
    
    hasPermission(permission) {
        const userPermissions = this.resolvePermissions(
            this.currentUser.roles || [this.currentUser.role]
        );
        return userPermissions.has(permission);
    }
    
    requiresOwnership(permission) {
        return [
            PERMISSIONS.POST_UPDATE,
            // Otros permisos que requieren ownership
        ].includes(permission);
    }
    
    isOwner(resource) {
        return resource.authorId === this.currentUser.id ||
               resource.userId === this.currentUser.id ||
               resource.ownerId === this.currentUser.id;
    }
    
    // Para debugging
    getUserPermissions() {
        return Array.from(this.resolvePermissions(
            this.currentUser?.roles || [this.currentUser?.role] || []
        ));
    }
}

// Uso en componentes
const authz = new AuthorizationManager();

function PostComponent({ post }) {
    const canEdit = authz.can(PERMISSIONS.POST_UPDATE, post);
    const canDelete = authz.can(PERMISSIONS.POST_DELETE, post);
    
    return (
        <div>
            <PostContent post={post} />
            {canEdit && <EditButton post={post} />}
            {canDelete && <DeleteButton post={post} />}
        </div>
    );
}

// Hook para React
function usePermissions() {
    const [permissions, setPermissions] = useState([]);
    
    useEffect(() => {
        const updatePermissions = () => {
            setPermissions(authz.getUserPermissions());
        };
        
        updatePermissions();
        authManager.subscribe(updatePermissions);
        
        return () => authManager.unsubscribe(updatePermissions);
    }, []);
    
    return {
        can: (permission, resource) => authz.can(permission, resource),
        permissions
    };
}

:locked: Problema #4: Session Management - Concurrency y Security

:cross_mark: Problemas Comunes:

  • Multiple sessions activas sin control
  • Sessions que no expiran correctamente
  • Concurrent logins causando inconsistencias

:white_check_mark: Session Manager Seguro:

class SessionManager {
    constructor() {
        this.sessions = new Map();
        this.maxSessions = 3; // Por usuario
        this.sessionTimeout = 30 * 60 * 1000; // 30 minutos
        this.cleanupInterval = null;
        
        this.startCleanup();
    }
    
    async createSession(userId, deviceInfo = {}) {
        // Limpiar sessions expiradas del usuario
        await this.cleanupUserSessions(userId);
        
        // Verificar límite de sessions
        const userSessions = this.getUserSessions(userId);
        if (userSessions.length >= this.maxSessions) {
            // Eliminar la sesión más antigua
            const oldestSession = userSessions
                .sort((a, b) => a.createdAt - b.createdAt)[0];
            await this.revokeSession(oldestSession.sessionId);
        }
        
        const sessionId = this.generateSecureId();
        const session = {
            sessionId,
            userId,
            createdAt: Date.now(),
            lastActivity: Date.now(),
            deviceInfo: {
                userAgent: deviceInfo.userAgent || '',
                ip: deviceInfo.ip || '',
                deviceType: this.detectDeviceType(deviceInfo.userAgent)
            },
            isActive: true
        };
        
        this.sessions.set(sessionId, session);
        
        console.log(`✅ Session created for user ${userId}:`, {
            sessionId: sessionId.substring(0, 8) + '...',
            activeSessions: this.getUserSessions(userId).length
        });
        
        return sessionId;
    }
    
    async validateSession(sessionId) {
        const session = this.sessions.get(sessionId);
        
        if (!session || !session.isActive) {
            return { valid: false, reason: 'SESSION_NOT_FOUND' };
        }
        
        // Check timeout
        const now = Date.now();
        if (now - session.lastActivity > this.sessionTimeout) {
            await this.revokeSession(sessionId);
            return { valid: false, reason: 'SESSION_EXPIRED' };
        }
        
        // Update activity
        session.lastActivity = now;
        
        return { 
            valid: true, 
            session: {
                userId: session.userId,
                sessionId: session.sessionId,
                deviceInfo: session.deviceInfo
            }
        };
    }
    
    async revokeSession(sessionId) {
        const session = this.sessions.get(sessionId);
        if (session) {
            session.isActive = false;
            this.sessions.delete(sessionId);
            
            console.log(`🔒 Session revoked:`, {
                sessionId: sessionId.substring(0, 8) + '...',
                userId: session.userId
            });
        }
    }
    
    async revokeAllUserSessions(userId, exceptSessionId = null) {
        const userSessions = this.getUserSessions(userId);
        
        for (const session of userSessions) {
            if (session.sessionId !== exceptSessionId) {
                await this.revokeSession(session.sessionId);
            }
        }
        
        console.log(`🔒 All sessions revoked for user ${userId} except current`);
    }
    
    getUserSessions(userId) {
        return Array.from(this.sessions.values())
            .filter(s => s.userId === userId && s.isActive);
    }
    
    getActiveSessions(userId) {
        return this.getUserSessions(userId).map(session => ({
            sessionId: session.sessionId.substring(0, 8) + '...',
            deviceType: session.deviceInfo.deviceType,
            lastActivity: new Date(session.lastActivity).toISOString(),
            isCurrent: false // Se marca desde el cliente
        }));
    }
    
    startCleanup() {
        this.cleanupInterval = setInterval(() => {
            this.cleanupExpiredSessions();
        }, 5 * 60 * 1000); // Cada 5 minutos
    }
    
    cleanupExpiredSessions() {
        const now = Date.now();
        let cleaned = 0;
        
        for (const [sessionId, session] of this.sessions.entries()) {
            if (now - session.lastActivity > this.sessionTimeout) {
                this.sessions.delete(sessionId);
                cleaned++;
            }
        }
        
        if (cleaned > 0) {
            console.log(`🧹 Cleaned ${cleaned} expired sessions`);
        }
    }
    
    async cleanupUserSessions(userId) {
        const userSessions = this.getUserSessions(userId);
        
        for (const session of userSessions) {
            if (Date.now() - session.lastActivity > this.sessionTimeout) {
                await this.revokeSession(session.sessionId);
            }
        }
    }
    
    generateSecureId() {
        return Array.from(crypto.getRandomValues(new Uint8Array(32)))
            .map(b => b.toString(16).padStart(2, '0'))
            .join('');
    }
    
    detectDeviceType(userAgent) {
        if (!userAgent) return 'unknown';
        
        if (/Mobile|Android|iPhone|iPad/.test(userAgent)) {
            return 'mobile';
        } else if (/Tablet/.test(userAgent)) {
            return 'tablet';
        } else {
            return 'desktop';
        }
    }
    
    // Para debugging y monitoring
    getStats() {
        const activeSessions = this.sessions.size;
        const userCounts = {};
        
        for (const session of this.sessions.values()) {
            userCounts[session.userId] = (userCounts[session.userId] || 0) + 1;
        }
        
        return {
            totalActiveSessions: activeSessions,
            uniqueUsers: Object.keys(userCounts).length,
            averageSessionsPerUser: activeSessions / Object.keys(userCounts).length || 0,
            topUsers: Object.entries(userCounts)
                .sort(([,a], [,b]) => b - a)
                .slice(0, 5)
        };
    }
}

const sessionManager = new SessionManager();

// Middleware de autenticación con session management
async function authenticateRequest(req, res, next) {
    const sessionId = req.headers['x-session-id'] || 
                      req.cookies['session-id'];
    
    if (!sessionId) {
        return res.status(401).json({ error: 'No session provided' });
    }
    
    const validation = await sessionManager.validateSession(sessionId);
    
    if (!validation.valid) {
        return res.status(401).json({ 
            error: 'Invalid session',
            reason: validation.reason 
        });
    }
    
    req.user = validation.session;
    next();
}

:police_car_light: Problema #5: Password Reset & Account Recovery - Security vs UX

:cross_mark: Vulnerabilidades Comunes:

  • Tokens de reset predecibles o que no expiran
  • Información leakage en mensajes de error
  • Race conditions en proceso de reset

:white_check_mark: Account Recovery Seguro:

class AccountRecoveryManager {
    constructor() {
        this.resetTokens = new Map();
        this.rateLimiter = new Map(); // IP -> attempts
        this.tokenExpiry = 15 * 60 * 1000; // 15 minutos
        this.maxAttempts = 5;
        this.timeWindow = 60 * 60 * 1000; // 1 hora
    }
    
    async initiatePasswordReset(email, clientIP) {
        // Rate limiting por IP
        const attempts = await this.checkRateLimit(clientIP);
        if (attempts.exceeded) {
            throw new Error('Too many reset attempts. Try again later.');
        }
        
        // Verificar si el email existe (sin revelar información)
        const user = await this.findUserByEmail(email);
        
        // Generar token seguro siempre (timing attack prevention)
        const resetToken = this.generateSecureToken();
        const hashedToken = await this.hashToken(resetToken);
        
        if (user) {
            // Invalidar tokens anteriores del usuario
            await this.invalidateUserTokens(user.id);
            
            // Guardar nuevo token
            this.resetTokens.set(hashedToken, {
                userId: user.id,
                email: user.email,
                createdAt: Date.now(),
                attempts: 0,
                used: false
            });
            
            // Enviar email (async, no esperar)
            this.sendResetEmail(user.email, resetToken)
                .catch(error => {
                    console.error('Failed to send reset email:', error);
                    // No fallar la operación por esto
                });
            
            console.log(`🔑 Password reset initiated for user: ${user.id}`);
        } else {
            console.log(`⚠️ Password reset attempted for non-existent email: ${email}`);
        }
        
        // Siempre responder igual (no revelar si el email existe)
        return {
            message: 'If the email exists, a reset link will be sent.',
            // No incluir información sobre si el email fue encontrado
        };
    }
    
    async validateResetToken(token) {
        const hashedToken = await this.hashToken(token);
        const tokenData = this.resetTokens.get(hashedToken);
        
        if (!tokenData) {
            return { valid: false, reason: 'INVALID_TOKEN' };
        }
        
        // Check expiration
        if (Date.now() - tokenData.createdAt > this.tokenExpiry) {
            this.resetTokens.delete(hashedToken);
            return { valid: false, reason: 'EXPIRED_TOKEN' };
        }
        
        // Check if already used
        if (tokenData.used) {
            return { valid: false, reason: 'TOKEN_ALREADY_USED' };
        }
        
        // Rate limiting por token
        tokenData.attempts++;
        if (tokenData.attempts > 3) {
            this.resetTokens.delete(hashedToken);
            return { valid: false, reason: 'TOO_MANY_ATTEMPTS' };
        }
        
        return {
            valid: true,
            userId: tokenData.userId,
            email: tokenData.email
        };
    }
    
    async resetPassword(token, newPassword) {
        const validation = await this.validateResetToken(token);
        
        if (!validation.valid) {
            throw new Error(`Invalid reset token: ${validation.reason}`);
        }
        
        // Validar password strength
        const passwordValidation = this.validatePasswordStrength(newPassword);
        if (!passwordValidation.valid) {
            throw new Error(`Password does not meet requirements: ${passwordValidation.message}`);
        }
        
        const hashedToken = await this.hashToken(token);
        const tokenData = this.resetTokens.get(hashedToken);
        
        try {
            // Hash del nuevo password
            const hashedPassword = await this.hashPassword(newPassword);
            
            // Update en database
            await this.updateUserPassword(validation.userId, hashedPassword);
            
            // Marcar token como usado
            tokenData.used = true;
            
            // Invalidar todas las sessions del usuario
            await sessionManager.revokeAllUserSessions(validation.userId);
            
            // Log de seguridad
            console.log(`🔒 Password reset completed for user: ${validation.userId}`);
            
            // Notificar al usuario por email
            this.sendPasswordChangedNotification(validation.email)
                .catch(error => console.error('Failed to send notification:', error));
            
            return { success: true };
            
        } catch (error) {
            console.error('Password reset failed:', error);
            throw new Error('Failed to reset password');
        } finally {
            // Limpiar token después de uso/intento
            setTimeout(() => {
                this.resetTokens.delete(hashedToken);
            }, 5000);
        }
    }
    
    async checkRateLimit(clientIP) {
        const now = Date.now();
        const attempts = this.rateLimiter.get(clientIP) || { count: 0, firstAttempt: now };
        
        // Reset window si ha pasado el tiempo
        if (now - attempts.firstAttempt > this.timeWindow) {
            attempts.count = 0;
            attempts.firstAttempt = now;
        }
        
        attempts.count++;
        this.rateLimiter.set(clientIP, attempts);
        
        return {
            exceeded: attempts.count > this.maxAttempts,
            remaining: Math.max(0, this.maxAttempts - attempts.count),
            resetTime: attempts.firstAttempt + this.timeWindow
        };
    }
    
    validatePasswordStrength(password) {
        const requirements = {
            minLength: password.length >= 8,
            hasUpper: /[A-Z]/.test(password),
            hasLower: /[a-z]/.test(password),
            hasNumber: /\d/.test(password),
            hasSpecial: /[!@#$%^&*(),.?\":{}|<>]/.test(password),
            notCommon: !this.isCommonPassword(password)
        };
        
        const failed = Object.entries(requirements)
            .filter(([_, passed]) => !passed)
            .map(([req, _]) => req);
        
        return {
            valid: failed.length === 0,
            requirements,
            message: failed.length > 0 ? `Missing: ${failed.join(', ')}` : 'Password meets all requirements'
        };
    }
    
    async invalidateUserTokens(userId) {
        // Buscar y eliminar tokens existentes del usuario
        for (const [hashedToken, tokenData] of this.resetTokens.entries()) {
            if (tokenData.userId === userId) {
                this.resetTokens.delete(hashedToken);
            }
        }
    }
    
    generateSecureToken() {
        return Array.from(crypto.getRandomValues(new Uint8Array(32)))
            .map(b => b.toString(16).padStart(2, '0'))
            .join('');
    }
    
    async hashToken(token) {
        const encoder = new TextEncoder();
        const data = encoder.encode(token);
        const hashBuffer = await crypto.subtle.digest('SHA-256', data);
        const hashArray = Array.from(new Uint8Array(hashBuffer));
        return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    }
    
    isCommonPassword(password) {
        const commonPasswords = [
            'password', '123456', 'password123', 'admin', 'qwerty',
            'letmein', 'welcome', 'monkey', '1234567890'
        ];
        return commonPasswords.includes(password.toLowerCase());
    }
    
    // Cleanup periódico
    startCleanup() {
        setInterval(() => {
            const now = Date.now();
            let cleaned = 0;
            
            for (const [token, data] of this.resetTokens.entries()) {
                if (now - data.createdAt > this.tokenExpiry) {
                    this.resetTokens.delete(token);
                    cleaned++;
                }
            }
            
            if (cleaned > 0) {
                console.log(`🧹 Cleaned ${cleaned} expired reset tokens`);
            }
        }, 5 * 60 * 1000); // Cada 5 minutos
    }
}

:hammer_and_wrench: Herramientas de Debugging Authentication

Chrome DevTools Network Tab:

// Snippet para debuggear auth headers
function debugAuthHeaders() {
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
        const [url, options = {}] = args;
        
        console.group(`🔐 Auth Request: ${url}`);
        console.log('Headers:', options.headers);
        console.log('Credentials:', options.credentials);
        console.log('Method:', options.method || 'GET');
        
        return originalFetch.apply(this, args).then(response => {
            console.log('Response Status:', response.status);
            console.log('Response Headers:', Object.fromEntries(response.headers.entries()));
            console.groupEnd();
            return response;
        });
    };
}

// Ejecutar en console
debugAuthHeaders();

JWT Decoder para Debugging:

function decodeJWT(token) {
    try {
        const [header, payload, signature] = token.split('.');
        
        return {
            header: JSON.parse(atob(header)),
            payload: JSON.parse(atob(payload)),
            signature: signature,
            isExpired: JSON.parse(atob(payload)).exp * 1000 < Date.now(),
            timeToExpiry: (JSON.parse(atob(payload)).exp * 1000 - Date.now()) / 1000
        };
    } catch (error) {
        return { error: 'Invalid JWT token' };
    }
}

// Uso
console.log(decodeJWT(localStorage.getItem('token')));

:bar_chart: Checklist de Auth Debugging

:white_check_mark: Token Management:

  • ¿Tokens se guardan en storage seguro?
  • ¿Refresh automático antes de expiración?
  • ¿Manejo de concurrent refresh requests?
  • ¿Cleanup en logout/error?

:white_check_mark: CORS & Headers:

  • ¿Origin permitido en backend?
  • ¿Credentials configurados correctamente?
  • ¿Custom headers declarados en CORS?
  • ¿Preflight requests funcionando?

:white_check_mark: Authorization:

  • ¿Permisos centralizados y consistentes?
  • ¿Checks en frontend Y backend?
  • ¿Ownership validation correcta?
  • ¿Roles inheritance funcionando?

:white_check_mark: Session Security:

  • ¿Límite de sessions concurrentes?
  • ¿Timeout y cleanup automático?
  • ¿Rate limiting en endpoints críticos?
  • ¿Logging de eventos de seguridad?

:light_bulb: Pro Tips de Auth Debugging

  1. Usa herramientas especializadas: Postman, Insomnia para testing de APIs
  2. Logging estructurado: Incluye context IDs para trace de requests
  3. Testing de edge cases: Tokens expirados, sessions concurrentes, etc.
  4. Monitoring en producción: Alertas para patrones anómalos de auth

Los problemas de autenticación pueden ser los más críticos porque afectan tanto seguridad como UX. La clave está en testing exhaustivo y monitoring proactivo.