Flujo de Autenticacion Movil
Autenticacion JWT independiente de Clerk para la app movil (nvito-client) y la PWA (nvito-pwa). Dos tipos de login, tokens con refresh proactivo, y un patron BFF que protege los JWT en cookies encriptadas.
Tabla de Contenidos
- Contexto General
- Login como Anfitrion (HOST)
- Login como Invitado (GUEST)
- Tokens JWT
- Modelo MobileSession
- Refresh Proactivo de Tokens
- Patron BFF en la PWA
- CSRF Double-Submit
- Proxy Catch-All
- Archivos Clave
1. Contexto General
La autenticacion movil de Nvito es completamente independiente de Clerk. Mientras que el panel de administracion (nvito-admin) delega la identidad a Clerk, las aplicaciones orientadas al dia del evento (nvito-client y nvito-pwa) manejan su propia autenticacion con JWT tokens.
La razon es practica: los invitados no tienen cuenta de usuario en Clerk. Acceden a la app a traves de un enlace directo, un codigo QR, o un codigo de acceso proporcionado por el anfitrion.
Existen dos flujos de login:
| Flujo | Usuario | Credenciales | Endpoint |
|---|---|---|---|
| HOST | Anfitrion del evento | EventAccessCode (8 chars) + PIN | POST /v1/mobile/auth/login-host |
| GUEST | Invitado | Access token (via deep link/QR) | POST /v1/mobile/auth/login-guest |
2. Login como Anfitrion (HOST)
El anfitrion recibe un EventAccessCode de 8 caracteres alfanumericos en mayusculas (ej. BODA2024) al crear el evento desde el admin. Ademas, configura un PIN numerico que se hashea con bcrypt en la base de datos.
Proceso paso a paso
- El anfitrion abre la app movil o PWA e ingresa el codigo del evento y su PIN
- La app envia
POST /v1/mobile/auth/login-hostcon{ eventAccessCode, pin } MobileAuthService.loginAsHost()ejecuta:- Busca el evento por
eventAccessCodeen la tablaevents - Verifica que el evento este en estado
ACTIVEoDRAFT - Compara el PIN con
bcrypt.compare(pin, event.hashedPin) - Si la validacion es exitosa, crea o actualiza una
MobileSession - Genera un par de tokens JWT (access + refresh)
- Busca el evento por
- La app almacena los tokens en SecureStore (nativo) o los recibe como cookies HttpOnly (PWA)
Diagrama de secuencia: Login HOST
3. Login como Invitado (GUEST)
El invitado no necesita credenciales manuales. Recibe un enlace directo o codigo QR que contiene un access token unico asociado a su registro de invitado. Este token es un string aleatorio de 32 caracteres almacenado en la tabla guests.
Proceso paso a paso
- El invitado escanea un QR o hace clic en un deep link (
nvito://guest/{accessToken}) - La app envia
POST /v1/mobile/auth/login-guestcon{ accessToken } MobileAuthService.loginAsGuest()ejecuta:- Busca el guest por
accessTokenen la tablaguests - Verifica que el guest no este marcado como
deletedAt - Verifica que el evento asociado este
ACTIVE - Crea o actualiza una
MobileSession - Genera un par de tokens JWT (access + refresh)
- Busca el guest por
- La app almacena los tokens y redirige a la pantalla principal del invitado
Diagrama de secuencia: Login GUEST
4. Tokens JWT
Los tokens JWT de la autenticacion movil tienen caracteristicas especificas que los diferencian de los JWT de Clerk usados en el admin.
Access Token (15 minutos)
| Campo | Valor | Descripcion |
|---|---|---|
sub | eventId | El evento al que esta asociada la sesion |
role | HOST o GUEST | Rol del usuario en el evento |
sessionId | UUID | ID unico de la MobileSession |
userId | UUID (solo HOST) | ID del usuario anfitrion en la tabla users |
guestId | UUID (solo GUEST) | ID del invitado en la tabla guests |
organizationId | UUID | Organizacion propietaria del evento |
exp | timestamp | Expiracion: 15 minutos desde la emision |
iat | timestamp | Momento de emision |
Refresh Token (30 dias)
El refresh token tiene el mismo payload pero con exp de 30 dias. Se almacena hasheado en la tabla MobileSession para poder invalidarlo de forma individual.
5. Modelo MobileSession
Cada login crea o actualiza un registro en la tabla mobile_sessions:
| Campo | Tipo | Descripcion |
|---|---|---|
id | UUID | Identificador unico de la sesion |
eventId | UUID | Evento asociado |
userId | UUID (nullable) | Para sesiones HOST |
guestId | UUID (nullable) | Para sesiones GUEST |
role | Enum | HOST o GUEST |
deviceId | String | Identificador del dispositivo |
refreshTokenHash | String | Hash del refresh token activo |
isActive | Boolean | Si la sesion esta vigente |
lastActivityAt | DateTime | Ultima actividad registrada |
createdAt | DateTime | Momento de creacion |
expiresAt | DateTime | Momento de expiracion (30 dias) |
Invalidacion de sesiones previas
Cuando un usuario inicia sesion desde el mismo dispositivo (deviceId), la sesion previa se invalida automaticamente (isActive = false). Esto previene la acumulacion de sesiones zombi y asegura un comportamiento predecible: un dispositivo, una sesion activa por evento.
6. Refresh Proactivo de Tokens
La app movil implementa un mecanismo de refresh proactivo para evitar interrupciones en la experiencia del usuario.
Funcionamiento
- Al recibir un access token, la app programa un timer que se dispara cuando faltan menos de 2 minutos para la expiracion
- El timer ejecuta
POST /v1/mobile/auth/refreshcon el refresh token - Si el refresh es exitoso, la app recibe un nuevo par de tokens y reprograma el timer
- Si el refresh falla (token expirado o sesion invalidada), se invoca el callback
onUnauthorizedque dispara el logout
En la app nativa (nvito-client)
El API client (src/api/client.ts) maneja el refresh automaticamente:
scheduleTokenRefresh()programa el timer basado en elexpdel JWTclearTokenRefreshTimer()cancela el timer al hacer logout- Los nuevos tokens se sincronizan con
AuthContextvia callbackonTokensRefreshed - Los tokens se persisten en SecureStore (encriptado por el OS)
En la PWA (nvito-pwa)
El refresh es transparente para el browser:
- El BFF proxy detecta cuando el access token esta por expirar (< 2 minutos)
- Automaticamente ejecuta el refresh contra nvito-api
- Actualiza las cookies encriptadas con los nuevos tokens
- El browser no se entera: la respuesta a su peticion original llega normalmente
7. Patron BFF en la PWA
La PWA implementa un patron Backend For Frontend donde los JWT tokens nunca llegan al JavaScript del browser. Esto elimina por completo el riesgo de robo de tokens via XSS.
Las 4 cookies
| Cookie | HttpOnly | Duracion | Path | Proposito |
|---|---|---|---|---|
__Host-nvito-at | Si | 15 min | / | Access token encriptado AES-256-GCM |
__Host-nvito-rt | Si | 30 dias | /api/auth | Refresh token encriptado AES-256-GCM |
__Host-nvito-csrf | No | Session | / | Token CSRF (legible por JS) |
__Host-nvito-csrf-sig | Si | Session | / | Firma HMAC-SHA256 del CSRF token |
El prefijo __Host- garantiza que las cookies solo se envien al dominio exacto, sobre HTTPS, y sin subdominios. Esto es un requisito de seguridad del navegador.
Encriptacion AES-256-GCM
Cada token se encripta con AES-256-GCM antes de almacenarse en la cookie:
- Clave: derivada de
COOKIE_ENCRYPTION_KEY(256 bits) via variable de entorno - IV: 12 bytes aleatorios generados con
crypto.randomBytes()para cada encriptacion - Auth tag: 16 bytes de autenticacion integrada en el cifrado
- Formato:
iv:authTag:ciphertextcodificado en base64
Diagrama del flujo BFF
8. CSRF Double-Submit
Toda mutacion (POST, PATCH, PUT, DELETE) que pasa por el BFF requiere validacion CSRF con el patron double-submit cookie.
Generacion
- Al hacer login, el BFF genera un token CSRF aleatorio de 32 bytes
- Crea una firma HMAC-SHA256 del token usando
CSRF_SECRET - Almacena el token en
__Host-nvito-csrf(legible por JS) - Almacena la firma en
__Host-nvito-csrf-sig(HttpOnly, no legible por JS)
Validacion
- El JavaScript del browser lee
__Host-nvito-csrfy lo incluye como headerx-csrf-token - El BFF recibe la peticion con: cookie CSRF, cookie CSRF-sig, y header x-csrf-token
- Verifica que el header coincida con la cookie (misma persona que recibio la cookie)
- Recalcula HMAC-SHA256 del header y compara con la firma de la cookie
- La comparacion usa
crypto.timingSafeEqual()para prevenir timing attacks
Por que funciona
Un atacante en otro dominio puede provocar que el browser envie las cookies (CSRF clasico), pero no puede leer la cookie __Host-nvito-csrf desde su dominio (Same-Origin Policy). Sin poder leer el token, no puede incluirlo como header, y la peticion falla la validacion.
9. Proxy Catch-All
El BFF expone un route handler catch-all en /api/proxy/[...path]/route.ts que actua como gateway transparente hacia nvito-api.
Pipeline de cada request
- Extraer path:
/api/proxy/events/123/guestsse traduce a/v1/mobile/events/123/guests - Validar CSRF: Solo para metodos mutantes (POST, PATCH, PUT, DELETE)
- Desencriptar JWT: Lee la cookie
__Host-nvito-at, desencripta con AES-256-GCM - Verificar expiracion: Si el JWT expira en < 2 minutos, ejecuta refresh transparente
- Forward: Ejecuta fetch contra nvito-api con
Authorization: Bearer <jwt> - Retornar: Pasa la respuesta del API al browser tal cual
Seguridad adicional
- CSP headers:
connect-src 'self'en la PWA impide que el JavaScript haga fetch directo a nvito-api - Rate limiting: Los endpoints de auth tienen rate limiting in-memory (5 req/IP/15min)
- No CORS: El proxy es same-origin, no necesita headers CORS
10. Archivos Clave
nvito-api (Backend)
| Archivo | Responsabilidad |
|---|---|
mobile-auth.service.ts | Logica de loginAsHost, loginAsGuest, refresh, logout |
mobile-auth.controller.ts | Endpoints REST para autenticacion movil |
mobile-auth.guard.ts | Guard que verifica JWT movil en cada request |
mobile-session.service.ts | CRUD de sesiones moviles, invalidacion |
nvito-client (App Nativa)
| Archivo | Responsabilidad |
|---|---|
src/contexts/AuthContext.tsx | Provider orquestador de autenticacion |
src/contexts/auth/auth-reducer.ts | Reducer puro: estado, acciones, transiciones |
src/contexts/auth/auth-storage.ts | Persistencia de tokens en SecureStore |
src/contexts/auth/use-session-restore.ts | Restaurar sesion al montar la app |
src/api/client.ts | API client con JWT auto-refresh y correlation IDs |
src/api/services/auth.service.ts | Servicio de login, refresh, logout |
nvito-pwa (Progressive Web App)
| Archivo | Responsabilidad |
|---|---|
src/lib/security/crypto.ts | Encriptacion/desencriptacion AES-256-GCM |
src/lib/security/csrf.ts | Generacion y validacion CSRF double-submit |
src/lib/security/rate-limiter.ts | Rate limiting in-memory por IP |
src/lib/bff/cookie-manager.ts | Gestion de las 4 cookies HttpOnly |
src/lib/bff/proxy-handler.ts | Logica del proxy catch-all |
src/app/api/proxy/[...path]/route.ts | Route handler del proxy |
src/app/api/auth/login/route.ts | Route handler de login BFF |