--- id: "US-AUTH-012" title: "Gestion de Sesiones" type: "User Story" status: "To Do" priority: "Alta" epic: "OQI-001" story_points: 5 created_date: "2025-12-05" updated_date: "2026-01-04" --- # US-AUTH-012: Gestión de Sesiones **Version:** 1.0.0 **Fecha:** 2025-12-05 **Estado:** Pendiente **Story Points:** 5 **Prioridad:** P1 (Alta) **Épica:** [OQI-001](../_MAP.md) --- ## Historia de Usuario **Como** usuario de Trading Platform **Quiero** poder ver y gestionar mis sesiones activas en diferentes dispositivos **Para** tener control sobre dónde está abierta mi cuenta y poder cerrar sesiones remotamente por seguridad --- ## Criterios de Aceptación ### AC-001: Página de sesiones **Dado** que estoy autenticado **Cuando** accedo a Configuración > Seguridad > Sesiones **Entonces** debería ver una lista de todas mis sesiones activas ### AC-002: Información de cada sesión **Dado** que estoy viendo mis sesiones activas **Cuando** veo una sesión en la lista **Entonces** debería ver: - Tipo de dispositivo (Computadora, Móvil, Tablet) - Sistema operativo (Windows, macOS, iOS, Android, Linux) - Navegador (Chrome, Safari, Firefox, etc.) - Ubicación aproximada (Ciudad, País) - Dirección IP - Fecha y hora de último acceso - Indicador de "Sesión actual" si es la sesión activa - Botón "Cerrar sesión" (excepto para sesión actual) ### AC-003: Sesión actual destacada **Dado** que estoy viendo mis sesiones **Cuando** identifico mi sesión actual **Entonces** debería estar: - Marcada claramente como "Esta sesión" - En la parte superior de la lista - Con un badge o color distintivo - Sin botón de "Cerrar sesión" ### AC-004: Cerrar una sesión individual **Dado** que veo una sesión que no reconozco **Cuando** hago click en "Cerrar sesión" **Entonces** debería: 1. Ver confirmación "¿Cerrar esta sesión?" 2. Al confirmar, invalidar ese token JWT 3. Ver mensaje "Sesión cerrada" 4. La sesión desaparece de la lista 5. Si esa sesión intenta hacer requests, recibe 401 Unauthorized ### AC-005: Cerrar todas las demás sesiones **Dado** que quiero cerrar sesión en todos mis dispositivos **Cuando** hago click en "Cerrar todas las demás sesiones" **Entonces** debería: 1. Ver confirmación "¿Cerrar todas las sesiones excepto la actual?" 2. Al confirmar, invalidar todos los tokens excepto el actual 3. Ver mensaje "X sesiones cerradas" 4. Solo quedar mi sesión actual en la lista ### AC-006: Detección de dispositivo **Dado** que inicio sesión desde diferentes dispositivos **Cuando** reviso mis sesiones **Entonces** debería ver íconos apropiados: - 💻 Computadora de escritorio - 📱 Teléfono móvil - 📋 Tablet - 🌐 Desconocido ### AC-007: Geolocalización **Dado** que inicio sesión desde diferentes ubicaciones **Cuando** reviso mis sesiones **Entonces** debería ver ubicación aproximada basada en IP: - "San Francisco, Estados Unidos" - "Ciudad de México, México" - "Madrid, España" - Si no se puede determinar: "Ubicación desconocida" ### AC-008: Alerta de sesión sospechosa **Dado** que inicio sesión desde un dispositivo o ubicación nueva **Cuando** completo el login **Entonces** debería: 1. Recibir email de notificación con: - Dispositivo y ubicación - Fecha y hora - Link para cerrar sesión si no fui yo 2. (Opcional) Ver notificación in-app ### AC-009: Historial de sesiones **Dado** que quiero ver sesiones pasadas **Cuando** veo la sección de historial **Entonces** debería ver últimas 20 sesiones incluyendo: - Sesiones activas (verde) - Sesiones cerradas manualmente (gris) - Sesiones expiradas (naranja) - Con timestamps de inicio y fin ### AC-010: Auto-expiración de sesiones **Dado** que una sesión lleva 30 días sin actividad **Cuando** esa sesión intenta hacer un request **Entonces** debería: - Recibir 401 Unauthorized - Ser redirigido a login - Desaparecer de la lista de sesiones activas ### AC-011: Sesión sin "Recordarme" **Dado** que inicié sesión sin marcar "Recordarme" **Cuando** pasan 24 horas **Entonces** esa sesión debería expirar automáticamente ### AC-012: Sesión con "Recordarme" **Dado** que inicié sesión con "Recordarme" marcado **Cuando** paso tiempo sin usar la app **Entonces** la sesión debería permanecer activa hasta 30 días ### AC-013: Refresh tokens **Dado** que tengo una sesión activa **Cuando** mi access token expira (15 minutos) **Entonces** debería: - Usar refresh token automáticamente - Obtener nuevo access token - Continuar usando la app sin interrupciones ### AC-014: Cerrar sesión actual **Dado** que quiero cerrar mi sesión actual **Cuando** hago click en "Cerrar sesión" en el header **Entonces** debería: 1. Invalidar mi token actual 2. Limpiar localStorage/cookies 3. Ser redirigido a la página de login 4. No poder acceder a rutas protegidas --- ## Mockup ``` ┌─────────────────────────────────────────────────────────────┐ │ Configuración > Seguridad > Sesiones │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Sesiones activas │ │ Gestiona dónde está abierta tu cuenta │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 💻 Chrome en macOS 🟢 Esta sesión │ │ │ │ │ │ │ │ San Francisco, Estados Unidos • 201.45.67.89 │ │ │ │ Última actividad: Hace 2 minutos │ │ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 📱 Safari en iPhone [Cerrar sesión]│ │ │ │ │ │ │ │ Ciudad de México, México • 189.203.45.12 │ │ │ │ Última actividad: Hace 3 horas │ │ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ 💻 Firefox en Windows [Cerrar sesión]│ │ │ │ │ │ │ │ Madrid, España • 85.123.45.67 │ │ │ │ Última actividad: Hace 2 días │ │ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 🚪 Cerrar todas las demás sesiones │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ──────────────────────────────────────────────────────── │ │ │ │ Historial de sesiones │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 📋 Chrome en iPad ⏱️ Sesión expirada │ │ │ │ Barcelona, España │ │ │ │ 15 Nov 2025 - 18 Nov 2025 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 📱 Chrome en Android ✓ Cerrada manual │ │ │ │ Bogotá, Colombia │ │ │ │ 10 Nov 2025 - 12 Nov 2025 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ Modal de confirmación: ┌─────────────────────────────────────────────────────────────┐ │ │ │ ⚠️ Cerrar todas las demás sesiones │ │ │ │ Esto cerrará sesión en todos tus otros dispositivos. │ │ Solo permanecerá activa tu sesión actual. │ │ │ │ Se cerrarán 2 sesiones. │ │ │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ Cancelar │ │ Cerrar sesiones │ │ │ └──────────────────────┘ └──────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ Email de nueva sesión: ┌─────────────────────────────────────────────────────────────┐ │ De: Trading Platform Security │ │ Asunto: Nueva sesión iniciada en tu cuenta │ │ │ │ Hola Juan, │ │ │ │ Se inició sesión en tu cuenta desde un nuevo dispositivo: │ │ │ │ 📱 Safari en iPhone │ │ 📍 Ciudad de México, México │ │ 🕐 5 de Diciembre, 2025 a las 14:30 CST │ │ 🌐 IP: 189.203.45.12 │ │ │ │ ¿Fuiste tú? │ │ Si reconoces esta actividad, puedes ignorar este email. │ │ │ │ ¿No fuiste tú? │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Cerrar esta sesión inmediatamente │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ También puedes cambiar tu contraseña desde: │ │ Configuración > Seguridad > Cambiar contraseña │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Tareas Técnicas ### Database (DB) - [ ] Tabla `user_sessions`: ```sql CREATE TABLE user_sessions ( id UUID PRIMARY KEY, user_id UUID REFERENCES users(id), refresh_token VARCHAR(512) UNIQUE NOT NULL, access_token_jti VARCHAR(255), -- JWT ID del access token actual device_type VARCHAR(50), -- 'desktop', 'mobile', 'tablet' device_name VARCHAR(255), -- 'Chrome on macOS' os VARCHAR(100), browser VARCHAR(100), ip_address VARCHAR(45), location_city VARCHAR(100), location_country VARCHAR(100), user_agent TEXT, remember_me BOOLEAN DEFAULT false, last_activity TIMESTAMP DEFAULT NOW(), expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT NOW(), INDEX idx_user_expires (user_id, expires_at), INDEX idx_refresh_token (refresh_token), INDEX idx_last_activity (last_activity) ); ``` - [ ] Tabla `session_history`: ```sql CREATE TABLE session_history ( id UUID PRIMARY KEY, user_id UUID REFERENCES users(id), device_name VARCHAR(255), ip_address VARCHAR(45), location_city VARCHAR(100), location_country VARCHAR(100), status VARCHAR(50), -- 'active', 'closed_manual', 'expired' started_at TIMESTAMP NOT NULL, ended_at TIMESTAMP, created_at TIMESTAMP DEFAULT NOW(), INDEX idx_user_started (user_id, started_at) ); ``` ### Backend (BE) - [ ] Modificar login para crear sesión: - Generar access token (JWT, 15 min) - Generar refresh token (aleatorio, 30 días) - Parsear user agent - Obtener geolocalización de IP - Guardar sesión en DB - Detectar si es nuevo dispositivo/ubicación - Enviar email si es sospechoso - [ ] Endpoint `GET /api/v1/auth/sessions` - Listar sesiones activas del usuario - Marcar sesión actual - Ordenar por last_activity DESC - [ ] Endpoint `GET /api/v1/auth/sessions/history` - Últimas 20 sesiones (activas + cerradas) - [ ] Endpoint `DELETE /api/v1/auth/sessions/:id` - Cerrar sesión específica - Invalidar tokens - Registrar en historial - [ ] Endpoint `DELETE /api/v1/auth/sessions/others` - Cerrar todas excepto actual - [ ] Endpoint `POST /api/v1/auth/refresh` - Validar refresh token - Generar nuevo access token - Actualizar last_activity - [ ] Endpoint `POST /api/v1/auth/logout` - Cerrar sesión actual - Invalidar tokens - [ ] Service `SessionService` - `createSession()` - `validateSession()` - `refreshAccessToken()` - `terminateSession()` - `terminateAllOtherSessions()` - `getActiveSessions()` - `isNewDevice()` - [ ] Service `DeviceDetectionService` - `parseUserAgent()` - `getDeviceType()` - `getOS()` - `getBrowser()` - [ ] Service `GeolocationService` - `getLocationFromIP()` - Usar: ipapi.co, ip-api.com, o MaxMind GeoIP2 - [ ] Librería: `ua-parser-js` (user agent parsing) - [ ] Cron job: Limpiar sesiones expiradas diariamente - [ ] Tests unitarios (15 casos) - [ ] Tests de integración (10 escenarios) ### Frontend (FE) - [ ] Página `Settings/Security/Sessions.tsx` - [ ] Componente `SessionCard.tsx` - [ ] Componente `SessionHistoryCard.tsx` - [ ] Modal de confirmación de cierre - [ ] Servicio de auto-refresh de tokens - Interceptor de Axios/Fetch - Detectar 401 - Llamar a /refresh - Reintentar request original - [ ] Almacenamiento de tokens: - Access token en memoria (state) - Refresh token en httpOnly cookie (más seguro) - [ ] Tests con React Testing Library ### Testing (QA) - [ ] E2E: Ver sesiones activas - [ ] E2E: Cerrar sesión individual - [ ] E2E: Cerrar todas las demás sesiones - [ ] E2E: Auto-refresh de access token - [ ] E2E: Sesión expira después de 30 días - [ ] E2E: Email de nueva sesión - [ ] E2E: Login desde múltiples dispositivos - [ ] Test de seguridad: Refresh token rotation - [ ] Test de seguridad: Session fixation - [ ] Performance: Lista de sesiones < 300ms --- ## Dependencias - **Bloqueantes:** - US-AUTH-002: Login genera tokens - Servicio de geolocalización (ipapi.co o similar) --- ## Definition of Ready (DoR) - [ ] Mockups aprobados - [ ] API contract definido - [ ] Estrategia de tokens definida (access + refresh) - [ ] Servicio de geolocalización seleccionado - [ ] Esquema de base de datos revisado --- ## Definition of Done (DoD) - [ ] Código implementado y revisado - [ ] Tests unitarios con 80%+ cobertura - [ ] Tests de integración pasando - [ ] Tests E2E implementados - [ ] Auto-refresh de tokens funcional - [ ] Geolocalización funcional - [ ] Device detection funcional - [ ] Email de alertas configurado - [ ] Cron job de limpieza configurado - [ ] Documentación actualizada - [ ] QA aprobado en staging - [ ] Deploy a producción exitoso --- ## Notas Técnicas ### Token Strategy: Access + Refresh **Access Token (JWT):** - Duración: 15 minutos - Almacenado en: Memoria (React state) - Incluye: user_id, email, role, jti (JWT ID) - Se envía en header: `Authorization: Bearer ` **Refresh Token:** - Duración: 24 horas (sin "Recordarme") o 30 días (con "Recordarme") - Almacenado en: httpOnly cookie - Es un string aleatorio (crypto.randomBytes) - Se guarda hasheado en DB - Rotation: Cada refresh genera nuevo token ### JWT Structure ```json { "jti": "session-uuid", // JWT ID (unique) "sub": "user-uuid", "email": "user@example.com", "role": "user", "iat": 1234567890, "exp": 1234568790 // +15 min } ``` ### Auto-Refresh Flow ```typescript // Axios interceptor axios.interceptors.response.use( response => response, async error => { if (error.response?.status === 401) { // Access token expiró, intentar refresh try { const { accessToken } = await refreshTokens(); // Actualizar token en memoria setAccessToken(accessToken); // Reintentar request original error.config.headers.Authorization = `Bearer ${accessToken}`; return axios.request(error.config); } catch (refreshError) { // Refresh falló, redirect a login window.location.href = '/login'; } } return Promise.reject(error); } ); ``` ### User Agent Parsing ```typescript import UAParser from 'ua-parser-js'; const parser = new UAParser(userAgent); const result = parser.getResult(); const deviceInfo = { device_type: result.device.type || 'desktop', // mobile, tablet, desktop device_name: `${result.browser.name} on ${result.os.name}`, os: `${result.os.name} ${result.os.version}`, browser: `${result.browser.name} ${result.browser.version}` }; ``` ### Geolocation Service ```typescript // Opción 1: ipapi.co (gratuito, 30k requests/mes) const response = await fetch(`https://ipapi.co/${ip}/json/`); const data = await response.json(); const location = { city: data.city, country: data.country_name, latitude: data.latitude, longitude: data.longitude }; // Opción 2: ip-api.com (gratuito, 45 requests/min) // Opción 3: MaxMind GeoIP2 (pago, más preciso) ``` ### New Device Detection ```typescript async function isNewDevice(userId, deviceName, ipAddress) { const existingSession = await db.userSessions.findFirst({ where: { user_id: userId, device_name: deviceName, ip_address: ipAddress, created_at: { gte: thirtyDaysAgo } } }); return !existingSession; } ``` ### Refresh Token Rotation Cada vez que se usa un refresh token, se genera uno nuevo: ```typescript async function refreshAccessToken(oldRefreshToken) { // 1. Validar refresh token const session = await findSessionByRefreshToken(oldRefreshToken); if (!session || session.expires_at < new Date()) { throw new Error('Invalid or expired refresh token'); } // 2. Generar nuevo access token const accessToken = generateJWT(session.user_id); // 3. Generar nuevo refresh token const newRefreshToken = crypto.randomBytes(64).toString('hex'); // 4. Actualizar sesión await db.userSessions.update({ where: { id: session.id }, data: { refresh_token: hashToken(newRefreshToken), access_token_jti: accessToken.jti, last_activity: new Date() } }); // 5. Devolver tokens return { accessToken, refreshToken: newRefreshToken }; } ``` ### Security Considerations 1. **Refresh Token Rotation:** - Cada refresh invalida el token anterior - Previene replay attacks 2. **httpOnly Cookies:** - Refresh token en httpOnly cookie - No accesible desde JavaScript - Previene XSS 3. **Secure Cookies:** - Flag `Secure` (solo HTTPS) - Flag `SameSite=Strict` 4. **JTI (JWT ID):** - Cada access token tiene ID único - Permite invalidación específica 5. **Device Fingerprinting:** - Detectar cambios sospechosos - Alertar al usuario 6. **Rate Limiting:** - Limitar requests a /refresh - Prevenir brute force ### Environment Variables ```env JWT_SECRET=your-secret-key-256-bits JWT_ACCESS_EXPIRY=15m JWT_REFRESH_EXPIRY_SHORT=24h # sin "Recordarme" JWT_REFRESH_EXPIRY_LONG=30d # con "Recordarme" GEOLOCATION_API_KEY=your-api-key # si usas servicio de pago ``` ### Cron Job: Cleanup ```typescript // Ejecutar diariamente a las 3 AM cron.schedule('0 3 * * *', async () => { // Eliminar sesiones expiradas await db.userSessions.deleteMany({ where: { expires_at: { lt: new Date() } } }); // Mover a historial // (opcional, si no usas soft deletes) }); ``` --- ## Requerimientos Relacionados - [RF-AUTH-006: Gestión de Sesiones](../requerimientos/RF-AUTH-006-sessions.md) ## Especificaciones Relacionadas - [ET-AUTH-002: JWT Tokens](../especificaciones/ET-AUTH-002-jwt.md) - [ET-AUTH-003: Database](../especificaciones/ET-AUTH-003-database.md) - [ET-AUTH-006: Session Management](../especificaciones/ET-AUTH-006-sessions.md)