--- id: "US-AUTH-011" title: "Recuperacion de Contrasena" type: "User Story" status: "To Do" priority: "Alta" epic: "OQI-001" story_points: 3 created_date: "2025-12-05" updated_date: "2026-01-04" --- # US-AUTH-011: Recuperación de Contraseña **Version:** 1.0.0 **Fecha:** 2025-12-05 **Estado:** Pendiente **Story Points:** 3 **Prioridad:** P0 (Crítica) **Épica:** [OQI-001](../_MAP.md) --- ## Historia de Usuario **Como** usuario de Trading Platform que olvidó su contraseña **Quiero** poder recuperar el acceso a mi cuenta **Para** poder volver a iniciar sesión sin necesidad de crear una nueva cuenta --- ## Criterios de Aceptación ### AC-001: Link de recuperación **Dado** que estoy en la página de login **Cuando** veo el formulario **Entonces** debería ver un link "¿Olvidaste tu contraseña?" **Y** debería estar claramente visible ### AC-002: Formulario de solicitud **Dado** que hago click en "¿Olvidaste tu contraseña?" **Cuando** accedo a la página de recuperación **Entonces** debería ver: - Título "Recupera tu contraseña" - Campo para ingresar email - Botón "Enviar instrucciones" - Link para volver a login ### AC-003: Validación de email **Dado** que estoy en el formulario de recuperación **Cuando** ingreso un email inválido **Entonces** debería ver mensaje "Email inválido" **Y** el botón debería estar deshabilitado ### AC-004: Email enviado (cuenta existe) **Dado** que ingresé un email registrado **Cuando** hago click en "Enviar instrucciones" **Entonces** debería: 1. Ver mensaje "Si el email está registrado, recibirás instrucciones" 2. Recibir un email con link de recuperación 3. El link debería expirar en 1 hora ### AC-005: Email no registrado **Dado** que ingresé un email no registrado **Cuando** hago click en "Enviar instrucciones" **Entonces** debería ver el mismo mensaje genérico **Y** NO debería revelar que el email no existe (seguridad) ### AC-006: Contenido del email **Dado** que solicité recuperación de contraseña **Cuando** reviso mi email **Entonces** debería recibir un email con: - Asunto: "Recupera tu contraseña de Trading Platform" - Saludo personalizado con mi nombre - Explicación clara del proceso - Botón/link de "Restablecer contraseña" - Nota de que expira en 1 hora - Nota de ignorar si no solicité el cambio ### AC-007: Click en link de recuperación **Dado** que recibí el email de recuperación **Cuando** hago click en el link **Entonces** debería ser redirigido a una página de "Nueva contraseña" **Y** el token debería validarse automáticamente ### AC-008: Formulario de nueva contraseña **Dado** que accedí con un token válido **Cuando** veo el formulario **Entonces** debería ver: - Campo "Nueva contraseña" - Campo "Confirmar nueva contraseña" - Indicadores de fortaleza de contraseña - Botón "Guardar nueva contraseña" ### AC-009: Validación de nueva contraseña **Dado** que estoy ingresando nueva contraseña **Cuando** ingreso una contraseña débil **Entonces** debería ver los mismos requisitos que en registro: - ✅/❌ Mínimo 8 caracteres - ✅/❌ Al menos una mayúscula - ✅/❌ Al menos una minúscula - ✅/❌ Al menos un número - ✅/❌ Al menos un carácter especial ### AC-010: Contraseñas no coinciden **Dado** que las contraseñas no coinciden **Cuando** intento guardar **Entonces** debería ver mensaje "Las contraseñas no coinciden" ### AC-011: Cambio exitoso **Dado** que ingresé una contraseña válida **Cuando** hago click en "Guardar nueva contraseña" **Entonces** debería: 1. Ver mensaje "Contraseña actualizada exitosamente" 2. Invalidar el token de recuperación 3. Cerrar todas las sesiones activas 4. Ser redirigido a login 5. Poder iniciar sesión con la nueva contraseña ### AC-012: Token expirado **Dado** que pasó más de 1 hora desde la solicitud **Cuando** intento usar el link de recuperación **Entonces** debería ver mensaje: - "Este link ha expirado. Solicita uno nuevo" **Y** debería ver botón "Solicitar nuevo link" ### AC-013: Token ya usado **Dado** que ya usé un token para cambiar contraseña **Cuando** intento usar el mismo link nuevamente **Entonces** debería ver mensaje: - "Este link ya fue usado" **Y** debería ver link a login ### AC-014: Token inválido **Dado** que el token fue manipulado o es inválido **Cuando** intento acceder **Entonces** debería ver mensaje: - "Link inválido. Solicita uno nuevo" ### AC-015: Rate limiting **Dado** que solicité 3 recuperaciones en 1 hora **Cuando** intento solicitar otra **Entonces** debería ver mensaje: - "Demasiadas solicitudes. Intenta en 1 hora" --- ## Mockup ``` Paso 1: Solicitud de recuperación ┌─────────────────────────────────────────────────────────────┐ │ │ │ 🔐 Recupera tu contraseña │ │ │ │ Ingresa tu email y te enviaremos instrucciones │ │ para recuperar tu contraseña. │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Email │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ usuario@example.com │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Enviar instrucciones │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ← Volver a inicio de sesión │ │ │ └─────────────────────────────────────────────────────────────┘ Paso 2: Confirmación ┌─────────────────────────────────────────────────────────────┐ │ │ │ ✉️ Revisa tu email │ │ │ │ Si el email está registrado, recibirás instrucciones │ │ para recuperar tu contraseña. │ │ │ │ Revisa también tu carpeta de spam. │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Volver a inicio de sesión │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ¿No recibiste el email? Intenta de nuevo en 5 minutos │ │ │ └─────────────────────────────────────────────────────────────┘ Email de recuperación: ┌─────────────────────────────────────────────────────────────┐ │ │ │ De: Trading Platform │ │ Para: usuario@example.com │ │ Asunto: Recupera tu contraseña de Trading Platform │ │ │ │ ───────────────────────────────────────────────────── │ │ │ │ Hola Juan, │ │ │ │ Recibimos una solicitud para recuperar tu contraseña. │ │ │ │ Haz click en el botón de abajo para crear una nueva: │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Restablecer mi contraseña │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ O copia este link en tu navegador: │ │ https://trading.com/reset-password?token=abc123... │ │ │ │ Este link expirará en 1 hora. │ │ │ │ Si no solicitaste este cambio, puedes ignorar este │ │ email. Tu contraseña no cambiará. │ │ │ │ Saludos, │ │ El equipo de Trading Platform │ │ │ └─────────────────────────────────────────────────────────────┘ Paso 3: Nueva contraseña ┌─────────────────────────────────────────────────────────────┐ │ │ │ 🔒 Crea una nueva contraseña │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Nueva contraseña │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ •••••••••••• 👁 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ✅ Mínimo 8 caracteres │ │ │ │ ✅ Una mayúscula │ │ │ │ ✅ Una minúscula │ │ │ │ ✅ Un número │ │ │ │ ❌ Un carácter especial │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Confirmar nueva contraseña │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ •••••••••••• 👁 │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Guardar nueva contraseña │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ Paso 4: Éxito ┌─────────────────────────────────────────────────────────────┐ │ │ │ ✅ Contraseña actualizada │ │ │ │ Tu contraseña ha sido actualizada exitosamente. │ │ Por seguridad, cerramos todas tus sesiones activas. │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Iniciar sesión │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Tareas Técnicas ### Database (DB) - [ ] Tabla `password_reset_tokens`: ```sql CREATE TABLE password_reset_tokens ( id UUID PRIMARY KEY, user_id UUID REFERENCES users(id), token VARCHAR(255) UNIQUE NOT NULL, expires_at TIMESTAMP NOT NULL, used_at TIMESTAMP, ip_address VARCHAR(45), created_at TIMESTAMP DEFAULT NOW(), INDEX idx_token_expires (token, expires_at) ); ``` - [ ] Tabla `password_reset_rate_limits`: ```sql CREATE TABLE password_reset_rate_limits ( id UUID PRIMARY KEY, email VARCHAR(255) NOT NULL, ip_address VARCHAR(45), attempts INT DEFAULT 1, window_start TIMESTAMP DEFAULT NOW(), INDEX idx_email_window (email, window_start) ); ``` ### Backend (BE) - [ ] Endpoint `POST /api/v1/auth/password/forgot` - Validar email - Rate limiting (3 por hora) - Generar token seguro (crypto.randomBytes) - Guardar token con expiración de 1 hora - Enviar email - Respuesta genérica (no revelar si existe) - [ ] Endpoint `GET /api/v1/auth/password/reset/:token` - Validar token existe y no expiró - Validar no fue usado - Devolver status del token - [ ] Endpoint `POST /api/v1/auth/password/reset` - Validar token - Validar nueva contraseña - Hash nueva contraseña - Actualizar password - Marcar token como usado - Invalidar todas las sesiones - Enviar email de confirmación - [ ] Service `PasswordResetService` - `requestReset(email)` - `validateToken(token)` - `resetPassword(token, newPassword)` - `generateSecureToken()` - [ ] Email templates (Nodemailer) - Template de recuperación - Template de confirmación - [ ] Tests unitarios (12 casos) - [ ] Tests de integración (8 escenarios) ### Frontend (FE) - [ ] Página `ForgotPassword.tsx` - [ ] Página `ResetPassword.tsx` - [ ] Form validation con React Hook Form - [ ] Password strength indicator - [ ] Manejo de estados (loading, success, error) - [ ] Manejo de tokens expirados/inválidos - [ ] Tests con React Testing Library ### Testing (QA) - [ ] E2E: Flujo completo de recuperación - [ ] E2E: Token expirado - [ ] E2E: Token ya usado - [ ] E2E: Token inválido - [ ] E2E: Rate limiting - [ ] E2E: Contraseña débil rechazada - [ ] Test de seguridad: Token debe ser criptográficamente seguro - [ ] Test de seguridad: No revelar si email existe - [ ] Performance: Email enviado en < 2s --- ## Dependencias - **Bloqueantes:** - US-AUTH-001: Necesita usuarios con contraseñas - US-AUTH-002: Para el flujo de login posterior --- ## Definition of Ready (DoR) - [ ] Mockups aprobados - [ ] Email templates diseñados - [ ] API contract definido - [ ] Estrategia de seguridad revisada - [ ] Rate limiting definido --- ## Definition of Done (DoD) - [ ] Código implementado y revisado - [ ] Tests unitarios con 80%+ cobertura - [ ] Tests de integración pasando - [ ] Tests E2E implementados - [ ] Email templates funcionales - [ ] Rate limiting implementado - [ ] Tokens seguros (crypto.randomBytes) - [ ] No se revela existencia de emails - [ ] Logs implementados - [ ] Documentación actualizada - [ ] QA aprobado en staging - [ ] Deploy a producción exitoso --- ## Notas Técnicas ### Token Generation ```typescript import crypto from 'crypto'; // Generar token seguro const token = crypto.randomBytes(32).toString('hex'); // Resultado: "a1b2c3d4e5f6..." (64 caracteres) // Hash del token antes de guardar (opcional, extra seguridad) const hashedToken = crypto .createHash('sha256') .update(token) .digest('hex'); ``` ### Password Reset Flow 1. Usuario solicita recuperación 2. Backend verifica rate limits 3. Backend genera token seguro 4. Backend guarda token con expiración (1 hora) 5. Backend envía email con link 6. Usuario click en link 7. Frontend valida token con backend 8. Usuario ingresa nueva contraseña 9. Backend valida token nuevamente 10. Backend actualiza contraseña 11. Backend marca token como usado 12. Backend invalida sesiones activas 13. Backend envía email de confirmación ### Email Template (HTML) ```html

