Docs

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

  1. Contexto General
  2. Login como Anfitrion (HOST)
  3. Login como Invitado (GUEST)
  4. Tokens JWT
  5. Modelo MobileSession
  6. Refresh Proactivo de Tokens
  7. Patron BFF en la PWA
  8. CSRF Double-Submit
  9. Proxy Catch-All
  10. 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:

FlujoUsuarioCredencialesEndpoint
HOSTAnfitrion del eventoEventAccessCode (8 chars) + PINPOST /v1/mobile/auth/login-host
GUESTInvitadoAccess 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

  1. El anfitrion abre la app movil o PWA e ingresa el codigo del evento y su PIN
  2. La app envia POST /v1/mobile/auth/login-host con { eventAccessCode, pin }
  3. MobileAuthService.loginAsHost() ejecuta:
    • Busca el evento por eventAccessCode en la tabla events
    • Verifica que el evento este en estado ACTIVE o DRAFT
    • 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)
  4. 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

  1. El invitado escanea un QR o hace clic en un deep link (nvito://guest/{accessToken})
  2. La app envia POST /v1/mobile/auth/login-guest con { accessToken }
  3. MobileAuthService.loginAsGuest() ejecuta:
    • Busca el guest por accessToken en la tabla guests
    • 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)
  4. 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)

CampoValorDescripcion
subeventIdEl evento al que esta asociada la sesion
roleHOST o GUESTRol del usuario en el evento
sessionIdUUIDID unico de la MobileSession
userIdUUID (solo HOST)ID del usuario anfitrion en la tabla users
guestIdUUID (solo GUEST)ID del invitado en la tabla guests
organizationIdUUIDOrganizacion propietaria del evento
exptimestampExpiracion: 15 minutos desde la emision
iattimestampMomento 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:

CampoTipoDescripcion
idUUIDIdentificador unico de la sesion
eventIdUUIDEvento asociado
userIdUUID (nullable)Para sesiones HOST
guestIdUUID (nullable)Para sesiones GUEST
roleEnumHOST o GUEST
deviceIdStringIdentificador del dispositivo
refreshTokenHashStringHash del refresh token activo
isActiveBooleanSi la sesion esta vigente
lastActivityAtDateTimeUltima actividad registrada
createdAtDateTimeMomento de creacion
expiresAtDateTimeMomento 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

  1. Al recibir un access token, la app programa un timer que se dispara cuando faltan menos de 2 minutos para la expiracion
  2. El timer ejecuta POST /v1/mobile/auth/refresh con el refresh token
  3. Si el refresh es exitoso, la app recibe un nuevo par de tokens y reprograma el timer
  4. Si el refresh falla (token expirado o sesion invalidada), se invoca el callback onUnauthorized que 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 el exp del JWT
  • clearTokenRefreshTimer() cancela el timer al hacer logout
  • Los nuevos tokens se sincronizan con AuthContext via callback onTokensRefreshed
  • 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

CookieHttpOnlyDuracionPathProposito
__Host-nvito-atSi15 min/Access token encriptado AES-256-GCM
__Host-nvito-rtSi30 dias/api/authRefresh token encriptado AES-256-GCM
__Host-nvito-csrfNoSession/Token CSRF (legible por JS)
__Host-nvito-csrf-sigSiSession/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:ciphertext codificado 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

  1. Al hacer login, el BFF genera un token CSRF aleatorio de 32 bytes
  2. Crea una firma HMAC-SHA256 del token usando CSRF_SECRET
  3. Almacena el token en __Host-nvito-csrf (legible por JS)
  4. Almacena la firma en __Host-nvito-csrf-sig (HttpOnly, no legible por JS)

Validacion

  1. El JavaScript del browser lee __Host-nvito-csrf y lo incluye como header x-csrf-token
  2. El BFF recibe la peticion con: cookie CSRF, cookie CSRF-sig, y header x-csrf-token
  3. Verifica que el header coincida con la cookie (misma persona que recibio la cookie)
  4. Recalcula HMAC-SHA256 del header y compara con la firma de la cookie
  5. 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

  1. Extraer path: /api/proxy/events/123/guests se traduce a /v1/mobile/events/123/guests
  2. Validar CSRF: Solo para metodos mutantes (POST, PATCH, PUT, DELETE)
  3. Desencriptar JWT: Lee la cookie __Host-nvito-at, desencripta con AES-256-GCM
  4. Verificar expiracion: Si el JWT expira en < 2 minutos, ejecuta refresh transparente
  5. Forward: Ejecuta fetch contra nvito-api con Authorization: Bearer <jwt>
  6. 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)

ArchivoResponsabilidad
mobile-auth.service.tsLogica de loginAsHost, loginAsGuest, refresh, logout
mobile-auth.controller.tsEndpoints REST para autenticacion movil
mobile-auth.guard.tsGuard que verifica JWT movil en cada request
mobile-session.service.tsCRUD de sesiones moviles, invalidacion

nvito-client (App Nativa)

ArchivoResponsabilidad
src/contexts/AuthContext.tsxProvider orquestador de autenticacion
src/contexts/auth/auth-reducer.tsReducer puro: estado, acciones, transiciones
src/contexts/auth/auth-storage.tsPersistencia de tokens en SecureStore
src/contexts/auth/use-session-restore.tsRestaurar sesion al montar la app
src/api/client.tsAPI client con JWT auto-refresh y correlation IDs
src/api/services/auth.service.tsServicio de login, refresh, logout

nvito-pwa (Progressive Web App)

ArchivoResponsabilidad
src/lib/security/crypto.tsEncriptacion/desencriptacion AES-256-GCM
src/lib/security/csrf.tsGeneracion y validacion CSRF double-submit
src/lib/security/rate-limiter.tsRate limiting in-memory por IP
src/lib/bff/cookie-manager.tsGestion de las 4 cookies HttpOnly
src/lib/bff/proxy-handler.tsLogica del proxy catch-all
src/app/api/proxy/[...path]/route.tsRoute handler del proxy
src/app/api/auth/login/route.tsRoute handler de login BFF
Esta pagina fue util?