Saltar al contenido principal

Seguridad y 2FA — Referencia Técnica

← Volver a Seguridad y 2FA

Dónde vive esto

Backend

Frontend apps/frontend-nextjs ahora tiene páginas de Configuración dedicadas para esto, todas enlazadas desde la barra lateral de configuración (page-components/SettingsPage.tsx):

La página /verification de frontend-admin maneja el lado de aprobar/rechazar/eliminar (ver el checklist a continuación).

Checklist de implementación técnica

  • setupTwoFactor / verifyTwoFactorSetup — implementados en user-two-factor-auth.resolver.js y conectados de extremo a extremo en SecuritySettingsPage.tsx; los antiguos campos de esquema enableTwoFactor / verifyTwoFactorCode se eliminaron por ser stubs obsoletos (los métodos del manager con el mismo nombre todavía existen y respaldan el paso de 2FA en el momento del inicio de sesión a través de verifyLoginTwoFactor)
  • disableTwoFactor, regenerateBackupCodes — implementados y conectados en SecuritySettingsPage.tsx; los antiguos campos de esquema verifyBackupCode / generateBackupCodes se eliminaron por ser stubs obsoletos — el consumo de códigos de respaldo ocurre dentro de verifyLoginTwoFactor en el momento del inicio de sesión, y la regeneración es regenerateBackupCodes (sin argumentos)
  • activeSessions / getCurrentSession / sessionDetails / loginHistory — todos conectados; activeSessions y loginHistory tienen frontend en SessionsSettingsPage.tsx
  • revokeSession / revokeAllOtherSessions — implementados y conectados en SessionsSettingsPage.tsx; terminateSession, terminateAllSessions, y terminateOtherSessions también están conectados (alias de revokeSession / revokeAllOtherSessions)
  • La verificación del JWT ahora exige un estado de sesión vivo — corregido en esta sesión. graphql/context/auth-helper.js#verifyTokenAndGetUser antes solo verificaba la firma/expiración del JWT, nunca user_session — así que revokeSession/terminateAllSessions/cerrar sesión en realidad no detenían la autenticación de un token; solo cambiaban una fila de la BD que nada volvía a leer. Ahora también llama a userSessionAccessService.isValid(decoded.sessionId) y rechaza la solicitud si la sesión está inactiva/revocada/expirada. Consulta Sesiones e Historial de Inicio de Sesión para la nueva mutación logout con la que va de la mano, y admin-auth-helper.js para la verificación equivalente del lado de administrador que esto refleja.
  • securityAlerts / markSecurityAlertRead / dismissSecurityAlert — implementados y conectados en SecurityAlertsPage.tsx; no existe un campo separado mySecurityAlertssecurityAlerts es el que está en el esquema
  • securityEventLog / securityStats — ambos conectados: securityEventLog lee el registro de auditoría persistido (mostrado en SecurityAlertsPage.tsx), y securityStats devuelve conteos reales (inicios de sesión, intentos fallidos/sospechosos/bloqueados, cantidad de dispositivos) derivados de security_event
  • checkSuspiciousLogin / detectSuspiciousActivity — conectados; sin interfaz en el frontend
  • Solicitud/aprobación/rechazo/eliminación de la insignia de verificación — el envío de la solicitud está conectado mediante verification.resolver.js y VerificationRequestPage.tsx (/settings/verification); la aprobación/rechazo/eliminación por parte del administrador está conectada en graphql/resolvers/admin/user-moderation.resolver.js y la página /verification de frontend-admin — la insignia en sí se muestra en los perfiles mediante isVerified

Autenticación de dos factores (2FA / TOTP)

twoFactorStatus devuelve si el 2FA está actualmente habilitado, cuántos códigos de respaldo quedan, y cuándo se usó por última vez — se usa para renderizar la tarjeta de configuración de 2FA.

El flujo de activación tiene dos pasos: setupTwoFactor (sin argumentos) genera un secret TOTP más un código QR — como un PNG en data-URL qrCode, un qrCodeSvg en línea seguro para CSP, y la otpauthUrl en bruto para clientes que renderizan su propio QR — luego verifyTwoFactorSetup confirma la configuración verificando el primer código TOTP (TwoFactorSetupInput { code }). Si tiene éxito, devuelve un conjunto de backupCodes de un solo uso que el usuario debe guardar de forma segura.

disableTwoFactor desactiva el 2FA — recibe un TwoFactorVerifyInput { code } con el código TOTP actual como confirmación. regenerateBackupCodes (sin argumentos) regenera el conjunto de códigos de respaldo, invalidando los anteriores.

query TwoFactorStatus { twoFactorStatus { isEnabled backupCodesCount lastUsed setupDate } }

# Step 1 — returns secret + QR code (PNG data-URL, inline SVG, and the raw otpauth:// URL)
mutation SetupTwoFactor {
setupTwoFactor { success message qrCode qrCodeSvg otpauthUrl secret backupCodes }
}

# Step 2 — confirm with first TOTP code from authenticator app
mutation VerifyTwoFactorSetup($code: String!) {
verifyTwoFactorSetup(input: { code: $code }) { success message backupCodes }
}

