Cross App Access (ID-JAG)
Cross App Access (XAA) lets an enterprise identity provider broker app-to-app access without asking the user to click through a second OAuth consent flow. It implements the IETF Identity Assertion Authorization Grant (ID-JAG, draft-ietf-oauth-identity-assertion-authz-grant), the protocol popularized by Okta as "Cross App Access" and adopted as an MCP authorization extension (MCP, the Model Context Protocol, is the standard way AI agents call tool servers) — it is how an enterprise IdP can grant Claude, ChatGPT, or any MCP client access to a company's MCP servers and SaaS APIs under central admin policy. An ID-JAG is a signed note from the identity provider saying "this user may use that app".
LumoAuth implements both roles of the draft:
| Role | What it does | Organization toggle |
|---|---|---|
| IdP role | Issue ID-JAGs: a requesting app exchanges the user's ID token for a signed assertion targeting another app's authorization server (token exchange). | xaa_idp_enabled |
| Resource-AS role | Accept ID-JAGs: a resource app redeems an assertion minted by a trusted external IdP for local access tokens (the JWT bearer grant). | xaa_resource_enabled |
Both toggles live in Portal → Security → Sign-in settings → Platform security, and both default to off. Turn one on and click Save changes. Policies and trusted issuers are managed in Portal → AI Agents → Agent rules → Cross-app access (/orgs/{orgId}/portal/delegation), which shows a notice while a role is still switched off.

Why this matters for MCP
Without XAA, every AI assistant ↔ MCP server pairing needs its own interactive OAuth consent per user, and enterprise admins have no central control. With XAA, the flow becomes:
- The user signs in to the AI assistant (e.g. Claude) via enterprise SSO — the assistant holds an ID token from the IdP.
- The assistant asks the IdP (token exchange) for an ID-JAG targeting the MCP server's authorization server.
- The assistant redeems the ID-JAG at the MCP server's authorization server (
jwt-bearer) for a normal access token — no user interaction, fully audited on both sides, revocable by the admin.
IdP role — issuing ID-JAGs
Setup
Everything is done on one page, AI Agents → Agent rules → Cross-app access
(/orgs/{orgId}/portal/delegation), which is a four-step checklist completed in
place. A glossary in the side column explains each term ("ID-JAG — a signed note from
your identity provider that says this user may use that app").
- Step 1 · Roles: switch on Issue identity assertions (tagged IdP role) and click Save switches. The same switch is on Sign-in settings; the page writes only the three Cross App Access keys of the organization's auth settings.
- Step 3 · Applications: on the requesting application's row, turn on Can obtain assertions. The switch saves itself (the row's Save button is the no-JS fallback); it is the same flag as Can obtain identity assertions (ID-JAG) in the app's Token settings.
- Step 4 · Which app may reach which service: click Add policy (Add your first
policy while the list is empty) and fill in the dialog: Application (the
requesting app) → Target authorization server (its issuer URL) → Allowed
scopes (space-separated). Click Add policy. Edit… in a row's actions menu
opens the same dialog prefilled and adds an Active switch to pause a pairing
without deleting it. Without a matching active policy the exchange fails with
invalid_target.


