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.
Problema #1: JWT Token Management Hell - Expiración y Refresh
Síntomas:
- Usuarios deslogueados inesperadamente
- “401 Unauthorized” intermitentes
- Loops infinitos de refresh tokens
- Sessions que persisten después de logout
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}` }
});
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;
}
}
Problema #2: CORS en Authentication - Preflight y Cookies
Problemas Típicos:
- Cookies que no se envían en requests cross-origin
- CORS preflight failures con custom headers
- Authentication headers bloqueados
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 })
});
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)
});
}
}
Problema #3: Role-Based Access Control (RBAC) Inconsistente
Síntomas:
- Usuarios viendo contenido no autorizado
- Checks de permisos inconsistentes entre frontend/backend
- UI que cambia inesperadamente basada en roles
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
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
};
}
Problema #4: Session Management - Concurrency y Security
Problemas Comunes:
- Multiple sessions activas sin control
- Sessions que no expiran correctamente
- Concurrent logins causando inconsistencias
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();
}
Problema #5: Password Reset & Account Recovery - Security vs UX
Vulnerabilidades Comunes:
- Tokens de reset predecibles o que no expiran
- Información leakage en mensajes de error
- Race conditions en proceso de reset
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
}
}
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')));
Checklist de Auth Debugging
Token Management:
- ¿Tokens se guardan en storage seguro?
- ¿Refresh automático antes de expiración?
- ¿Manejo de concurrent refresh requests?
- ¿Cleanup en logout/error?
CORS & Headers:
- ¿Origin permitido en backend?
- ¿Credentials configurados correctamente?
- ¿Custom headers declarados en CORS?
- ¿Preflight requests funcionando?
Authorization:
- ¿Permisos centralizados y consistentes?
- ¿Checks en frontend Y backend?
- ¿Ownership validation correcta?
- ¿Roles inheritance funcionando?
Session Security:
- ¿Límite de sessions concurrentes?
- ¿Timeout y cleanup automático?
- ¿Rate limiting en endpoints críticos?
- ¿Logging de eventos de seguridad?
Pro Tips de Auth Debugging
- Usa herramientas especializadas: Postman, Insomnia para testing de APIs
- Logging estructurado: Incluye context IDs para trace de requests
- Testing de edge cases: Tokens expirados, sessions concurrentes, etc.
- 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.
