Buscar en la documentación

Módulo Auth

Inicio y cierre de sesión, verificación en dos pasos (MFA), recuperación y cambio de contraseña, tokens, bloqueo progresivo y vencimiento de la sesión.

14 min de lecturaVer en GitHub

Se encarga de comprobar quién es la persona y de mantener su sesión. Los permisos de cada acción los resuelve cada módulo con sus Policies; Auth solo entrega la lista de permisos del usuario para que el front arme el menú.

Endpoints

MétodoRutaSesiónLímiteRespuesta
POST/api/v1/auth/loginNo5 por minuto (correo + IP)Token, vencimiento y usuario
POST/api/v1/auth/logoutSíGeneral204 y el token queda revocado
GET/api/v1/auth/meSíGeneralUsuario con roles, permisos y estado del MFA
POST/api/v1/auth/mfa/verifyIngreso a medias5 por minutoSegundo paso: code o recovery_code; devuelve la sesión completa
POST/api/v1/auth/mfa/setupIngreso a medias o sesiónGeneralClave nueva sin confirmar y su dirección otpauth:// para el QR
POST/api/v1/auth/mfa/setup/confirmIngreso a medias o sesión5 por minutoActiva el MFA con el primer code; devuelve los 8 códigos de recuperación y, si venía de un ingreso a medias, la sesión completa
POST/api/v1/auth/mfa/recovery-codesSí5 por minutoCon un code actual, 8 códigos nuevos; los anteriores dejan de servir
DELETE/api/v1/auth/mfaSí5 por minutoCon un code actual, desactiva el MFA si la política del rol no lo exige
POST/api/v1/auth/password/forgotNo3 cada 15 minutos (correo + IP)202 siempre: si el correo es de una cuenta activa, le llega un código
POST/api/v1/auth/password/verify-codeNo5 por minuto (correo + IP)Con email y code, un pase de un solo uso (ticket) y si la cuenta pide segundo factor (mfa_required)
POST/api/v1/auth/password/resetNo5 por minuto (correo + IP)Con email, ticket, password, su confirmación y, si hace falta, mfa_code o recovery_code; 204
PUT/api/v1/auth/passwordSí5 por minuto por personaCon current_password, password y su confirmación; 204

Todas las rutas de la API tienen además el límite general de 120 peticiones por minuto, por usuario o por IP.

POST /api/v1/auth/login
{ "email": "agente1@trazo.test", "password": "••••••••••••" }

200 OK
{
  "data": {
    "status": "complete",
    "token": "12|trazo_…",
    "token_type": "Bearer",
    "expires_at": "2026-10-22T20:54:28+00:00",
    "user": {
      "id": "01m5fr6tp5p0cbwjtjwdsst5n0",
      "name": "Laura Gómez",
      "email": "agente1@trazo.test",
      "roles": [{ "code": "agent", "name": "Agente" }],
      "permissions": ["dashboard.view.own", "incidents.create", "…"],
      "mfa": { "enabled": true, "required": true, "recovery_codes_left": 8 }
    }
  }
}

status dice qué sigue después de la contraseña:

statusTokenuserQué hace el front
completeSesión completaSíEntra
setup_suggestedSesión completaSíEntra, pero ofrece configurar el MFA con “Configurar más tarde” (usa un ingreso de gracia)
mfa_verifyIngreso a mediasnullPide el código de la aplicación
mfa_setupIngreso a mediasnullExige configurar el MFA para entrar

Flujo del login