# Turn off 2FA (requires valid TOTP code)
mutation DisableTwoFactor($code: String!) {
disableTwoFactor(input: { code: $code }) { success message }
}

# Regenerate backup codes (invalidates old set)
mutation RegenerateBackupCodes { regenerateBackupCodes { success message backupCodes } }

No hay una mutación independiente verifyBackupCode o enableTwoFactor en el esquema — esos nombres se eliminaron por ser stubs obsoletos. El consumo de códigos de respaldo ocurre como parte de completar un inicio de sesión condicionado por 2FA (ver más abajo), y activar el 2FA es el par setupTwoFactor / verifyTwoFactorSetup de arriba.

Sesiones activas

activeSessions devuelve todas las sesiones actuales con metadatos del dispositivo y la bandera isCurrent. El atajo currentSession viene preseleccionado para la visualización "Este dispositivo". Usa expiresAt para mostrar cuándo una sesión expirará automáticamente.

loginHistory muestra todos los intentos de inicio de sesión — tanto exitosos como fallidos. Las entradas fallidas incluyen failureReason (por ejemplo, invalid_password, account_suspended) — se usa para mostrar una alerta de seguridad si el usuario ve intentos fallidos que no reconoce.

query ActiveSessions {
activeSessions {
sessions {
id deviceName deviceType browser os
ipAddress location isCurrent lastActivity createdAt
}
total
currentSession { id deviceName }
}
}

query GetCurrentSession { getCurrentSession { id deviceName isCurrent } }

query LoginHistory($limit: Int) {
loginHistory(limit: $limit) {
entries {
loginMethod deviceName ipAddress location
success failureReason createdAt
}
total
}
}

# Invalidate a specific session (signs out that device)
mutation TerminateSession($sessionId: ID!) { terminateSession(sessionId: $sessionId) { success } }

# Sign out all devices
mutation TerminateAllSessions { terminateAllSessions { sessionsTerminated } }

# Sign out all devices except the current one
mutation TerminateOtherSessions { terminateOtherSessions { sessionsTerminated } }

Detección de inicio de sesión sospechoso

checkSuspiciousLogin (sin argumentos — evalúa al usuario/solicitud autenticado actual) devuelve isSuspicious, un riskScore, una lista de factors (por ejemplo, señales de riesgo detectadas), y requiresVerification. No hay un campo recommendation ni un tipo SuspiciousLoginInput en el esquema actual.

query CheckSuspiciousLogin {
checkSuspiciousLogin { isSuspicious riskScore factors requiresVerification }
}

Completar un inicio de sesión condicionado por 2FA

login devuelve un AuthResponse. Si la cuenta tiene el 2FA habilitado, token vuelve como null y requiresTwoFactor es true, junto con un twoFactorToken de corta duración y el twoFactorMethod a solicitar (authenticator, sms, o email). El cliente entonces llama a verifyLoginTwoFactor con ese token y el código — que acepta el código TOTP/SMS/correo electrónico o un código de respaldo (verificado primero, y consumido si se usa) — para obtener el token de sesión real.

mutation VerifyLoginTwoFactor($twoFactorToken: String!, $code: String!) {
verifyLoginTwoFactor(twoFactorToken: $twoFactorToken, code: $code) {
user { id username }
token
requiresTwoFactor
}
}

Alertas de seguridad

securityAlerts(limit, offset, severity) devuelve un SecurityAlertsResponse con la lista de alertas más total y unreadCount. Cada SecurityAlert tiene un alertType (por ejemplo, suspicious_login, new_login, password_change, email_change, two_factor_change), un severity (low/medium/high/critical), un title/description, y una bandera isRead en lugar de una bandera de descarte — markSecurityAlertRead y dismissSecurityAlert ambos reciben un alertId y devuelven un Boolean simple. Una alerta password_change se emite automáticamente desde password.manager.js al cambiar la contraseña, y una alerta two_factor_disabled desde two-factor-auth.manager.js cuando se desactiva el 2FA.

type SecurityAlert {
id: ID!
userId: ID!
alertType: String!
severity: String! # "low", "medium", "high", "critical"
title: String!
description: String!
isRead: Boolean!
createdAt: DateTime!
metadata: JSON
}

query SecurityAlerts($limit: Int, $offset: Int, $severity: String) {
securityAlerts(limit: $limit, offset: $offset, severity: $severity) {
alerts { id alertType severity title description isRead createdAt }
total
unreadCount
}
}

mutation MarkSecurityAlertRead($alertId: ID!) { markSecurityAlertRead(alertId: $alertId) }
mutation DismissSecurityAlert($alertId: ID!) { dismissSecurityAlert(alertId: $alertId) }

Configuración de notificaciones de seguridad

Cada tipo de evento de seguridad puede activarse o desactivarse de forma independiente. updateSecurityNotificationSettings persiste los cambios. enableSecurityNotifications / disableSecurityNotifications son atajos masivos.

query SecurityNotificationSettings {
securityNotificationSettings {
loginAlerts suspiciousActivity passwordChanges
emailChanges twoFactorChanges newDeviceLogin
accountRecovery privacyChanges
}
}

