Autenticación — Ajolote SSO
Ajolote es el proveedor de identidad de la organización (paquete @cuatro-quinas/ajolote-client-angular,
construido sobre better-auth). Sign-in por email + password, JWT por request.
Si ves código de Firebase auth en el repo, es resto viejo. La autenticación es Ajolote.
El flujo
Frontend
AjoloteAuthServicetiene la sesión (un nanostore de better-auth expuesto como signals:session(),isPending). Se configura enapp.config.tscon{ baseURL, appId }.authInterceptoresperawhenSessionResolved()y, con sesión activa, llamaajolote.getValidToken()(que refresca el JWT si está por expirar) y adjuntaAuthorization: Bearer <token>.AuthServiceenvuelve login / logout yhandleSessionExpired()(toast + redirect a login).- Después del sign-in,
PermissionServicecargaGET /role/allow. Ver Permisos y roles.
Backend
- El JWT lo valida passport;
AUTH_SERVICE_URLapunta a Ajolote. - Las rutas públicas se declaran en la variable de entorno
PUBLIC_ROUTES. - JIT-provisioning: el usuario se crea en Proteus en su primer request autenticado
(
AccessService.findOrCreateFromAjolote), persistiendo elappIddel login enuser.company. No hay alta manual de usuarios como paso previo. - La metadata del usuario se cachea en Redis (
ACCESSREG-<id>,USERSCOPE-<id>). - Los roles se siembran en Ajolote con un script de seed.
Multi-tenant: el front no conoce los tenants
El frontend manda X-App-Id = 'proteus' y Ajolote resuelve el tenant server-side; el tenant queda
pinneado en una cookie httpOnly.
El front nunca conoce la lista de tenants ni el slug elegido. Se eligió un enfoque tipo BFF: el backend
resuelve el tenant por email contra una lista privada y devuelve solo el appId necesario. La lista no se
expone.
Del lado de Proteus, el appId persistido en user.company es lo que después alimenta el filtro por
empresa. Ver Multi-tenant.
Gotchas
catchError en el authInterceptorEl catchError va antes del switchMap a propósito: así solo captura el fallo de
getValidToken(), o sea una sesión realmente irrecuperable. Un 401 o 500 del backend downstream no debe
desloguear. Si lo reordenás, cualquier error de la API expulsa al usuario.
- Sin
whenSessionResolved(), el primer request del arranque (típicamente/role/allowdesde el componente raíz) sale sin token y da un 401 espurio en ese único endpoint. - Un 403 es falta de permisos, no sesión vencida. No desloguea.
AjoloteAuthServicese provee con un factory explícito enapp.config.ts: el paquete se compila sin AOT, así que suprovidedIn: 'root'no trae metadata y Angular pediría el compilador JIT en runtime.- Al cerrar sesión hay que limpiar el centro médico activo y los permisos, o se arrastran al usuario siguiente. Ver Centro médico activo.
- Un 401 genuino sí significa sesión inválida, porque el backend usa 500 para errores internos y
success: falsecon HTTP 200 para errores de negocio. Ver Contrato de respuesta.