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 sub to a local user via subject mapping, and mints an MCP token with sub=user and act=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:

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.Exchange validates the assertion (XAAIDPService handles JWKS lookup + signature verification; AssertionJTIStore guards replay).
  • Policy engine (XAAPolicyService) evaluates all policies for the assertion’s IdP; ANY match allows, none matches = access_denied.
  • Subject mapping — auto_map uses {iss}:{sub} as the token’s sub; strict looks up an explicit subject_mapping row and refuses if absent.
  • MintIssuer signs the token; machine_token_store records 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