1. Contexto General
La autenticación movil de Nvito es completamente independiente de Clerk. Mientras que el panel de administración (nvito-admin) delega la identidad a Clerk, las aplicaciones orientadas al día del evento (nvito-client y nvito-pwa) manejan su propia autenticación 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 código QR, o un código 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. Además, 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 código 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 validación 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
Login como Anfitrion (HOST)
3. Login como Invitado (GUEST)
El invitado no necesita credenciales manuales. Recibe un enlace directo o código QR que contiene un access token único 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
Login como Invitado (GUEST)
4. Tokens JWT
Los tokens JWT de la autenticación movil tienen caracteristicas específicas que los diferencian de los JWT de Clerk usados en el admin.
Access Token (15 minutos)
| Campo | Valor | Descripción |
|---|---|---|
sub | eventId | El evento al que está asociada la sesion |
role | HOST o GUEST | Rol del usuario en el evento |
sessionId | UUID | ID único 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 | Organización 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 días. 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 | Descripción |
|---|---|---|
id | UUID | Identificador único 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 está vigente |
lastActivityAt | DateTime | Ultima actividad registrada |
createdAt | DateTime | Momento de creación |
expiresAt | DateTime | Momento de expiración (30 dias) |
Invalidación de sesiones previas
Cuando un usuario inicia sesion desde el mismo dispositivo (deviceId), la sesion previa se invalida automáticamente (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 expiración
- 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 automáticamente:
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 está por expirar (< 2 minutos)
- Automáticamente 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 patrón 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 días | /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 autenticación integrada en el cifrado
- Formato:
iv:authTag:ciphertextcodificado en base64
Diagrama del flujo BFF
Patron BFF en la PWA
8. CSRF Double-Submit
Toda mutacion (POST, PATCH, PUT, DELETE) que pasa por el BFF requiere validación CSRF con el patrón double-submit cookie.
Generación
- 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)
Validación
- 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 validación.
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 métodos mutantes (POST, PATCH, PUT, DELETE)
- Desencriptar JWT: Lee la cookie
__Host-nvito-at, desencripta con AES-256-GCM - Verificar expiración: 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 | Lógica de loginAsHost, loginAsGuest, refresh, logout |
mobile-auth.controller.ts | Endpoints REST para autenticación movil |
mobile-auth.guard.ts | Guard que verifica JWT movil en cada request |
mobile-session.service.ts | CRUD de sesiones moviles, invalidación |
nvito-client (App Nativa)
| Archivo | Responsabilidad |
|---|---|
src/contexts/AuthContext.tsx | Provider orquestador de autenticación |
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 | Generación y validación CSRF double-submit |
src/lib/security/rate-limiter.ts | Rate limiting in-memory por IP |
src/lib/bff/cookie-manager.ts | Gestión de las 4 cookies HttpOnly |
src/lib/bff/proxy-handler.ts | Lógica 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 |