flowchart TD
  A[POST /auth/login] --> R{¿Más de 5 intentos<br>en un minuto?}
  R -- Sí --> E429[429 TOO_MANY_REQUESTS]
  R -- No --> L{¿Correo bloqueado?}
  L -- Sí --> E423[423 ACCOUNT_LOCKED<br>con Retry-After]
  L -- No --> C{¿Credenciales correctas<br>y cuenta activa?}
  C -- No --> F[Suma un fallo]
  F --> S{¿Llegó a un escalón?}
  S -- Sí --> B[Bloquea y lo registra en el log] --> E423
  S -- No --> E401[401 INVALID_CREDENTIALS]
  C -- Sí --> M{¿Tiene MFA?}
  M -- Sí --> V[Token de ingreso a medias<br>status mfa_verify]
  M -- No --> P{¿Su rol lo exige?}
  P -- No --> OK[Sesión completa<br>status complete]
  P -- Sí --> G{¿Le queda gracia?}
  G -- Sí --> SG[Sesión completa y gasta un ingreso de gracia<br>status setup_suggested]
  G -- No --> SU[Token de ingreso a medias<br>status mfa_setup]

Errores

CódigoEstadoCuándoParámetros
INVALID_CREDENTIALS401Contraseña incorrecta, correo inexistente o cuenta inactiva—
ACCOUNT_LOCKED423Demasiados fallos seguidos con ese correoretry_after (segundos), también en la cabecera Retry-After
TOO_MANY_REQUESTS429Más de 5 intentos por minuto con el mismo correo desde la misma IPCabecera Retry-After
UNAUTHENTICATED401Sin token, con un token revocado, vencido o inactivo, o con el token de un ingreso a medias fuera de las rutas del segundo paso—
MFA_CODE_INVALID422El código de la aplicación no es correctoattempts_left, durante el ingreso
MFA_CODE_ALREADY_USED422Era un código correcto, pero ya se había usado—
MFA_RECOVERY_CODE_INVALID422El código de recuperación no existe o ya se usóattempts_left
MFA_TOO_MANY_ATTEMPTS401Quinto código incorrecto en el mismo ingreso: se revoca el token—
MFA_ALREADY_ENABLED409Configurar el MFA cuando ya está activo—
MFA_NOT_ENABLED409Verificar, regenerar o desactivar sin MFA activo—
MFA_SETUP_NOT_STARTED409Confirmar sin haber pedido antes una clave—
MFA_REQUIRED_BY_POLICY409Desactivar el MFA cuando el rol lo exige—
MFA_NOT_PENDING409Verificar un código con una sesión que ya está completa—
MFA_CODE_REQUIRED422Recuperar la contraseña de una cuenta con MFA sin enviar ningún código—
PASSWORD_RESET_CODE_INVALID422Código de recuperación incorrecto, vencido, agotado o de un correo que no existe (la misma respuesta)—
PASSWORD_RESET_EXPIRED422El pase venció, ya se usó o la solicitud se anuló por intentos—
PASSWORD_BREACHED422La contraseña nueva está en filtraciones conocidas o entre las más comunes (error del campo password)—
PASSWORD_REPETITIVE422La contraseña nueva repite demasiado los mismos caracteres o un mismo bloque (error del campo password)—
PASSWORD_SEQUENTIAL422La contraseña nueva es casi toda una secuencia del alfabeto, los números o el teclado (error del campo password)—
PASSWORD_PERSONAL422La contraseña nueva contiene el correo, el nombre o la marca (error del campo password)—
CURRENT_PASSWORD_INVALID422Al cambiarla desde Mi cuenta, la contraseña actual no es correcta—
PASSWORD_SAME_AS_CURRENT422La contraseña nueva es igual a la actual—

Reglas de seguridad

No revelar qué cuentas existen

  • El mismo INVALID_CREDENTIALS sirve para la contraseña incorrecta, el correo que no existe y la cuenta inactiva. Las tres respuestas son idénticas, salvo el trace_id.
  • Mismo tiempo de respuesta. Si el correo no existe, la contraseña se verifica igual contra un hash de relleno con los mismos parámetros de Argon2id. Así, el tiempo de respuesta no delata nada.
  • El bloqueo se aplica por correo, exista o no la cuenta. Si solo se bloquearan las cuentas reales, “cuenta bloqueada” confirmaría que ese correo existe.

Bloqueo progresivo

