Skip to main content
Version: 2026.1

Authentication

The agent-server does not own users. Every /agent-server/api/* endpoint (except two explicit exceptions) inherits authentication from Pimcore itself: the browser sends the user's Pimcore session cookie, the agent-server validates it against the PHP backend, and MCP tool calls run with the user's real permissions. Admin endpoints use a separate bearer token.

The three auth flows

FlowUsed byCredentialEndpoint reached
User session cookieBrowser → agent-server; agent-server → auth-validator onlyPimcore PHPSESSID cookie/agent-server/api/* (excluding admin), GET /pimcore-studio/api/user/current-user-information (auth-validator call only)
MCP access token (per chat session)Agent-server → all server-to-server PHP calls for a chat sessionAuthorization: Bearer pmcp_…/pimcore-studio/api/bundle/agent/* (bundle API firewall), /pimcore-mcp/agent/* (MCP firewall)
Admin bearer tokenAgent-server → PHP config export; operator → admin endpointsAGENT_SERVER_ADMIN_TOKENGET /pimcore-studio/api/bundle/agent/configurations/export, /agent-server/api/admin/*

Each flow is detailed below. See MCP Integration → Authentication forwarding for the full token lifecycle (issue / refresh / re-mint / revoke / GC).

authentication_flow.pngauthentication_flow.pngauthentication_flow.png

Flow

  1. Browser includes the Pimcore session cookie on every request.
  2. Nginx proxies to the agent-server, forwarding the cookie.
  3. Auth middleware calls GET /pimcore-studio/api/user/current-user-information with the cookie.
  4. PHP resolves the session to a Pimcore user and returns identity (ID, username, admin flag, permissions).
  5. The identity is cached so repeat requests don't re-hit PHP.

Identity cache

The validated identity is cached in-memory keyed by sha256(cookie). TTL defaults to 60 seconds (AGENT_SERVER_AUTH_CACHE_TTL). After expiry the next request re-validates against PHP.

This is the validation cache only. Conversation data lives in Session Storage.

Bearer scope and the bundle-API firewall

The browser cookie is used only to authenticate the agent-server's own /agent-server/api/* endpoints (via the auth-validator call GET /pimcore-studio/api/user/current-user-information). All subsequent server-to-server PHP calls for a chat session use the chat-scoped bearer (Authorization: Bearer pmcp_…):

  • Bundle API (/pimcore-studio/api/bundle/agent/*) — protected by a dedicated pimcore_agent_bundle_api firewall whose authenticator chain is SessionBridge → McpAccessTokenAuthenticator → PAT. The firewall definition is exposed as %pimcore_agent.bundle_api_firewall_settings%; the consuming project wires it in security.yaml (see Installation → Configure Security).
  • Internal MCP servers (/pimcore-mcp/agent/*) — protected by the pimcore_mcp firewall from Studio Backend.

In both cases McpAccessTokenAuthenticator resolves the bearer to the chat initiator's Pimcore User and Pimcore's standard permission system applies. An agent has exactly the permissions of the user who started the chat — no privilege escalation, no service-account fallback.

External MCP servers (mcpServers) receive neither the cookie nor the MCP access token; they use whatever auth their YAML configures. For the issue/refresh/re-mint lifecycle of the bearer see MCP Integration → Token lifecycle.

Exceptions

These endpoints do not require a cookie:

  • GET /agent-server/api/health — health check.
  • GET /agent-server/api/docs — Swagger UI (only when NODE_ENVproduction).

Per-provider LLM authentication

Each inference provider in pimcore_agent.inference.providers carries its own auth_mode and token. The agent-server applies provider auth at session-creation time — after resolving which provider an agent uses — before the SDK session is started:

auth_modeEffect
byokBuilds an SDK provider block: { type, baseUrl, apiKey }. The token value (after ${VAR} interpolation) becomes the apiKey.
githubSets the session-level gitHubToken. No provider block is added.

A session's provider credentials are determined once at creation and held for the lifetime of the SDK session. If the agent changes mid-conversation (agent switch) the SDK session is recreated with the new agent's provider resolved from scratch.

Mixing byok and github agents in the same deployment is supported — each agent's session uses its own provider's credentials independently. The single caveat is that only one GitHub identity (GitHub PAT) is used across all github-mode providers in a deployment; multiple distinct GitHub accounts per provider are not yet supported.

For the full schema and examples of provider configuration, see Inference Providers.

Admin bearer token

Admin endpoints are machine-to-machine: the agent-server fetches the config export endpoint in PHP, and an operator may trigger a reload or list models.

AGENT_SERVER_ADMIN_TOKEN=<strong-random-value>

If AGENT_SERVER_ADMIN_TOKEN is unset, admin endpoints return 403. In production use a strong random value.

Endpoints

EndpointUsed by
GET /pimcore-studio/api/bundle/agent/configurations/exportThe agent-server, on startup and on every reload. Verified with hash_equals; bypasses the user-session firewall via a PUBLIC_ACCESS rule in security.yaml.
POST /agent-server/api/admin/reload-agentsRe-fetches the config export, clears sessions, returns model-validation warnings.
GET /agent-server/api/admin/modelsLists available LLM models, cross-references them against agent configs.

Example:

curl -X POST http://localhost/agent-server/api/admin/reload-agents \
-H "Authorization: Bearer $AGENT_SERVER_ADMIN_TOKEN"

Mercure publisher JWT

Live cross-client chat sync publishes each turn event to Studio's per-user Mercure topic. This is a fourth, narrow credential — used only to publish to the hub, never to authenticate a user:

PropertyValue
CredentialPublisher JWT (HS256), signed with the shared MERCURE_JWT_KEY
Signed byThe agent-server, in-process (mercure-publisher, node:crypto — no JWT dependency)
Publish scopeURI Template studio-backend-default/user/{id} (least privilege; the hub rejects a …/user/* glob)
TargetA private update to studio-backend-default/user/{userId}, POSTed to MERCURE_SERVER_URL

The agent-server already knows the chat initiator's Pimcore userId (it owns the session), so it targets the correct user topic without any lookup. Because the update is private, the hub delivers it only to subscribers whose cookie JWT (minted by Studio's ClientTokenService on app load) authorises that exact topic — so a user's events reach only that user's own tabs/devices. Cross-tenant isolation is enforced by the hub, not by an ownership check in agent-server code.

Security notes:

  • The shared MERCURE_JWT_KEY lives in the agent-server env at the same trust level as PHP (PHP signs the same hub with the same secret). It is never shipped to the browser.
  • Publish scope is limited to the user-topic namespace, never the bare * (PHP's server token uses the broader scope; the agent-server deliberately stays narrower).
  • If MERCURE_JWT_KEY is unset/blank the publisher is a no-op (a startup warning is logged) — chat still works without live sync; PHP remains the record. See Real-time Sync and Configuration → Environment Variables.

CSRF protection

Every state-changing request (POST / PUT / DELETE) to /agent-server/api/* must include X-Requested-With: XMLHttpRequest. This triggers the CORS preflight for cross-origin requests and blocks silent cross-site form submissions.

The frontend sets the header automatically in both the streaming fetch client (agent-stream.service.ts) and the RTK Query slice (agent-api-slice.ts). If you add a new endpoint or call from custom code, include the header:

fetch('/agent-server/api/…', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'include',
body: JSON.stringify(payload)
})

Rate limiting

Per-user limits via @fastify/rate-limit, keyed by authenticated user ID (falling back to IP):

Endpoint groupLimit
Chat (POST /chat, POST /chat/:id)20 / min
Sessions (GET /sessions)30 / min
Admin (POST /admin/*)5 / min

Over the limit → HTTP 429.

Other security measures

  • Credential isolation — user session cookies are not forwarded beyond the auth-validator call. All server-to-server PHP calls (bundle API and internal MCP servers) use the chat-scoped pmcp_… bearer; third-party servers under mcpServers receive only what their YAML defines. The chat session id is bound to the bearer server-side and recovered from the validated token, not from any client-supplied header.
  • Error sanitisation — internal error details (stack traces, SDK errors, upstream URLs) never reach the client. SSE error events carry generic messages; full details are server-side logs.
  • Message length cap — chat messages are capped at 10,000 characters to prevent token-cost abuse and storage exhaustion.
  • Request IDs — every response returns X-Request-Id. Nginx-supplied IDs are preserved; otherwise a UUID is generated.

Audit log

Security events are emitted as structured JSON with "level": "audit":

EventWhen
auth_no_cookieNon-exception endpoint hit without a cookie.
auth_failedCookie validation against Pimcore returned no user.
session_access_deniedUser tried to access another user's session.
admin_auth_missing / admin_auth_failedAdmin endpoint hit without / with wrong token.
admin_reload_agents / admin_list_modelsSuccessful admin actions.

The tool-security hook adds its own audit records — see Tool Security.