Skip to content

Flujo de Autenticación

Base URL: /api/v1 Implementado en: camaroneras_backend (módulo src/modules/auth)

Requests y responses 2xx: bodies (tipos, opcionales, enums) y el shape de las respuestas exitosas están en la API Reference, generada desde el spec OpenAPI. Acá quedan solo las respuestas de error (4xx/5xx) y las reglas de negocio/flujo.

Resumen

Sin auto-registro público: toda cuenta nace por invitación (de tenant o de plataforma — ver plataforma/rbac-flow.md y plataforma/superadmin-flow.md). La única alta que queda en dos pasos es la delegada: el superadmin invita sin nombrar la empresa y el propio cliente la nombra al aceptar, vía un token temporal de scope limitado. El resto de la sesión usa JWT access + refresh revocable.

El signup público (POST /auth/signupverify-emailonboarding) existió hasta deprecar-signup-publico; ver ADR-0013 (superseded) y ADR-0050 para el detalle de la decisión.

Estados del usuario

accept-invitation (con membresía)     → active
accept-invitation (alta delegada)     → pending_onboarding
onboarding (nombra la organización)   → active

invited es el estado previo a aceptar cualquier invitación (de tenant, alta directa de plataforma o alta delegada). pending_verification es un estado del enum sin productor (era el paso 1 del signup retirado) — se conserva en el tipo por no forzar una migración destructiva, pero ningún flujo actual lo produce.

Tipos de token

TokenScopeSecretDuración (env)Sirve para
onboardingonboardingJWT_ONBOARDING_SECRET30mSolo nombrar la organización (alta delegada)
accessfullJWT_ACCESS_SECRET15mToda la app autenticada
refreshJWT_REFRESH_SECRET7dRenovar (guardado hasheado en BD, rota)

Alta de cuentas (por invitación)

Toda cuenta nueva entra por POST /auth/accept-invitation (ver plataforma/rbac-flow.md para el detalle del magic link e invitación de tenant, y plataforma/superadmin-flow.md para el alta de plataforma). Ese endpoint bifurca según si el invitado ya tiene organización:

  • Con membresía (invitación de tenant, o alta directa de plataforma con name): activa la cuenta y devuelve sesión completa.
  • Sin membresía (alta delegada de plataforma, POST /platform/organizations sin name): define la contraseña pero deja al usuario en pending_onboarding y devuelve un onboardingToken en vez de sesión.

Onboarding — nombrar la organización (alta delegada)

POST /auth/onboarding — Bearer onboardingToken

  • Requiere user en pending_onboarding.
  • Transacción: crea Organization + Role "Administrador" + vínculo OrganizationUser, activa el user.
  • Emite access + refresh token (sesión completa).

Sesión

POST /auth/signin — público (respuesta polimórfica)

  • Verifica credenciales (argon2). Si la contraseña es incorrecta o el email no existe → 401.
  • Si la cuenta está en pending_onboarding (alta delegada sin completar), reanuda: devuelve el estado y un onboardingToken nuevo (no filtra info porque exige contraseña correcta).
  • Si la membresía o la organización están desactivadas → 401 (la desactivación de una org desde la plataforma bloquea a sus usuarios; refresh aplica lo mismo).

POST /auth/refresh — público (refresh por cookie httpOnly O body)

Body opcional (refreshToken — mobile/Postman envían el token; el admin web manda {} y usa la cookie).

  • Fuente del token: body si viene; si no, cookie refresh_token. Sin ninguno → 401.
  • Verifica el refresh token contra la BD (hasheado, no revocado, no expirado).
  • Rota: revoca el anterior, emite uno nuevo y re-setea la cookie.

POST /auth/signout — Bearer accessToken

Body opcional (refreshToken — igual que refresh: cookie o body).

  • Revoca el refresh token en BD y limpia la cookie (Set-Cookie vacía).

Los endpoints que emiten sesión (signin activo, onboarding, accept-invitation, refresh) además del body envían Set-Cookie:

refresh_token=<jwt>; HttpOnly; SameSite=Lax; Path=/api/v1/auth; Max-Age=604800[; Secure]
  • HttpOnly: JavaScript no puede leerla (mitiga robo por XSS).
  • Path=/api/v1/auth: solo viaja a los endpoints de auth.
  • Secure solo en producción (env COOKIE_SECURE=true, requiere HTTPS).
  • El mobile (Flutter) ignora la cookie y usa el refreshToken del body.

