Skip to main content

Autenticación (Clerk)

Los tres clientes autentican con Clerk, usando el template JWT jwt-oxitec. El backend verifica el token localmente y resuelve rol y permisos contra su propia base de datos.

El camino del token

user_id es el id numérico de nuestra base, y viene del claim que el template inyecta desde el publicMetadata de Clerk.

Un usuario de Clerk sin id numérico autentica pero no funciona

Pasa la verificación del token y después falla en cualquier consulta, con errores opacos. Si un usuario nuevo "entra pero no ve nada", verificá su publicMetadata en Clerk.

Decisión: la autenticación es 100% local

tryClerkJWTAuth no llama a la Backend API de Clerk (users.getUser).

Antes lo hacía, y era redundante: traía user_id y email que ya vienen en el token, más un roleId que nadie consumía. El problema real era otro: cuando esa llamada fallaba o la rate-limiteaban, el middleware descartaba un token válido, caía al fallback de Passport, devolvía 401, y el front hacía signOut(). Se veía como "la web me desloguea de la nada".

No reintroduzcas la llamada a la API de Clerk en el camino de auth

Todo lo que hace falta para autenticar está en el token: firma + template_name. El rol y los permisos salen de nuestra base, por user_id.

Cadena de estrategias

AUTH_TYPE (default CLERK) elige el orden:

  1. Clerk JWT — verificación local (firma + template_name).
  2. Si falla → Passport JWT (JWT_SECRET) como fallback. Si tampoco, responde 401 { success: false, message: "…" }.
  3. AUTH_TYPE=FIREBASE usa Firebase Admin en vez de Clerk.

LOG_JWT_ERRORS=true prende el log verboso de cada paso — es lo primero que hay que activar para depurar un 401 raro.

Rutas públicas

Se listan en la env PUBLIC_ROUTES (coma-separada) y se evalúan con isPublicRoute().

Dos webhooks son públicos por código

/v1/whatsapp/webhook y /v2/email/webhook están hardcodeados como públicos, sin importar PUBLIC_ROUTES. Su autenticidad la valida la firma del webhook, no el middleware de auth.

Cliente web

services/auth.service.ts envuelve @clerk/clerk-js. Cuatro cosas que hay que saber antes de tocarlo:

  • Token cacheado con lock de concurrencia, y un lock separado para el force-refresh post-401.
  • Se refresca 10 s antes del exp: con TTL de 60 s del template quedan ~50 s de uso real.
  • Circuit breaker de rate-limit: ante un 429 de Clerk entra en cooldown de 30 s y sirve el token cacheado, en vez de amplificar el 429.
  • El listener de Clerk limpia el acceso cacheado cuando cambia o se va el usuario.

La política de 401 del interceptor está en Estructura del front.

Cliente mobile

@clerk/clerk-expo con cache en expo-secure-store.

Siempre pasá por services/auth.ts (getValidToken() / handleAuthError()), nunca hooks de Clerk desde código de API. services/authProviderConfig.ts + contexts/AuthBridgeContext.tsx permiten que el backend conmute a Firebase en runtime. lib/clerkErrors.ts traduce los códigos de Clerk a castellano.

Sesión expirada: se tira un error tipado, no se devuelve null

Cuando la sesión venció y no hay token, services/api.ts tira un SessionExpiredError. Devolver null hacía que las mutations "resolvieran" y el call-site crasheara leyendo .savedOffline sobre null.

Ese error no se encola offline: el replay también necesita token.

Credenciales

Solo nombres de variables; los valores viven en el entorno de cada ambiente:

DóndeVariables
BackendCLERK_SECRET_KEY, CLERK_JWT_KEY, CLERK_AUTHORIZED_PARTIES, JWT_SECRET, AUTH_TYPE
Frontenvironment.clerk.publishableKey
MobileEXPO_PUBLIC_CLERK_PUBLISHABLE_KEY, EXPO_PUBLIC_JWT_CLERK_TEMPLATE

Setup y troubleshooting detallados ya escritos en el repo: oxitesa-backend/docs/CLERK_JWT_SETUP.md y docs/TROUBLESHOOTING_JWT.md.