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.