Enterprise-Asserted Agent Identity (XAA)
At a glance. Your corporate IdP (Okta, Entra ID, Auth0) signs an “ID-JAG” JWT asserting the user the agent is acting for; the agent is the OAuth client presenting the assertion. AuthPlane validates it against the IdP’s JWKS, evaluates policy (IdP × client × scope × resource), resolves the assertion’s
subto a local user via subject mapping, and mints an MCP token withsub=userandact=agent. No per-user consent screen. For enterprise fleets where the IdP is the authority on which humans (and, transitively, which agents) may access which resources.
Topology
flowchart LR
IdP["Enterprise IdP<br/>(Okta, Entra)"]
Agent["MCP Agent"]
AS["AuthPlane"]
MCP["MCP Server"]
IdP -->|"signs ID-JAG"| Agent
Agent -->|"jwt-bearer assert."| AS
AS -->|mints MCP token| MCP
The enterprise IdP is the source of truth for agent identity — separate from any user login (which may not exist at all in a headless deployment). AuthPlane validates the assertion, applies policy, and issues the MCP token.
Flow
sequenceDiagram
participant Agent as MCP Agent
participant IdP as Enterprise IdP
participant AS as AuthPlane
participant MCP as MCP Server
Agent->>IdP: requests an ID-JAG for a given end-user
IdP-->>Agent: signs a JWT with header {typ: "oauth-id-jag+jwt"}, includes:<br/>iss: <IdP issuer><br/>aud: <AuthPlane audience><br/>sub: <user identity as known to the IdP><br/>exp, iat, jti<br/>(The agent identity is NOT in the assertion — the agent authenticates<br/>to AuthPlane as an OAuth client, with its own client_id/secret.)
Agent->>AS: POST /oauth/token<br/>grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer<br/>assertion=<the ID-JAG JWT><br/>client_id=$CLIENT_ID ← this is the AGENT<br/>client_secret=$CLIENT_SECRET<br/>resource=https://mcp.example.com/mcp<br/>scope=tools/read
AS->>AS: validates:<br/>Assertion signature against registered IdP's JWKS (cached)<br/>iss matches a trusted IdP<br/>aud matches the IdP's registered audience<br/>exp within max_assertion_age (default 5m)<br/>jti not previously used (replay guard)
AS->>AS: runs policy engine:<br/>(idp × client × scope × resource) tuple must match at least one policy<br/>Deny by default
AS->>AS: resolves subject:<br/>auto_map: local user is "{iss}:{sub}"<br/>strict: local user comes from an explicit xaa_subject_mappings row (else deny)
AS->>AS: mints MCP access token:<br/>sub = <local user id> ← the human, not the agent<br/>act = { sub: <agent client_id> } ← the agent is the actor<br/>scope = intersection(requested, policy.scopes)<br/>aud = requested resource
AS-->>Agent: { access_token, token_type=Bearer, expires_in=3600 }
Agent->>MCP: standard Bearer request
DPoP works normally — send a DPoP header on step 3 to get a DPoP-bound token.
When to use
- Fleet of enterprise agents, central policy control needed.
- Headless environments (CI runners, cron jobs, backend services) where browser consent isn’t possible.
- You want the corporate IdP to be the authority on which humans (and their agents) can access which resources — not AuthPlane’s consent UI.
- Interactive per-user consent screens don’t fit the flow (the human already consented at the IdP).
Don’t use when:
- Interactive users can consent in a browser → oidc-federated-login is simpler.
- No enterprise IdP → use standard single-mcp or m2m-client-credentials.
- You want AuthPlane consent screens as an audit trail → XAA replaces those with policy audit rows.
How to configure
Full step-by-step in Guides: Enterprise-Managed Auth. Summary:
Enable XAA:
xaa:
enabled: true
token_expiry: 1h
max_assertion_age: 5m
subject_mode: auto_map # or "strict"
jwks_cache_ttl: 1h
XAA config is YAML-only at v0.1.x — no env-var overrides.
Register the trusted IdP (no CLI subcommand; use the admin REST API):
curl -s -X POST http://localhost:9001/admin/idps \
-H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Corp Okta",
"issuer": "https://acme.okta.com",
"jwks_uri": "https://acme.okta.com/.well-known/jwks.json",
"audience": "https://auth.example.com"
}'
Create a policy (deny by default; at least one match needed — no CLI subcommand; use the admin REST API):
curl -s -X POST http://localhost:9001/admin/xaa/policies \
-H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"idp_id": "idp_abc123",
"client_ids": ["my-mcp-client"],
"scopes": ["tools/echo", "tools/search"],
"resources": ["https://mcp.example.com/mcp"]
}'
Register the MCP client with jwt-bearer grant (must be confidential):
authserver admin client create \
--name enterprise-agent \
--grant-types urn:ietf:params:oauth:grant-type:jwt-bearer \
--auth-method client_secret_post \
--scopes 'tools/echo||Echo tool' \
--scopes 'tools/search||Search tool'
How AuthPlane handles it
JWTBearerService.Exchangevalidates the assertion (XAAIDPServicehandles JWKS lookup + signature verification;AssertionJTIStoreguards replay).- Policy engine (
XAAPolicyService) evaluates all policies for the assertion’s IdP; ANY match allows, none matches =access_denied. - Subject mapping —
auto_mapuses{iss}:{sub}as the token’ssub;strictlooks up an explicitsubject_mappingrow and refuses if absent. MintIssuersigns the token;machine_token_storerecords it for revocation + introspection.- Metrics:
authplane_xaa_policy_evaluation_total{decision},authplane_xaa_idp_operations_total,authplane_xaa_subject_resolutions_total.
Testing without a real corporate IdP
Use xaa.dev — public XAA playground built on Okta. Two-minute setup with ngrok. Full walkthrough at authserver/docs/how-to/test-xaa-with-xaa-dev.md.
Verify
# Trusted IdP registered (no CLI subcommand; query the admin REST API)
curl -s http://localhost:9001/admin/idps \
-H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY"
# At least one policy exists
curl -s http://localhost:9001/admin/xaa/policies \
-H "Authorization: Bearer $AUTHPLANE_ADMIN_API_KEY"
# Metric proves policy evaluation is happening
curl -s http://localhost:9001/metrics | grep authplane_xaa_policy_evaluation_total
# → authplane_xaa_policy_evaluation_total{decision="allow"} <n>
# authplane_xaa_policy_evaluation_total{decision="deny"} <n>
See also
- Guides: Enterprise-Managed Auth — end-to-end walkthrough
- Concepts: Cross-App Access (XAA) — mental model
- Concepts: Grants & flows → JWT Bearer
- Test XAA with xaa.dev (authserver repo)
- Reference: Configuration → xaa