Docs/API Reference/Authentication & Authorization

Authentication & Authorization

Default scheme
Authorization: Bearer <access-token> — a Keycloak-issued JWT, validated by Spring's OAuth2 resource server in SecurityConfig.
Token issuer
Keycloak at https://auth.example.com, realm agentic-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

CredentialHeaderUsed byVerified where
Keycloak JWTAuthorization: Bearer <token>Web console, admin console, other registered OAuth clients, external callersSpring resource server (SecurityConfig.jwtDecoder)
Demo sandbox tokenX-Demo-TokenAnonymous /api/demo/** sandbox and /tryDemoSessionController / DemoChatController (hash lookup; minted by POST /api/demo/session)
Device tokenX-Device-Token (pairing code for claim)Paired phones and desktop daemonsDeviceGatewayController, DeviceAuthResolver
MCP / developer API keyAuthorization: Bearer vmcp_<key>/mcp/** and /api/dev/**McpApiKeyAuthFilter. See Developer API.
Share idNone (id in the path)/api/share/agents/**, /api/share/workflows/**Controller resolves the id; rate-limited, revocable
Provider signature / secretVaries/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

  1. Signature against the JWKS at app.keycloak.jwk-set-uri (container-reachable, deliberately separate from the issuer).
  2. Default JWT validation (timestamps) via JwtValidators.
  3. Issuer must be the primary issuer-uri or one of the comma-separated KEYCLOAK_ADDITIONAL_ISSUERS. See ADR-0005. A missing additional issuer 401s every call while browser logins still look fine.
  4. Authorized party (azp) must be one of app.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 no azp is not rejected by this validator.
  5. Tenant claim. TenantContextFilter reads tenant_id; an authenticated JWT with none gets 403 with 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 gets 403.
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/credentialPERM_MANAGE_INTEGRATIONS
Channel credentials, defaults, pairing, contacts, routing (/api/channels/** writes), /api/elevenlabs/credentialPERM_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/*/policiesPERM_MANAGE_AGENTS
Guardrail rule writes, procedures writes, /api/tenant/llm-gateway/content-policy PUTPERM_MANAGE_GUARDRAILS
PUT /api/approvals/policy/**PERM_MANAGE_APPROVALS
/api/tools/code/**, /api/tools/openapi/** writesPERM_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, resolutionsPERM_MANAGE_BILLING
/api/tenant/data-export, data-deletion, data-usagePERM_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 elseanyRequest().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

PathVerbNote
/actuator/health/**, /actuator/info, /actuator/prometheusanyPrometheus scraped on the internal network; the edge proxy is expected to block /actuator/* publicly.
/api/system/status/live, /api/public/statusGETBare liveness and component status.
/api/public/landing-eventPOSTFunnel beacon; controller enforces event whitelist and per-IP cap.
/api/signup, /api/auth/social-providersPOST, GETPre-login; signup has its own rate limiter.
/api/demo/**anyPath-prefix rule; identity via X-Demo-Token. Anything added here is public.
/api/share/agents/**, /api/share/workflows/**, /api/share/branding/**any / GETAnonymous share links (branding: GET only).
/api/public/email/unsubscribeGET, POSTHMAC 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.jsonGETA2A discovery and card-signing keys.
/api/edge/config, /api/edge/manifest, /api/edge/tools, /api/edge/memory, /api/edge/tools/execute, /api/edge/turnsGET / POSTFilter-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, /smsPOSTDevice credential inside the controller. /api/devices/register uses a Keycloak JWT.
/oauth/google/callback, /oauth/x/callback, /oauth/atlassian/callbackGETThird-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-filesanypermitAll at the filter layer; the credential each controller expects was not confirmed while writing this page (not documented here).
Why a bearer header can break a public route
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