Updated: 2026-01-05 Audience: Platform & support engineers Goal: Let each tenant authenticate against its own IdP while keeping a single Eneo deployment.
This guide applies when FEDERATION_PER_TENANT_ENABLED=true. Use this mode if:
- Multi-tenant: Each tenant needs their own IdP (Entra ID, Okta, Auth0, etc.)
- Single-tenant with API management: You want to manage OIDC via API instead of environment variables (changes take effect without restart)
Not using federation? For simple single-tenant deployments, keep FEDERATION_PER_TENANT_ENABLED=false and configure OIDC via environment variables (OIDC_DISCOVERY_ENDPOINT, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET). See the deployment templates.
FEDERATION_PER_TENANT_ENABLED=truein backend.env- Super admin API key available (needed for every
/api/v1/sysadmin/...call) - Redis running (recommended for the debug toggle; file fallback works)
- Each tenant has a slug (
tenant.slug). Backfill once with:cd backend uv run python -m intric.cli.backfill_tenant_slugs
Terminology
- tenant_id – UUID in admin APIs (e.g.,
123e4567-e89b-12d3-a456-426614174000) - tenant slug – URL-safe label exposed to the frontend (e.g.,
examplea)
-
Provide a new federation config
curl -X PUT "https://api.eneo.local/api/v1/sysadmin/tenants/{tenant_id}/federation" \ -H "X-API-Key: {SUPER_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "provider": "entra_id", "canonical_public_origin": "https://examplea.eneo.local", "discovery_endpoint": "https://login.microsoftonline.com/{azure-tenant-id}/v2.0/.well-known/openid-configuration", "client_id": "{azure-client-id}", "client_secret": "{azure-client-secret}", "allowed_domains": ["examplea.gov"], "scopes": ["openid", "email", "profile"] }'
-
Register redirect URI with the IdP (must match
canonical_public_origin):
https://examplea.eneo.local/auth/callback -
Smoke test the configuration (dry run; no user interaction):
curl -X POST \ -H "X-API-Key: {SUPER_API_KEY}" \ https://api.eneo.local/api/v1/sysadmin/tenants/{tenant_id}/federation/test
Repeat for each tenant (e.g., MunicipalityExampleB with Auth0, MunicipalityExampleC with Okta). Only the IdP-specific fields change.
To update the current setup later without resending every field, use PATCH /api/v1/sysadmin/tenants/{tenant_id}/federation. Omitted fields stay unchanged:
curl -X PATCH "https://api.eneo.local/api/v1/sysadmin/tenants/{tenant_id}/federation" \
-H "X-API-Key: {SUPER_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"allowed_domains": ["examplea.gov", "examplea.se"]
}'Use PUT when you want to provide a full new federation definition or replace the current one.
Use this flow when a tenant reports “login failed”. Everything happens on the backend; no code redeploy needed.
curl -X POST https://api.eneo.local/api/v1/sysadmin/observability/oidc-debug/ \
-H "X-API-Key: {SUPER_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"enabled": true, "duration_minutes": 10, "reason": "Case #452"}'- Auto-expires after the requested duration (max 120 minutes)
- Stored in Redis (
observability:oidc_debug) or/app/data/debug_flags/oidc_debug.jsonfallback
Check status:
GET /api/v1/sysadmin/observability/oidc-debug/
Ask the user for the correlationId surfaced in the UI. Filter server logs:
journalctl -u backend.service -o cat | jq 'select(.correlation_id=="fa4fdb8fdbeee426")'| Breadcrumb (log message) | Typical cause | Next action |
|---|---|---|
callback.domain_rejected |
Email domain not in tenant allowed_domains |
Add domain or ask user to use approved account |
callback.user_missing |
Email not found in tenant user table | Invite/import the user for that tenant |
callback.user_tenant_mismatch |
User exists but belongs to another tenant | Move user to correct tenant or adjust login link |
callback.state_cache_error |
Redis unavailable during login | Check Redis health; retry once cache is restored |
initiate.state_cache_failed |
Redis write failed during initiate | Investigate Redis/file permissions, then retry |
Turn off logging once the incident is resolved:
curl -X POST https://api.eneo.local/api/v1/sysadmin/observability/oidc-debug/ \
-H "X-API-Key: {SUPER_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"enabled": false, "reason": "Case #452 closed"}'- Report comes in → enable debug toggle (10 min window)
- Reproduce or ask user to retry → capture
correlationId - Filter logs → identify breadcrumb from table above
- Fix config
- Add tenant credential (
/sysadmin/tenants/{id}/credentials/...) if LLM access is missing - Adjust federation settings (
/federationendpoints)
- Add tenant credential (
- Disable toggle → confirm resolution with tenant
FEDERATION_PER_TENANT.md– architecture & migration notes- AI Providers – provider configuration & credential management
TROUBLESHOOTING.md– incident catalogue