Token exchange request
The client authenticates as usual and presents the user's ID token:
POST /orgs/{orgId}/api/v1/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&subject_token=eyJraWQiOi... # the user's ID token from THIS tenant
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&audience=https://auth.wiki-app.example.com # issuer URL of the RESOURCE app's AS
&scope=mcp.read mcp.write
&client_id=client_abc123
&client_secret=...
Validation performed: the client is XAA-enabled, the subject token is a valid ID token issued by this organization to this client (signature via organization keys, exp, aud/azp = requesting client), the user is active, and an active grant policy allows this (client → audience) pair. Granted scopes are the intersection of the request and the policy.
Response
{
"access_token": "eyJ0eXAiOiJvYXV0aC1pZC1qYWcrand0Iiwia2lkIjoi...",
"issued_token_type": "urn:ietf:params:oauth:token-type:id-jag",
"token_type": "N_A",
"expires_in": 300,
"scope": "mcp.read mcp.write"
}
token_type is N_A, as the token exchange standard prescribes — the issued token is not an access token for a resource server; it is an authorization grant for the target AS.
The ID-JAG itself
A JWT signed with the organization signing key, typ: "oauth-id-jag+jwt":
{
"iss": "https://app.lumoauth.dev/orgs/acme/api/v1",
"sub": "1042",
"aud": "https://auth.wiki-app.example.com",
"client_id": "client_abc123",
"scope": "mcp.read mcp.write",
"jti": "9f6c1c9e2a...",
"iat": 1767225600,
"exp": 1767225900
}
ID-JAGs are single-use, five-minute artifacts. Every issuance is audited as xaa.id_jag_issued.
Resource-AS role — accepting ID-JAGs
Setup
- Step 1 · Roles: switch on Accept identity assertions (tagged Resource role) and click Save switches. This also advertises
urn:ietf:params:oauth:grant-type:jwt-beareringrant_types_supportedandauthorization_grant_profiles_supported: ["urn:ietf:params:oauth:grant-profile:id-jag"](MCP Enterprise-Managed Authorization) in both discovery documents. The optional Require an explicit scope when redeeming switch (xaa_resource_require_scope, also on Sign-in settings) refuses redemptions that do not name their scopes, so a redeeming app never silently receives every scope it is registered for. - Step 3 · Applications: on each resource application that should be able to redeem assertions, turn on Can redeem assertions (the same flag as Can redeem identity assertions (ID-JAG) in the app's Token settings). Redemption is a per-client opt-in for confidential clients only — public clients show Public client — not allowed and the switch is refused server-side; enabling the organization toggle alone lets no client redeem anything.
- Step 2 · Trusted identity providers: click Register provider (Register your first provider while the list is empty) to register the external IdP: its Issuer URL, optionally an explicit Keys (JWKS URL) (otherwise keys are resolved via OIDC discovery from the issuer; a pinned inline JWKS is also supported via the admin API — remote JWKS documents must be JSON and at most 256 KiB), How to match the user (the subject mapping strategy), optionally Only from this client at the provider (a binding to a single upstream
client_id), and Who may redeem these assertions — a picker over this organization's confidential apps (none selected = every app with the opt-in above). Both gates must pass. Click Register provider. Edit… in a row's actions menu opens the same dialog prefilled and adds an Enabled switch to stop accepting a provider's assertions without deleting it.

Subject mapping strategies
- Email claim (default): the assertion must carry an
emailclaim andemail_verified: true; it is mapped to a local user with the same, verified email address. Assertions with a missing or non-trueemail_verifiedare refused — an IdP that does not vouch for the address cannot bind the assertion to a local account by e-mail. - External subject: the assertion
subis looked up as a linked user identity (UserIdentityof typeidjag:{issuerId}), typically provisioned alongside SCIM/SSO onboarding from the same IdP.
A subject that cannot be mapped fails with invalid_grant.
Access token request
The resource app authenticates with its own client credentials at this organization (confidential clients only) and presents the assertion:
POST /orgs/{orgId}/api/v1/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJ0eXAiOiJvYXV0aC1pZC1qYWcrand0Iiwia2lkIjoi...
&client_id=wiki_backend
&client_secret=...
Processing rules enforced (per the draft):
- the redeeming client is confidential, has Can redeem assertions switched on, and is permitted by the issuer's Who may redeem these assertions list (
unauthorized_clientotherwise); - header
typMUST beoauth-id-jag+jwt; algorithm allow-list (RS/ES/PS families only); issMUST be a registered, enabled trusted issuer; signature verified against its JWKS (SSRF-guarded fetch, cached, DNS-pinned);audMUST equal this organization's issuer URL exactly (as published in discovery);exp/iatsanity (max 24 h age, 60 s clock skew);jtisingle-use (replay cache, scoped per organization and issuer) — a replayed assertion fails withinvalid_grant;- optional upstream
client_idbinding.
Scope semantics
The assertion's scope claim is the authorization the IdP granted, so it is intersected strictly with the redeeming client's registered scopes:
- an assertion without a
scopeclaim grants no scope — the token is issued with an emptyscope, never with the client's full registered set; - set the organization auth setting
xaa_resource_require_scope: trueto refuse scope-less assertions outright withinvalid_grant; - an optional
scoperequest parameter may only narrow the result further.
Resource claim (MCP Enterprise-Managed Authorization)
When the assertion carries a resource claim (one URI or an array), the access token is audience-restricted to it (aud = the resource) instead of the redeeming client, and the matching registered MCP server policy is applied: allowed_client_ids (invalid_target if the redeeming client is not allowed), token_lifetime (caps expires_in) and the scope vocabulary (invalid_scope). Refresh tokens stay bound to the same resources.
Response
Standard tokens, bound to the redeeming client. A refresh token is included when the client is registered for refresh_token.
{
"access_token": "eyJ0eXAiOiJhdCtqd3QiLCJraWQiOi...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "mcp.read"
}
JWT access tokens carry an act claim preserving the upstream delegation context, shaped per the OAuth Actor Profile (sub + iss namespace + sub_profile):
{
"sub": "87",
"aud": "https://mcp.wiki-app.example.com",
"client_id": "wiki_backend",
"scope": "mcp.read",
"act": {
"sub": "client_abc123",
"iss": "https://idp.customer.example.com",
"sub_profile": "workload"
}
}
Sender-constrained redemption (DPoP)
Redemption can be sender-constrained with DPoP (a proof that ties the token to the client's key, so a stolen copy cannot be replayed): send a DPoP proof header (htm=POST, htu = the token endpoint) with the jwt-bearer request and the issued access token is bound to that key — JWT tokens carry cnf.jkt, opaque tokens are bound server-side — and the response advertises "token_type": "DPoP" (plus a DPoP-Nonce header when nonces are enabled). The refresh token, when issued, is bound to the same key, so refreshing it requires a proof for that key. The usual rules apply: a client registered with dpop_bound_access_tokens must send a proof, organizations that require server nonces answer a nonce-less proof with use_dpop_nonce, and a bad proof fails with invalid_dpop_proof. When the assertion's resource names a registered MCP server with Require DPoP on, a redemption without a valid proof is refused with invalid_dpop_proof — the MCP server only accepts sender-constrained tokens.
Redemptions are audited as xaa.id_jag_redeemed (with dpop_bound); every rejection is audited as xaa.id_jag_rejected with a reason.
Discovery
Both the OpenID discovery document and the OAuth authorization-server discovery document advertise:
- IdP role:
urn:ietf:params:oauth:grant-type:token-exchangeingrant_types_supported(always). - Resource-AS role (once
xaa_resource_enabledis on):urn:ietf:params:oauth:grant-type:jwt-beareringrant_types_supportedandauthorization_grant_profiles_supported: ["urn:ietf:params:oauth:grant-profile:id-jag"]— the MCP Enterprise-Managed Authorization extension's signal that this authorization server accepts ID-JAGs for its MCP servers. entity_profiles_supported: ["user", "ai_agent", "workload"]— theact.sub_profileclassifications used in delegated tokens (always).
Error reference
| Condition | Error |
|---|---|
| Organization role toggle off | invalid_request (IdP) / unsupported_grant_type (resource) |
| Client not XAA-enabled (IdP) / not enabled to redeem, or not in the issuer's redeeming-app list (resource) | unauthorized_client |
Assertion email_verified missing or not true (email mapping) | invalid_grant |
Assertion without scope while xaa_resource_require_scope is on | invalid_grant |
Assertion resource names an MCP server that does not allow the redeeming client | invalid_target |
| No grant policy for (client, audience) | invalid_target |
| Subject token not a valid ID token of this organization / foreign token | invalid_grant |
Untrusted issuer, wrong aud, bad signature, expired, replayed jti, unmappable subject | invalid_grant |
| Requested scopes entirely outside policy | invalid_scope |
Standards used on this page
| Plain name | Standard |
|---|---|
| Cross App Access (identity assertion JWT authorization grant, ID-JAG) | Identity Assertion Authorization Grant (draft) |
| Model Context Protocol (MCP) authorization | MCP Authorization specification |
| Token exchange (delegation) | RFC 8693 |
| JWT client authentication and the JWT bearer grant | RFC 7523 |
| Sender-constrained tokens (DPoP) | RFC 9449 |
| Discovery document | RFC 8414 |
See the standards reference for what each one covers and how LumoAuth uses it.