Docs/Architecture/Identity & Access (Keycloak)

Identity & Access (Keycloak)

Keycloak 26 is the platform's identity provider. Every API call into core is authenticated as an OAuth2 resource-server request against realm agentic-ai; tenancy itself is derived from the JWT's tenant_id claim. The subtle part is not OAuth — it is that core must accept tokens from two issuer URLs for the same realm, and that realm import files drift from the live realm.

flowchart TD
  U["User / client"] --> KC["Keycloak 26
realm: agentic-ai"] KC -->|"issues JWT
iss = public or internal URL"| APP["core (OAuth2 resource server)"] APP --> V{"issuer check"} V -->|"KEYCLOAK_ISSUER_URI
(raw/internal)"| OK["validate via JWKS"] V -->|"KEYCLOAK_ADDITIONAL_ISSUERS
auth.example.com/..."| OK V -->|"anything else"| R401["401"] OK --> AZP{"azp in allowed list?"} AZP -->|no| R401 AZP -->|yes| CTX["TenantContext ← tenant_id claim
SecurityContext ← roles + PERM_*"]

1. Realm and clients

Realm agentic-ai (source: deploy/keycloak/agentic-ai-realm.json) registers these clients:

Client IDUsed by
agentic-ai-webThe tenant web app (app.*)
agentic-ai-platform-adminThe super-admin console (admin.*)
agentic-ai-coreCore backend (service-account / confidential flows)
agentic-ai-mobileReserved for an external mobile client (not in this repository)
agentic-ai-appReserved for an external desktop/agent client
agentic-ai-ideReserved for an external IDE client

Identity-provider stubs for GitHub, Google and Microsoft exist in the realm JSON with placeholder client IDs (REPLACE_WITH_*) — they are enabled in the live realm only, one of the known JSON↔live drifts.

2. Roles and authorities

Realm roleScope
tenant-adminFull administration within one tenant
platform-adminCross-tenant super-admin; the only role allowed under /api/platform/**
agent-builderCreate and configure agents/workflows
delegated-userLimited delegated operations
end-customerChannel-facing consumer identity

Beneath the roles sit granular PERM_* authorities enforced in SecurityConfig:

AuthorityGrants
PERM_MANAGE_USERSUser administration within the tenant
PERM_MANAGE_AGENTSAgent lifecycle
PERM_MANAGE_GUARDRAILSGuardrail/policy configuration
PERM_MANAGE_INTEGRATIONSChannel/provider integrations
PERM_MANAGE_APPROVALSHITL approval queues
PERM_VIEW_AUDIT_LOGRead the tenant audit log
PERM_VIEW_USAGEUsage/cost dashboards
PERM_MANAGE_TEMPLATESCommunity template submission/moderation

3. The dual-issuer requirement

Removing KEYCLOAK_ADDITIONAL_ISSUERS 401s everything

Core validates tokens against two issuer URIs for the same realm: KEYCLOAK_ISSUER_URI (the raw/internal issuer — historically localhost:8180 or the container DNS name) and KEYCLOAK_ADDITIONAL_ISSUERS (the public issuer, https://auth.example.com/realms/agentic-ai). Browser-issued tokens carry the public issuer; internal flows carry the raw one. Both must be configured — never "fix" one into the other. If either is missing, that whole class of tokens fails with 401 on every endpoint.

Token validation additionally enforces an allowed-azp list: only tokens issued to the registered clients above are accepted. JWKS is fetched internally from http://keycloak:8080 (compose DNS or the in-cluster Service), not through the public domain: a CDN or WAF in front of auth.<your-domain> may block Python user agents and break the Python services' key fetch.

4. User management and signup

Realm JSON ≠ live realm

deploy/keycloak/agentic-ai-realm.json is a provisioning artifact, not a mirror. Known drift: identity providers (GitHub/Google/ Microsoft) are enabled in the live realm but carry placeholder IDs in the JSON. Trust the live realm for "what is configured"; trust the JSON for "what a fresh environment gets" — and reconcile after imports.

5. Security configuration

6. Failure modes quick table

SymptomRoot cause to check first
Every request 401s after a deploy/env changeMissing or wrong KEYCLOAK_ADDITIONAL_ISSUERS (or primary issuer rewritten to the public URL)
401 for one client type onlyClient's azp not in the allowed-azp list
Python service can't start / can't validate tokensJWKS pointed at the public domain and blocked by an edge proxy; use the internal Keycloak URL
User exists but lands in public schema / sees no datatenant_id attribute dropped — check unmanagedAttributePolicy on the realm
/api/platform/** 403 for an admin userUser has tenant-admin, not the platform-admin realm role