Enterprise-Managed Auth (XAA)
TL;DR — Your corporate IdP (Okta, Entra ID, Auth0) signs an “ID-JAG” JWT asserting agent identity. AuthPlane validates it against the IdP’s JWKS and mints an MCP token — skipping per-user consent for policy-approved agent×scope×resource combinations. Requires
xaa.enabled: true, one or more trusted IdP registrations, at least one policy, and MCP clients registered with thejwt-bearergrant. YAML-only at v0.1.x (env-var overrides not exposed).
When XAA fits
Use XAA when:
- Enterprise wants central control over which agents can call which tools, at which scope levels.
- Per-user consent screens don’t fit the environment (headless agents, CI runners, no browser).
- Your IdP can be extended to sign a custom JWT type (ID-JAG) — Okta, Entra ID, Auth0, and any OIDC provider that supports custom token types.
When NOT to use XAA: you have interactive users who can consent in a browser — use plain OIDC federation instead. XAA replaces user consent with enterprise policy; that’s the whole point.
Prerequisites
- AuthPlane running with
xaa.enabled: true - A trusted IdP that can sign ID-JAG assertions (JWT with
typ: oauth-id-jag+jwt) - Admin API key (
AUTHPLANE_ADMIN_API_KEY)
Enable XAA
xaa:
enabled: true
token_expiry: 1h # issued access-token lifetime
max_assertion_age: 5m # max age of an ID-JAG at time of exchange
require_resource: false # require ?resource=... on token requests
subject_mode: auto_map # "auto_map" or "strict"
jwks_cache_ttl: 1h # how long IdP JWKS keys are cached
At v0.1.x, all XAA config is YAML-only — no env-var overrides.
Step 1 — Register a trusted IdP
Tell AuthPlane which IdPs it should trust for XAA assertions. The IdP’s issuer + JWKS URI + audience.
curl -s -X POST http://localhost:9001/admin/idps \
-H "Authorization: Bearer $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"
}'
audience— the value the IdP will put in the ID-JAG’saudclaim. Defaults to your AS’s issuer URL if omitted.jwks_uri— auto-discovered from{issuer}/.well-known/openid-configurationif omitted (SSRF-protected fetch).
Response includes id: "idp_..." — the ID you’ll reference in policies. JWKS keys are cached for xaa.jwks_cache_ttl; force refresh with POST /admin/idps/{id}/refresh-keys.
There’s no CLI subcommand for XAA IdPs — the curl above is the canonical way to register a trusted IdP. Every other XAA resource (policies, subject mappings) is admin-REST-only too.
Step 2 — Create an authorization policy
Policies control which (IdP × client × scope × resource) combinations AuthPlane will mint tokens for.
curl -s -X POST http://localhost:9001/admin/xaa/policies \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Allow Acme agents",
"idp_id": "idp_abc123",
"client_ids": ["my-mcp-client"],
"scopes": ["tools/echo", "tools/search"],
"resources": ["https://mcp.example.com/mcp"]
}'
Deny by default — a request that matches no policy is rejected. Multiple policies can apply; if any allows, request proceeds.
Step 3 — Subject mapping (optional)
Controls how the external IdP’s sub claim maps to a local user identity.
Two modes (set via xaa.subject_mode in YAML):
auto_map(default) — external subjects accepted as-is. The issued token’ssubclaim uses the format{issuer}:{idp_subject}(e.g.,https://acme.okta.com:alice@acme.com).strict— only explicitly mapped subjects are allowed. Unmapped subjects →access_denied.
Create explicit mappings:
curl -s -X POST http://localhost:9001/admin/xaa/subject-mappings \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"idp_id": "idp_abc123",
"idp_subject": "alice@acme.com",
"local_user_id": "usr_local_alice"
}'
When a mapping exists (in either mode), the token’s sub uses local_user_id instead of the federated format — useful when you want AuthPlane audit rows tied to your own user IDs, not the IdP’s opaque subject strings.
Step 4 — Register an MCP client with jwt-bearer grant
The MCP client must have urn:ietf:params:oauth:grant-type:jwt-bearer in its grant_types:
curl -s -X POST http://localhost:9000/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "enterprise-agent",
"grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"],
"token_endpoint_auth_method": "client_secret_post"
}'
Response includes the client_id and client_secret to use on token requests.
Confidential clients (client_secret_post or client_secret_basic) are required — public clients can’t do XAA.
Step 5 — Exchange an ID-JAG for an access token
The IdP signs an ID-JAG assertion. The MCP client presents it to AuthPlane:
curl -s -X POST http://localhost:9000/oauth/token \
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
-d "assertion=$ID_JAG_JWT" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "scope=tools/echo" \
-d "resource=https://mcp.example.com/mcp"
Response:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "tools/echo"
}
The issued token is a standard RFC 9068 AT-JWT — validate it the same way as any other AuthPlane token in your MCP server. DPoP works the same way (send a DPoP header on the /oauth/token call to get a DPoP-bound token).
ID-JAG assertion format
The JWT your IdP signs must have:
Header:
{ "alg": "RS256|ES256|PS256", "typ": "oauth-id-jag+jwt", "kid": "..." }
Payload:
{
"iss": "<idp issuer>", // must match a registered trusted IdP
"aud": "<audience>", // must match the IdP's registered audience
"sub": "<user subject>", // per your IdP's convention
"exp": <unix>, // must be within max_assertion_age
"iat": <unix>,
"jti": "<unique>" // replay-guarded — single use
}
Extra claims (groups, department, custom claims) are ignored by AuthPlane at v0.1.x — they can influence policy decisions in a future release. Your IdP can include whatever’s convenient.
Testing with xaa.dev
If you don’t have an enterprise IdP handy, use xaa.dev — the public XAA playground built on Okta. Full walkthrough with ngrok in authserver/docs/how-to/test-xaa-with-xaa-dev.md. Two-minute setup: register idp.xaa.dev as a trusted IdP, run AuthPlane behind ngrok, request an ID-JAG from xaa.dev’s playground UI, present it to your /oauth/token.
Metrics
Once XAA is in prod:
authplane_xaa_policy_evaluation_total{decision="allow|deny"}— every policy checkauthplane_xaa_idp_operations_total— IdP registry CRUD + JWKS refreshauthplane_xaa_subject_resolutions_total— subject mapping resolutions
rate(authplane_xaa_policy_evaluation_total{decision="deny"}[5m]) spiking = policy misconfig or unauthorized attempts.
Troubleshooting
Related
- Concepts: Cross-App Access (XAA) — mental model
- Concepts: Grants & flows → JWT Bearer
- Reference: Configuration → xaa — every YAML knob
- Topologies: Enterprise XAA — end-to-end topology diagram
- Test XAA with xaa.dev (authserver repo) — reproducible playground walkthrough