Fallos seguidosEspera
51 minuto
105 minutos
1515 minutos
20 o más1 hora en cada fallo
  • Mientras dure el bloqueo, ni siquiera la contraseña correcta permite ingresar.
  • Un ingreso correcto reinicia el contador. Si no, el contador vence 24 horas después del primer fallo.
  • Dónde vive: en Redis, en la base de los contadores de seguridad (aparte de la caché de datos, ver Shared), con la huella SHA-256 del correo como clave, nunca el correo en claro.
  • Ajustes: los escalones son datos en config/trazo.php.
  • Registro en el log: cada bloqueo se registra como advertencia, con la huella, la IP y la cantidad de fallos. Los fallos aislados no se registran, para no llenar el log de ruido.
  • Evento del sistema: el bloqueo queda además como evento login_locked, agrupado por cuenta y con el correo enmascarado (da***@gmail.com), para que el Super admin lo vea en Sistema. Lo mismo pasa con un ingreso cortado por códigos MFA (mfa_attempts_exceeded), un código de recuperación anulado (reset_code_voided) y Have I Been Pwned sin responder (external_service_unavailable).

Sesión

ReglaValorDónde se cambia
Duración máxima8 horas desde el ingresoSESSION_MAX_MINUTES
InactividadSe cierra tras 1 hora sin peticionesSESSION_IDLE_MINUTES
Cuenta desactivadaSus sesiones abiertas dejan de valer en la siguiente petición—
Cierre de sesiónEl token se borra en el servidor—
  • Tokens:
    • Se guarda solo el hash SHA-256 del token. El valor en claro se entrega una única vez, al iniciar sesión.
    • Los tokens llevan el prefijo trazo_, para que los escáneres de secretos los reconozcan si alguno se filtra.
    • Cada día, el scheduler borra los tokens vencidos hace más de un día.
  • Contraseñas: al ingresar, si cambiaron los parámetros de Argon2id, la contraseña se vuelve a calcular con los nuevos. Las reglas de longitud mínima y contraseñas filtradas se aplican al crearlas o cambiarlas, no al ingresar.

Verificación en dos pasos (MFA)

TOTP estándar (RFC 6238): 6 dígitos que cambian cada 30 segundos, compatibles con cualquier aplicación de autenticación (Google Authenticator, Microsoft Authenticator, Authy…). La librería es pragmarx/google2fa, envuelta detrás del puerto OneTimePasswords.

Política por rol

  • Es un dato: la tabla mfa_policies dice, por rol, si el MFA es obligatorio y cuántos ingresos se puede posponer (grace_logins). Al instalar, todos los roles lo exigen con un ingreso de gracia.
  • Con varios roles gana la regla más estricta: obligatorio si algún rol lo exige, y la gracia más corta entre ellos.
  • Gracia: users.mfa_grace_used cuenta los ingresos pospuestos. El primero entra con setup_suggested; desde el segundo, mfa_setup exige configurarlo para entrar.
  • No hay atajo para apagarlo, ni siquiera en local: quien evalúa la aplicación la usa como en producción, con su teléfono.

Token de un ingreso a medias

  • Cuando falta el segundo paso, el login entrega un token con la habilidad mfa (no session) que vence a los 5 minutos.
  • El autenticador de Sanctum (AuthServiceProvider) lo rechaza en cualquier ruta salvo las del segundo paso (auth.mfa.challenge.*) y cerrar sesión. No hace falta revisarlo en cada módulo.
  • Al verificar o configurar con éxito, ese token se revoca y se entrega una sesión completa.
  • Cinco códigos incorrectos en un mismo ingreso revocan el token: hay que volver a poner la contraseña. Además, todos los endpoints de códigos tienen el límite de 5 por minuto por persona.

