Cross-App Access (XAA)
TL;DR — Two different things called “enterprise IdP integration”. OIDC federation is about user login — the user’s identity comes from your corporate IdP. XAA (Cross-App Access) is about agent identity — your IdP signs a JWT asserting the agent’s identity, AuthPlane validates it against the IdP’s JWKS and mints an MCP token without a consent screen. Central policy replaces per-user consent for enterprise-controlled agent fleets. Uses the JWT Bearer grant (RFC 7523) with the ID-JAG assertion type (
oauth-id-jag+jwt) — an emerging IETF draft referenced by the MCP Authorization spec’s Enterprise-Managed Authorization profile.
The problem it solves
Per-user consent screens are fine for one user picking one integration. They break down when:
- You have a fleet of enterprise agents (headless CI runners, background jobs, backend services acting as themselves) that can’t consent.
- You want central policy control — “these agents can access these tools with these scopes” — determined by the enterprise IT team, not by each user clicking approve.
- Compliance requires that agent access is enterprise-attested, not user-consented.
XAA moves consent from “user clicks approve” to “enterprise-issued ID-JAG proves agent identity → AuthPlane policy allows”.
The wire flow
1. Your enterprise IdP signs an ID-JAG for an agent:
JWT header: {typ: "oauth-id-jag+jwt"}
Payload: {iss: <IdP>, aud: <AuthPlane>, sub: <agent-identity>, exp, iat, jti}
Signed with IdP's private key.
2. Agent → AuthPlane POST /oauth/token
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=<the ID-JAG>
client_id=$AGENT_CLIENT_ID
client_secret=$AGENT_SECRET
resource=https://mcp.example.com/mcp
scope=tools/read
3. AuthPlane validates:
- Signature (against IdP's cached JWKS)
- iss matches a registered trusted IdP
- aud matches the IdP's registered audience
- exp within max_assertion_age (default 5m)
- jti not previously used (replay guard)
4. AuthPlane runs policy engine:
- (IdP × client × scope × resource) tuple must match at least one policy
- Deny by default
5. AuthPlane mints an MCP access token — no consent screen involved
6. Agent ← AuthPlane { access_token, expires_in: 3600 }
Two independent-but-related concepts
Enterprises often need both at the same time:
- OIDC federation (topologies/oidc-federated-login) — human users log into AuthPlane via their corporate IdP.
- XAA — enterprise agents get MCP tokens from AuthPlane via ID-JAG.
They stack. Users log in via SSO; agents authenticate via XAA. Same corporate IdP, two different authentication paths at AuthPlane.
What an ID-JAG contains
The JWT your IdP signs. AuthPlane accepts one very specific typ: oauth-id-jag+jwt.
Header:
{
"alg": "RS256|ES256|PS256",
"typ": "oauth-id-jag+jwt",
"kid": "<idp-signing-key-id>"
}
Payload:
{
"iss": "https://acme.okta.com", // your IdP's issuer
"aud": "https://auth.example.com", // AuthPlane's audience
"sub": "<agent-identity-in-your-IdP>", // per your IdP convention
"exp": 1708762500,
"iat": 1708762200,
"jti": "unique-assertion-id"
}
Extra claims (groups, department, custom) are accepted but ignored by policy today — future versions may make them policy inputs.
Policy engine
Policies describe which (IdP × client × scope × resource) combinations are allowed.
Policy: "Allow Acme agents"
IdP: idp_abc123 (Acme Okta)
Client IDs: [my-mcp-client] (or empty = any client registered against this IdP)
Scopes: [tools/echo, tools/search] (or empty = client's default scopes)
Resources: [https://mcp.example.com/mcp] (or empty = any resource)
Multiple policies can apply; if ANY allows, the request proceeds. Deny by default — an ID-JAG that matches no policy is rejected with access_denied.
Scope narrowing at issue time: issued_scope = intersection(requested_scope, policy.scopes). Explicit least-privilege.
Subject mapping
Two modes:
auto_map(default) — external subjects accepted as-is. Token’ssub={iss}:{sub}(e.g.,https://acme.okta.com:alice@acme.com).strict— only explicitly mapped subjects allowed. Unmapped subjects →access_denied.
Explicit mappings translate external subjects to your own local user IDs:
Mapping: idp_abc123 / alice@acme.com → usr_local_alice
Useful when you want AuthPlane audit rows tied to your own user IDs, not the IdP’s opaque strings.
Replay guard
Every ID-JAG’s jti is stored on first use (in assertion_jti_store). A second use → invalid_grant "assertion jti already used". Assertions are single-use.
The assertion_jti table is one of the targets of authserver purge — schedule externally per Operate: Backup, upgrade, purge.
Metrics
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
Alert on rate(authplane_xaa_policy_evaluation_total{decision="deny"}[5m]) > 0 in normal ops — either policy misconfig or unauthorized attempts.
Test playground
xaa.dev — public XAA test IdP built on Okta. Two-minute setup with ngrok, no enterprise tenant required. Full walkthrough in authserver/docs/how-to/test-xaa-with-xaa-dev.md.
Related
- Guides: Enterprise-Managed Auth — end-to-end enablement
- Topologies: Enterprise-Asserted Agent Identity — full topology diagram
- Concepts: Grants & flows → JWT Bearer
- Reference: Configuration → xaa
- Concepts: Agent identity — how the agent identity appears in tokens
- Test XAA with xaa.dev (authserver repo)