Skip to main content

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:

RoleWhat it doesOrganization toggle
IdP roleIssue 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 roleAccept 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.

The two Cross App Access switches in the Platform security section of Sign-in settings

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:

  1. The user signs in to the AI assistant (e.g. Claude) via enterprise SSO — the assistant holds an ID token from the IdP.
  2. The assistant asks the IdP (token exchange) for an ID-JAG targeting the MCP server's authorization server.
  3. 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").

  1. 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.
  2. 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.
  3. 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.

The Cross-app access page with its four-step checklist

The Add identity assertion policy dialog

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​

  1. 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-bearer in grant_types_supported and authorization_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.
  2. 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.
  3. 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.

The Register trusted identity provider dialog

Subject mapping strategies​

  • Email claim (default): the assertion must carry an email claim and email_verified: true; it is mapped to a local user with the same, verified email address. Assertions with a missing or non-true email_verified are 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 sub is looked up as a linked user identity (UserIdentity of type idjag:{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_client otherwise);
  • header typ MUST be oauth-id-jag+jwt; algorithm allow-list (RS/ES/PS families only);
  • iss MUST be a registered, enabled trusted issuer; signature verified against its JWKS (SSRF-guarded fetch, cached, DNS-pinned);
  • aud MUST equal this organization's issuer URL exactly (as published in discovery);
  • exp/iat sanity (max 24 h age, 60 s clock skew);
  • jti single-use (replay cache, scoped per organization and issuer) — a replayed assertion fails with invalid_grant;
  • optional upstream client_id binding.

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 scope claim grants no scope — the token is issued with an empty scope, never with the client's full registered set;
  • set the organization auth setting xaa_resource_require_scope: true to refuse scope-less assertions outright with invalid_grant;
  • an optional scope request 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-exchange in grant_types_supported (always).
  • Resource-AS role (once xaa_resource_enabled is on): urn:ietf:params:oauth:grant-type:jwt-bearer in grant_types_supported and authorization_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"] — the act.sub_profile classifications used in delegated tokens (always).

Error reference​

ConditionError
Organization role toggle offinvalid_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 oninvalid_grant
Assertion resource names an MCP server that does not allow the redeeming clientinvalid_target
No grant policy for (client, audience)invalid_target
Subject token not a valid ID token of this organization / foreign tokeninvalid_grant
Untrusted issuer, wrong aud, bad signature, expired, replayed jti, unmappable subjectinvalid_grant
Requested scopes entirely outside policyinvalid_scope

Standards used on this page​

Plain nameStandard
Cross App Access (identity assertion JWT authorization grant, ID-JAG)Identity Assertion Authorization Grant (draft)
Model Context Protocol (MCP) authorizationMCP Authorization specification
Token exchange (delegation)RFC 8693
JWT client authentication and the JWT bearer grantRFC 7523
Sender-constrained tokens (DPoP)RFC 9449
Discovery documentRFC 8414

See the standards reference for what each one covers and how LumoAuth uses it.