mutation UpdateSecurityNotificationSettings($input: SecurityNotificationSettingsInput!) {
updateSecurityNotificationSettings(input: $input) { loginAlerts newDeviceLogin }
}

mutation EnableSecurityNotifications { enableSecurityNotifications }
mutation DisableSecurityNotifications { disableSecurityNotifications }

Registro de eventos de seguridad

securityEventLog es un registro de auditoría de solo lectura de todas las acciones relevantes de seguridad en la cuenta (inicios de sesión, cambios de contraseña, cambios de 2FA, etc.). Cada evento tiene un riskLevel. securityStats resume el registro — se usa para un panel de puntaje de seguridad. detectSuspiciousActivity ejecuta una verificación en tiempo real y devuelve si se encontraron patrones sospechosos.

query SecurityEventLog($limit: Int) {
securityEventLog(limit: $limit) {
id eventType description ipAddress userAgent location riskLevel createdAt
}
}

query SecurityStats {
securityStats {
totalLogins failedAttempts suspiciousActivities
blockedAttempts devicesCount lastSecurityScan
}
}

query DetectSuspiciousActivity { detectSuspiciousActivity }

Verificación de cuenta (insignia)

getVerificationStatus devuelve el estado de verificación actual del usuario y su categoría. verificationBadgeInfo devuelve los datos de renderizado (color, ícono, nombre para mostrar) necesarios para mostrar la insignia sin codificar cada categoría de forma fija.

requestVerification envía una nueva solicitud de verificación (VerificationRequestPage.tsx, /settings/verification) — verificationPriceCoins le indica al cliente cuántas monedas cuesta la solicitud por adelantado. grantVerification / removeVerification todavía existen en el esquema, pero el flujo de revisión del administrador (frontend-admin /verification) en realidad pasa por los equivalentes restringidos a administradores en graphql/resolvers/admin/user-moderation.resolver.js: adminGetPendingVerificationRequests (cola), adminVerifyUser (aprobar), adminRejectVerificationRequest (rechazar una solicitud pendiente), y adminRemoveVerification (revocar una insignia ya otorgada).

query VerificationStatus($userId: ID!) {
getVerificationStatus(userId: $userId) {
isVerified verificationStatus verificationCategory verifiedAt verificationRequestedAt
}
}

query VerificationBadgeInfo($userId: ID!) {
verificationBadgeInfo(userId: $userId) {
isVerified badgeType verificationCategory badgeColor badgeIcon displayName
}
}

mutation RequestVerification($input: RequestVerificationInput!) {
requestVerification(input: $input) { success message }
}

# Admin only:
mutation AdminGetPendingVerificationRequests($limit: Int, $offset: Int) {
adminGetPendingVerificationRequests(limit: $limit, offset: $offset) { requests { id user { id username } verificationCategory } total }
}
mutation AdminVerifyUser($userId: ID!, $verificationType: String) {
adminVerifyUser(userId: $userId, verificationType: $verificationType) { success message }
}
mutation AdminRejectVerificationRequest($userId: ID!, $reason: String) {
adminRejectVerificationRequest(userId: $userId, reason: $reason) { success message }
}
mutation AdminRemoveVerification($userId: ID!, $reason: String!) {
adminRemoveVerification(userId: $userId, reason: $reason) { success message }
}

Presencia y actividad

getUserPresence devuelve el estado en línea en vivo de un usuario, la hora de la última vez visto, y la actividad actual (si compartir el estado de actividad está habilitado). getUserActivityStats devuelve analíticas de uso para un período de tiempo — útil en la pantalla de analíticas del perfil. updateLastSeen y setUserOffline son llamados por el latido (heartbeat) del cliente y al pasar la app a segundo plano/cerrarse.

query UserPresence($userId: ID!) {
getUserPresence(userId: $userId) { isOnline lastSeen status activity deviceType }
}

query ActivityStats($userId: ID!, $timeframe: String) {
getUserActivityStats(userId: $userId, timeframe: $timeframe) {
totalTime activeDays mostActiveHour activityStreak lastActivity
}
}

# Called by client heartbeat to update online presence
mutation UpdateLastSeen { updateLastSeen }

# Called when the app moves to background or the user logs out
mutation SetUserOffline { setUserOffline }

Exportación de datos (RGPD)

Consulta Exportación de Datos para la API completa.

mutation RequestDataExport($input: DataExportRequestInput!) {
requestDataExport(input: $input) { success exportId estimatedCompletion }
}

query DataExportStatus($exportId: ID) {
dataExportStatus(exportId: $exportId) {
status progressPercentage downloadUrl expiresAt fileSize
}
}

query MyDataExports { myDataExports { id status exportType requestedAt completedAt downloadUrl } }

mutation CancelDataExport($exportId: ID!) { cancelDataExport(exportId: $exportId) { success } }
mutation DeleteDataExport($exportId: ID!) { deleteDataExport(exportId: $exportId) { success } }

Opciones de exportación

exportType acepta: full, posts, messages, media, profile.

Filtros opcionales: includePosts, includeComments, includeMessages, includeMedia, includeProfile, dateRangeStart, dateRangeEnd.