Skip to main content

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.

Firebase auth fue removido

Si ves código de Firebase auth en el repo, es resto viejo. La autenticación es Ajolote.

El flujo​

Frontend​

  1. AjoloteAuthService tiene la sesión (un nanostore de better-auth expuesto como signals: session(), isPending). Se configura en app.config.ts con { baseURL, appId }.
  2. authInterceptor espera whenSessionResolved() y, con sesión activa, llama ajolote.getValidToken() (que refresca el JWT si está por expirar) y adjunta Authorization: Bearer <token>.
  3. AuthService envuelve login / logout y handleSessionExpired() (toast + redirect a login).
  4. Después del sign-in, PermissionService carga GET /role/allow. Ver Permisos y roles.

Backend​

  • El JWT lo valida passport; AUTH_SERVICE_URL apunta 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 el appId del login en user.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.

Decisión de diseño

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​

El orden del catchError en el authInterceptor

El 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/allow desde 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.
  • AjoloteAuthService se provee con un factory explícito en app.config.ts: el paquete se compila sin AOT, así que su providedIn: '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: false con HTTP 200 para errores de negocio. Ver Contrato de respuesta.