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.
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".
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:
- Clerk JWT — verificación local (firma +
template_name). - Si falla → Passport JWT (
JWT_SECRET) como fallback. Si tampoco, responde401 { success: false, message: "…" }. AUTH_TYPE=FIREBASEusa 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().
/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.
nullCuando 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ónde | Variables |
|---|---|
| Backend | CLERK_SECRET_KEY, CLERK_JWT_KEY, CLERK_AUTHORIZED_PARTIES, JWT_SECRET, AUTH_TYPE |
| Front | environment.clerk.publishableKey |
| Mobile | EXPO_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.