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 ID | Used by |
|---|---|
agentic-ai-web | The tenant web app (app.*) |
agentic-ai-platform-admin | The super-admin console (admin.*) |
agentic-ai-core | Core backend (service-account / confidential flows) |
agentic-ai-mobile | Reserved for an external mobile client (not in this repository) |
agentic-ai-app | Reserved for an external desktop/agent client |
agentic-ai-ide | Reserved 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 role | Scope |
|---|---|
tenant-admin | Full administration within one tenant |
platform-admin | Cross-tenant super-admin; the only role allowed under /api/platform/** |
agent-builder | Create and configure agents/workflows |
delegated-user | Limited delegated operations |
end-customer | Channel-facing consumer identity |
Beneath the roles sit granular PERM_* authorities enforced in
SecurityConfig:
| Authority | Grants |
|---|---|
PERM_MANAGE_USERS | User administration within the tenant |
PERM_MANAGE_AGENTS | Agent lifecycle |
PERM_MANAGE_GUARDRAILS | Guardrail/policy configuration |
PERM_MANAGE_INTEGRATIONS | Channel/provider integrations |
PERM_MANAGE_APPROVALS | HITL approval queues |
PERM_VIEW_AUDIT_LOG | Read the tenant audit log |
PERM_VIEW_USAGE | Usage/cost dashboards |
PERM_MANAGE_TEMPLATES | Community template submission/moderation |
3. The dual-issuer requirement
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
- Signup:
/api/signupcreates the Keycloak user and provisions the tenant schema in one flow. - Admin operations: user management goes through the Keycloak Admin REST API via
KEYCLOAK_ADMIN_BASE_URL(core/adminmodule's KeycloakAdminClient). unmanagedAttributePolicy=ADMIN_EDITis mandatory. Without it, Keycloak drops non-mapped user attributes — includingtenant_id— and every token for that user silently loses its tenant claim, which then breaks tenant resolution downstream.- Post-import reconcile scripts exist because realm imports do not fully reproduce the live realm — run them after any realm re-import.
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
SecurityConfigruns under profile!insecure— theinsecureprofile exists for local/test use and must never reach an environment.- The app fails closed: with neither the secure nor the insecure profile, authentication is still enforced — there is no configuration mistake that silently opens the API.
- Route-level rules include
/api/platform/** → ROLE_platform-admin; webhook paths are opened per-channel with their own signature validation instead of JWT auth.
6. Failure modes quick table
| Symptom | Root cause to check first |
|---|---|
| Every request 401s after a deploy/env change | Missing or wrong KEYCLOAK_ADDITIONAL_ISSUERS (or primary issuer rewritten to the public URL) |
| 401 for one client type only | Client's azp not in the allowed-azp list |
| Python service can't start / can't validate tokens | JWKS 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 data | tenant_id attribute dropped — check unmanagedAttributePolicy on the realm |
/api/platform/** 403 for an admin user | User has tenant-admin, not the platform-admin realm role |