Apariencia
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/signup→verify-email→onboarding) existió hastadeprecar-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) → activeinvited 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
| Token | Scope | Secret | Duración (env) | Sirve para |
|---|---|---|---|---|
| onboarding | onboarding | JWT_ONBOARDING_SECRET | 30m | Solo nombrar la organización (alta delegada) |
| access | full | JWT_ACCESS_SECRET | 15m | Toda la app autenticada |
| refresh | — | JWT_REFRESH_SECRET | 7d | Renovar (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/organizationssinname): define la contraseña pero deja al usuario enpending_onboardingy devuelve unonboardingTokenen 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;
refreshaplica 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-Cookievacía).
Cookie httpOnly del refresh token (admin web)
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.Securesolo en producción (envCOOKIE_SECURE=true, requiere HTTPS).- El mobile (Flutter) ignora la cookie y usa el
refreshTokendel 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(scopeonboarding) NO da acceso a endpointsfull. - 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
organizationIdviaja en el access token, nunca en el body.
Flujo cliente — Admin (React)
Rutas
| Ruta | Tipo | Guard |
|---|---|---|
/login | pública | GuestRoute (si hay sesión → /dashboard) |
/accept-invitation | pública | GuestRoute |
/onboarding | pública | requiere onboardingToken (si no → /login) |
/forgot-password | pública | GuestRoute |
/reset-password | pública | GuestRoute |
/dashboard | protegida | ProtectedRoute (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: sinonboardingTokenen 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 /loginEste 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.
Manejo de tokens en el cliente (cookie httpOnly + memoria)
- 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: truey 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):
SessionRestoredetecta sesión persistida sin access token →POST /auth/refreshcon 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 cuentapending_onboarding).
Flujo cliente — Mobile (Flutter)
Pendiente: se documentará al construir el auth de mobile (incluirá manejo offline).