Recupera tu contraseña

Hola {{userName}},

Recibimos una solicitud para recuperar tu contraseña.

Restablecer mi contraseña

O copia este link: {{resetLink}}

Este link expirará en 1 hora.

Si no solicitaste este cambio, puedes ignorar este email.

``` ### Environment Variables ```env PASSWORD_RESET_TOKEN_EXPIRY=3600 # 1 hora en segundos PASSWORD_RESET_RATE_LIMIT=3 # intentos por ventana PASSWORD_RESET_RATE_WINDOW=3600 # 1 hora FRONTEND_URL=https://trading.com ``` ### Security Best Practices 1. **Tokens seguros:** - Usar crypto.randomBytes (no Math.random) - Mínimo 32 bytes (256 bits) - Considerar hashear antes de guardar 2. **No revelar información:** - Mismo mensaje para email existente o no - No indicar si un email está registrado 3. **Rate limiting:** - Máximo 3 intentos por hora por email - También limitar por IP 4. **Expiración:** - Tokens expiran en 1 hora - Tokens de un solo uso 5. **Invalidar sesiones:** - Al cambiar contraseña, cerrar todas las sesiones - Forzar nuevo login 6. **Logging:** - Log de todas las solicitudes - Log de tokens usados/expirados - No loggear el token en texto plano 7. **Email de confirmación:** - Notificar al usuario que su contraseña cambió - Incluir información de cuándo y desde dónde ### Rate Limiting Implementation ```typescript async function checkRateLimit(email: string, ip: string) { const oneHourAgo = new Date(Date.now() - 3600000); const attempts = await db.passwordResetRateLimits.count({ where: { email, window_start: { gte: oneHourAgo } } }); if (attempts >= 3) { throw new Error('Too many password reset attempts'); } // Registrar intento await db.passwordResetRateLimits.create({ data: { email, ip_address: ip } }); } ``` --- ## Requerimientos Relacionados - [RF-AUTH-002: Autenticación por Email](../requerimientos/RF-AUTH-002-email.md) ## Especificaciones Relacionadas - [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md) - [ET-AUTH-003: Database](../especificaciones/ET-AUTH-003-database.md)