GET /auth/me — Bearer accessToken

  • Devuelve el usuario autenticado con su rol y organización.

Recuperación de contraseña

POST /auth/forgot-password — público

  • Solo envía el código (6 dígitos, expira 5 min) si la cuenta existe y está active.

POST /auth/reset-password — público

  • Valida código (no expirado, correcto, máx 5 intentos). Password 8-16 caracteres.
  • Tras el cambio, revoca todas las sesiones activas (refresh tokens) del usuario.
  • Errores → 400 con mensaje genérico "Código inválido o expirado" (no filtra detalles).

Seguridad

  • Contraseñas y refresh tokens hasheados con argon2.
  • Códigos de verificación hasheados en BD.
  • Secrets JWT separados por tipo de token (un token no sirve donde no debe).
  • El onboardingToken (scope onboarding) NO da acceso a endpoints full.
  • Refresh tokens revocables (logout real) y con rotación.
  • Admin web: ningún token se persiste en localStorage — refresh en cookie httpOnly, access solo en memoria (un XSS no tiene nada que robar del storage).

Notas

  • Email en desarrollo: Mailtrap (sandbox). Los códigos (reset) y magic links (invitación) llegan al inbox de Mailtrap, no a un correo real.
  • Multi-tenant: el organizationId viaja en el access token, nunca en el body.

Flujo cliente — Admin (React)

Rutas

RutaTipoGuard
/loginpúblicaGuestRoute (si hay sesión → /dashboard)
/accept-invitationpúblicaGuestRoute
/onboardingpúblicarequiere onboardingToken (si no → /login)
/forgot-passwordpúblicaGuestRoute
/reset-passwordpúblicaGuestRoute
/dashboardprotegidaProtectedRoute (si no hay sesión → /login)

Sin auto-registro: no hay ruta pública que cree una cuenta desde cero. /login no ofrece link de registro — quien no tiene cuenta debe pedir una invitación al administrador de su organización.

Flujo de recuperación de contraseña (admin)

  • Link "¿Olvidaste tu contraseña?" en /login/forgot-password.
  • /forgot-password: pide email → respuesta neutra → navega a /reset-password.
  • /reset-password: OTP (6 dígitos) + nueva contraseña + confirmación → éxito → /login.
  • Reenvío de código con cooldown.

Guards

  • ProtectedRoute: sin sesión completa (access token + user) → redirige a /login.
  • GuestRoute: con sesión completa → redirige a /dashboard (evita ver login logueado).
  • /onboarding: sin onboardingToken en el store → redirige a /login.

Redirecciones por estado (tras signin)

El signin devuelve status. El admin redirige según el caso:

status = active             → guardar sesión → /dashboard
status = pending_onboarding → guardar onboardingToken → /onboarding
401                         → mostrar error, queda en /login

Este camino (pending_onboarding → nuevo onboardingToken/onboarding) es lo que recupera a un invitado delegado que pierde su onboardingToken en memoria antes de completar el registro (ej. recarga de página): inicia sesión con la contraseña que ya definió en accept-invitation y el signin lo devuelve directo a /onboarding con un token nuevo — no hizo falta tocar SignInPage para este caso.

  • Access token: SOLO en memoria (Zustand sin persist). No toca localStorage.
  • Refresh token: el cliente nunca lo ve — vive en la cookie httpOnly; Axios usa withCredentials: true y el navegador la adjunta solo en /auth/*.
  • Persistido (localStorage): únicamente datos de UI (user, organization, permissions, clients, onboardingToken). Jamás tokens de sesión.
  • Silent refresh al recargar (F5): SessionRestore detecta sesión persistida sin access token → POST /auth/refresh con la cookie → repone el access en memoria y recién entonces renderiza las rutas. Si falla (cookie expirada/revocada) → limpia → /login.

Estados límite

  • Onboarding token expirado (401 en /onboarding): el interceptor de Axios NO intenta refrescar ni limpia la sesión para este endpoint. El usuario puede reanudar volviendo a /login (el signin reemite el token).
  • Sesión perdida / access token expirado en endpoints full: el interceptor hace auto-refresh (cookie); si el refresh falla, limpia la sesión → /login.
  • Alta delegada abandonada: el usuario retoma con /login (signin detecta la cuenta pending_onboarding).

Flujo cliente — Mobile (Flutter)

Pendiente: se documentará al construir el auth de mobile (incluirá manejo offline).