Authentication & Authorization
- Default scheme
Authorization: Bearer <access-token>— a Keycloak-issued JWT, validated by Spring's OAuth2 resource server inSecurityConfig.- Token issuer
- Keycloak at
https://auth.example.com, realmagentic-ai. - Stateless
- No sessions, CSRF disabled (
SessionCreationPolicy.STATELESS); the API never sets auth cookies. - Fail-closed
- The secure chain is
@Profile("!insecure"). Missing profile configuration fails startup instead of serving an open API.
Credential types at a glance
| Credential | Header | Used by | Verified where |
|---|---|---|---|
| Keycloak JWT | Authorization: Bearer <token> | Web console, admin console, other registered OAuth clients, external callers | Spring resource server (SecurityConfig.jwtDecoder) |
| Demo sandbox token | X-Demo-Token | Anonymous /api/demo/** sandbox and /try | DemoSessionController / DemoChatController (hash lookup; minted by POST /api/demo/session) |
| Device token | X-Device-Token (pairing code for claim) | Paired phones and desktop daemons | DeviceGatewayController, DeviceAuthResolver |
| MCP / developer API key | Authorization: Bearer vmcp_<key> | /mcp/** and /api/dev/** | McpApiKeyAuthFilter. See Developer API. |
| Share id | None (id in the path) | /api/share/agents/**, /api/share/workflows/** | Controller resolves the id; rate-limited, revocable |
| Provider signature / secret | Varies | /webhook/**, /api/billing/webhook/** | Per controller. See Webhooks. |
Obtaining a token
Browsers and clients sign in through Keycloak (OIDC). For scripted access the
repository's Postman collection uses Keycloak's standard token endpoint,
POST https://auth.example.com/realms/agentic-ai/protocol/openid-connect/token,
form-encoded, in two shapes: a password grant
(grant_type, client_id, client_secret, username,
password) and a service-account grant (grant_type, client_id,
client_secret). Which Keycloak clients permit which grant is realm configuration; see
Keycloak Provisioning. Never paste client
secrets or tokens into shared docs or tickets; use <token> placeholders.
curl -H "Authorization: Bearer <token>" https://api.example.com/api/agents
What a token must satisfy
- Signature against the JWKS at
app.keycloak.jwk-set-uri(container-reachable, deliberately separate from the issuer). - Default JWT validation (timestamps) via
JwtValidators. - Issuer must be the primary
issuer-urior one of the comma-separatedKEYCLOAK_ADDITIONAL_ISSUERS. See ADR-0005. A missing additional issuer 401s every call while browser logins still look fine. - Authorized party (
azp) must be one ofapp.keycloak.allowed-azp. Default:agentic-ai-web,agentic-ai-platform-admin,agentic-ai-mobile,agentic-ai-app,agentic-ai-ide,agentic-ai-core. A client that serves open self-registration for a separate product should deliberately be left out. Setting the property empty disables the check (an emergency lever). A token with noazpis not rejected by this validator. - Tenant claim.
TenantContextFilterreadstenant_id; an authenticated JWT with none gets403with a JSON body saying the account is not linked to an organization (platform-level accounts are tenant-less by design). A tenant whose provisioning never completed also gets403.
sequenceDiagram participant C as Client participant K as Keycloak participant A as core SecurityConfig participant F as TenantContextFilter participant X as Controller C->>K: token request K-->>C: JWT (iss, azp, tenant_id, realm_access.roles) C->>A: request + Bearer JWT A->>A: signature, timestamps, iss in accepted set, azp allowed A->>A: authorities = ROLE_* + PERM_* (GranularPermissionConverter) A->>A: URL matcher (permitAll / hasAuthority / authenticated) A->>F: tenant_id to TenantContext F->>X: handler runs in tenant schema X->>X: resource check (requireAccess VIEWER / RUNNER)
Authorization model
Layer 1: authorities
GranularPermissionConverter emits two kinds of authority for every JWT:
ROLE_<name> from the token's realm_access.roles
(KeycloakRoleConverter), and PERM_<SCOPE> from the caller's stored
user_permission_grant rows via PermissionAuthoritySource. Grants bind
tenant-admins too: the role seeds a full bundle, it is not a bypass.
Layer 2: URL matchers (first match wins)
| Path (verb) | Requirement |
|---|---|
/api/platform/** | ROLE_platform-admin |
/api/admin/users/**; other /api/admin/** (catch-all) | PERM_MANAGE_USERS |
/api/admin/templates/**; /api/admin/issues/**; /api/admin/llm-usage/**, /spend-reports/** | PERM_MANAGE_TEMPLATES; PERM_VIEW_AUDIT_LOG; PERM_VIEW_USAGE |
/api/audit-log/** | PERM_VIEW_AUDIT_LOG |
Writes to /api/providers, /api/mcp/registry, /api/mcpserver/grants, /api/webhooks, /api/knowledge/connectors, /api/sdlc/**, /api/jev, /api/capabilities, /api/sarvam/credential | PERM_MANAGE_INTEGRATIONS |
Channel credentials, defaults, pairing, contacts, routing (/api/channels/** writes), /api/elevenlabs/credential | PERM_MANAGE_CHANNELS |
Workflow node webhooks and schedules (/api/workflows/*/nodes/*/webhook, /schedule, /trigger-channel) | PERM_MANAGE_WORKFLOWS_ADMIN |
Agent schedule, specialties, triggers writes; /api/rl/agents/*/policies | PERM_MANAGE_AGENTS |
Guardrail rule writes, procedures writes, /api/tenant/llm-gateway/content-policy PUT | PERM_MANAGE_GUARDRAILS |
PUT /api/approvals/policy/** | PERM_MANAGE_APPROVALS |
/api/tools/code/**, /api/tools/openapi/** writes | PERM_MANAGE_TOOLS (code generate/validate: ROLE_tenant-admin or ROLE_agent-builder) |
/api/code-intel/** | ROLE_tenant-admin or ROLE_agent-builder |
/api/leads/** | PERM_MANAGE_LEADS |
PUT /api/tenant/settings, branding logo writes, AI model preferences writes, /api/tenant/sso/** | PERM_MANAGE_TENANT_SETTINGS |
/api/tenant/upgrade, invoices, billing-profile, subscription cancel, resolutions | PERM_MANAGE_BILLING |
/api/tenant/data-export, data-deletion, data-usage | PERM_MANAGE_DATA_GOVERNANCE |
/api/briefs/** | Reads and readiness: authenticated; tune/rehearse/experiments: PERM_MANAGE_AGENTS; apply/rollback: both PERM_MANAGE_AGENTS and PERM_MANAGE_WORKFLOWS_ADMIN; trust: ROLE_tenant-admin |
| Everything else | anyRequest().authenticated() |
This table summarises the matchers; the full ordered list is in
SecurityConfig.filterChain. Reads on integration resources generally fall through
to plain authenticated(), so any tenant member can read status while only holders of
the permission can write.
Layer 3: per-resource access
Agents and workflows have their own sharing levels. AgentChatStreamController.chat and
AgUiRunController.run call agentService.requireAccess(agentId, PermissionLevel.RUNNER);
AgUiRunController.tools requires VIEWER. A caller without access gets 404 rather than 403
(anti-enumeration). Workflow run requires RUNNER (WorkflowController.run). Denied requests
refused by the URL matchers are written to the audit log (authz.denied).
Unauthenticated and alternate-credential paths
| Path | Verb | Note |
|---|---|---|
/actuator/health/**, /actuator/info, /actuator/prometheus | any | Prometheus scraped on the internal network; the edge proxy is expected to block /actuator/* publicly. |
/api/system/status/live, /api/public/status | GET | Bare liveness and component status. |
/api/public/landing-event | POST | Funnel beacon; controller enforces event whitelist and per-IP cap. |
/api/signup, /api/auth/social-providers | POST, GET | Pre-login; signup has its own rate limiter. |
/api/demo/** | any | Path-prefix rule; identity via X-Demo-Token. Anything added here is public. |
/api/share/agents/**, /api/share/workflows/**, /api/share/branding/** | any / GET | Anonymous share links (branding: GET only). |
/api/public/email/unsubscribe | GET, POST | HMAC token in ?t=. |
/api/templates/gallery/**, /api/plugins/catalog, /api/plugins/artifacts/**, /api/commerce/feed/** | GET (feed also POST) | Public catalogues; the feed's POST is the public cart checkout. |
/.well-known/agent-card.json, /.well-known/agent.json, /.well-known/jwks.json | GET | A2A discovery and card-signing keys. |
/api/edge/config, /api/edge/manifest, /api/edge/tools, /api/edge/memory, /api/edge/tools/execute, /api/edge/turns | GET / POST | Filter-level permitAll; /tools/execute and /tools require a JWT or X-Device-Token inside the controller. /api/edge/manifest/full and /manifest/registry need ROLE_platform-admin. |
/api/devices/claim, /poll, /commands/*/result, /screen-state, /notifications, /app-usage, /sms | POST | Device credential inside the controller. /api/devices/register uses a Keycloak JWT. |
/oauth/google/callback, /oauth/x/callback, /oauth/atlassian/callback | GET | Third-party OAuth redirects; identity via signed state. |
/webhook/**, /api/billing/webhook/** | POST (GET for verification handshakes) | See Webhooks. /webhook/alerts/** and /mcp/**, /api/dev/** sit on dedicated filter chains that skip bearer-JWT processing. |
/scim/v2/**, POST /api/tenant/comp-grant, POST /api/internal/app-builder/generate-files | any | permitAll at the filter layer; the credential each controller expects was not confirmed while writing this page (not documented here). |
permitAll() does not skip bearer processing. If a route is on the main chain and a caller
sends a non-JWT Authorization: Bearer value, the resource server rejects it with 401 before the
controller runs. That is why the Alertmanager webhook and the MCP/developer-API paths have their own
@Order(1) chains without oauth2ResourceServer.
Environments
Deployed with a domain: API https://api.<your-domain>, Keycloak https://auth.<your-domain>.
Local development runs core with Keycloak on localhost:8180 as the primary issuer (local-dev only;
never a public URL) plus the additional-issuer entry for the public host. Clients must send tokens minted by
the same environment they call.
Evidence & references
core/security/src/main/java/com/agenticai/core/security/SecurityConfig.java— decoder, issuers,azpcheck, filter chains, every matcher above.core/security/.../GranularPermissionConverter.java,KeycloakRoleConverter.java—ROLE_/PERM_authority mapping.core/security/.../TenantContextFilter.java—tenant_idclaim, 403 shapes.core/agents/.../AgentController.java,agui/AgUiRunController.java,core/workflow/.../WorkflowController.java— resource-levelrequireAccess.core/tenancy/.../demo/DemoSessionController.java—X-Demo-Tokenheader constant.Agentic-AI-Platform.postman_collection.json— token request shapes.- ADR-0005, Identity & Access, Security Architecture.