Protección del secreto y de los códigos

  • Secreto cifrado con la clave de la aplicación (cast encrypted): en la base, en un respaldo o en un log nunca aparece en claro. Solo sale una vez, al configurar.
  • Sin repetición: se guarda el paso de tiempo del último código aceptado y un código igual o anterior se rechaza. La comprobación es un UPDATE condicional, así que dos pedidos a la vez con el mismo código no pueden pasar los dos.
  • Ventana de ±30 segundos para tolerar un reloj de teléfono un poco corrido.
  • Códigos de recuperación: 8, de 10 caracteres sin letras que se confunden (ABCDE-FGHJK). Se guarda solo su hash SHA-256: son aleatorios y largos (unos 50 bits), así que no necesitan un hash lento. Cada uno sirve una vez y se aceptan con minúsculas, espacios o sin guion.

Avisos por correo

Se envía un aviso, por la cola (cifrado, como todo correo; ADR-0062) y en texto plano, cuando se activa o desactiva el MFA, cuando se usa un código de recuperación, cuando se generan códigos nuevos y cuando cambia la contraseña. Dice qué pasó, cuándo (hora de Colombia) y qué hacer si no fue la persona. No lleva enlaces ni datos sensibles. En local llega a Mailpit (http://localhost:8025).

Registro en el log

Solo lo que puede ser un ataque o un problema: un ingreso con código de recuperación (con cuántos quedan) y un ingreso cortado por intentos fallidos. Los códigos incorrectos aislados no se registran.

Recuperar y cambiar la contraseña

Política de contraseñas

  • De 12 a 128 caracteres, con confirmación. El máximo evita que una contraseña enorme haga trabajar de más a Argon2id.
  • No puede estar filtrada. Primero se revisa una lista local de las 10.000 más comunes (resources/security/common-passwords.txt, de SecLists). Después, Have I Been Pwned con su consulta anónima: solo salen los primeros 5 caracteres del SHA-1 y la respuesta viene con relleno. Si el servicio no responde en 3 segundos, queda la lista local y se registra una advertencia; nadie se queda sin poder cambiar su contraseña por una caída externa.
  • No puede ser predecible (ADR-0070). WeakPasswordPatterns (dominio) rechaza, cada caso con su código:
    • PASSWORD_REPETITIVE: menos de 5 caracteres distintos (ddddddddddddd, abababababab) o un bloque repetido (hola1hola1hola1).
    • PASSWORD_SEQUENTIAL: secuencias de 4 o más del alfabeto, los números o las filas del teclado, en cualquier sentido, que ocupan la mitad o más de la contraseña (123456789012, qwertyuiopas). Un 1234 dentro de una frase larga sí se acepta.
    • PASSWORD_PERSONAL: contiene el correo (lo que va antes de la @ y sus pedazos), el nombre o la marca (trazo, psicol), sin importar mayúsculas ni tildes. No se buscan pedazos de menos de 4 letras. Al cambiarla desde Mi cuenta se usan el correo y el nombre; al recuperarla, solo el correo que trae la petición, porque buscar el nombre antes de validar el pase revelaría datos de la cuenta.
  • Siguiendo la guía del NIST SP 800-63B, no se exigen mayúsculas, números ni símbolos: esas reglas llevan a contraseñas como Clave2026!.
  • Las revisiones van de la más barata a la más cara y cortan en la primera falla (bail): largo, confirmación, patrones y, al final, la filtración. Así no se consulta el servicio en vano.

Recuperación en tres pasos

sequenceDiagram
  participant P as Persona
  participant API
  participant Correo
  P->>API: POST /password/forgot (email)
  API-->>Correo: Código de 6 dígitos (15 min), si la cuenta existe y está activa
  API->>P: 202 siempre
  P->>API: POST /password/verify-code (email, code)
  API->>P: ticket (10 min) y mfa_required
  P->>API: POST /password/reset (ticket, password, mfa_code o recovery_code)
  API->>API: Guarda la contraseña, cierra todas las sesiones y levanta el bloqueo del login
  API-->>Correo: Aviso "Tu contraseña cambió"
  API->>P: 204
  • No revela qué cuentas existen: pedir el código siempre responde 202, y un código de un correo inexistente da el mismo PASSWORD_RESET_CODE_INVALID que uno mal escrito.
  • Código y pase con HMAC-SHA256 y la clave de la aplicación. Con un millón de combinaciones, un hash simple se descifraría con una copia de la base.
  • Una sola solicitud vigente: pedir otro código anula los anteriores. Cinco fallos (del código o del segundo factor) anulan la solicitud y se registran en el log.
  • Segundo factor: si la cuenta tiene MFA, se pide el código de la aplicación o uno de los códigos de recuperación, para quien perdió el teléfono y olvidó la contraseña a la vez. Un código incorrecto responde con attempts_left.
  • Al terminar: se cierran todas las sesiones, se levanta el bloqueo del login para ese correo y llega el aviso por correo.

Cambio desde Mi cuenta

  • Pide la contraseña actual. No vuelve a pedir el MFA: la sesión ya lo pasó.
  • Cierra las demás sesiones y deja abierta la actual. Llega el mismo aviso por correo.

Capas

CapaPiezaQué hace
DomainLockoutPolicyEscalones del bloqueo (lógica pura)
DomainLoginAttemptStoreContrato para llevar fallos y bloqueos
DomainInvalidCredentialsException, AccountLockedException, MfaExceptionsErrores del negocio
DomainMfa/MfaGate, MfaRequirement, LoginStep, RecoveryCodeQué sigue después de la contraseña, la política más estricta y los códigos de recuperación (lógica pura)
ApplicationLoginUser, LogoutUserCasos de uso
ApplicationMfa/VerifyMfa, StartMfaSetup, ConfirmMfaSetup, RegenerateRecoveryCodes, DisableMfa, TotpCheckCasos de uso del MFA y la comprobación de un código sin repetición
ApplicationCredentialVerifier, SessionTokens, SessionUserReader, OneTimePasswords, MfaStore, MfaPolicies, SecurityNotifierPuertos que implementa la infraestructura
InfrastructureEloquentCredentialVerifier, SanctumSessionTokens, CacheLoginAttemptStore, EloquentSessionUserReaderImplementaciones con Eloquent, Sanctum y Redis
InfrastructureGoogle2faOneTimePasswords, EloquentMfaStore, DatabaseMfaPolicies, MailSecurityNotifier, Mail/SecurityNoticeTOTP con la librería, guardado del MFA, política por rol y avisos por correo
DomainPassword/PasswordPolicy, Password/WeakPasswordPatterns, Password/PasswordWeakness, Password/ResetCode, PasswordExceptionsLargo de las contraseñas, patrones predecibles, generación del código y del pase, y errores de la contraseña
ApplicationPassword/RequestPasswordReset, VerifyResetCode, ResetPassword, ChangePasswordCasos de uso de la recuperación y del cambio
ApplicationUserAccounts, PasswordResets, ResetCodeSender, BreachedPasswordsPuertos de la contraseña
InfrastructureEloquentUserAccounts, DatabasePasswordResets, MailResetCodeSender, Mail/PasswordResetCode, HibpBreachedPasswordsCuentas, solicitudes con HMAC, correo del código y revisión de contraseñas filtradas
InfrastructureAuthServiceProviderConexión de los puertos, límites del login, del MFA y de la contraseña, inactividad, alcance del token de un ingreso a medias y morph map
InfrastructureConsole/DevTokenCommandtrazo:dev-token {correo}: un token de sesión sin MFA para el escaneo de ZAP. Solo en desarrollo; en producción no se registra
HttpAuthController, MfaController, LoginRequest, VerifyMfaRequest, MfaCodeRequest, SessionPayload, SessionUserResourceEndpoints, validación de entrada y forma de la respuesta
HttpPasswordController, ForgotPasswordRequest, VerifyResetCodeRequest, ResetPasswordRequest, ChangePasswordRequest, Rules/NewPassword, Rules/NotWeakPassword, Rules/NotBreachedPasswordEndpoints de la contraseña y la regla única de contraseñas nuevas

Dónde está en el código