ndovuagent-auth

CROSS-APP ACCESS · THE FLAGSHIP EXPLAINER

How XAA works

Cross-App Access explained from the draft: the four roles and three trust relationships, the two hops on the wire (token exchange at the IdP, jwt-bearer grant at the Resource Authorization Server), the ID-JAG claims that matter, and the attack/defense map - every mechanic with its section citation.

updated October 31, 2026all cites against draft -05then validate a grant yourself
01 · The problem XAA names

Enterprises sign every application into one identity provider with OpenID Connect or SAML. But when one application needs to call another application's API on the user's behalf, OAuth's answer has been a direct app-to-app flow (an authorization code handshake between the two apps) that the IdP never sees (Section 1). The IdP that manages who can reach what sits the delegation out.

Cross-App Access (XAA) moves that decision inside the trust boundary that already exists: the IdP both apps already trust for SSO brokers the delegation. It does so by profiling identity chaining into concrete deliverables (Section 1): the Token Exchange request that obtains an ID-JAG, the ID-JAG itself with its claims, the processing rules the Resource Authorization Server applies when exchanging an ID-JAG for an access token, and the metadata used to advertise support.

The draft's own example: a wiki app wants to read your chat history at a chat app from another vendor. Both are SSO'd into the enterprise IdP. With XAA, the wiki asks the IdP for a grant naming the chat app's authorization server, and trades that grant for an access token (Section 4.1).

02 · Roles and trust relationships

Four roles (Section 2.1): the Client (the wiki app), the IdP Authorization Server (the enterprise IdP), the Resource Authorization Server (the chat app's AS), and the Resource Server (the chat API). Three separate registration relationships connect them (Section 5): client-to-IdP for SSO, client-to-Resource-AS for API access, and Resource-AS-to-IdP - the pre-existing SSO trust the whole pattern rides on.

The Client and the IdP AS live in trust domain A; the Resource AS and Resource Server in trust domain B. The ID-JAG is the object that crosses the boundary: signed by the IdP, consumed by the Resource AS. The Resource Server never talks to the IdP at all - it validates access tokens from its own AS only (Section 2.1).

TRUST DOMAIN ATRUST DOMAIN BClientIdP ASResource ASResource Server1 · user SSO (OIDC / SAML)ID Token (+ optional refresh token)2 · token exchange (RFC 8693)requested_token_type=id-jagaudience=<Resource AS issuer>ID-JAG, signed by the IdP AS3 · jwt-bearer grant (RFC 7523)assertion=<ID-JAG>access token, issued by the Resource AS4 · API callAuthorization: Bearer <access token>already trusts each other for SSO + subject resolution (out of band)

The two hops are steps 2 and 3: the grant crosses the trust boundary; the access token never leaves domain B. (Sections 2.1, 4.1, 5)

03 · Hop one: the token exchange

The client authenticates to the IdP - always; a request without client authentication is rejected withinvalid_client (Section 4.3.3) - and presents the Identity Assertion it got from SSO as thesubject_token. The audience parameter is the Resource Authorization Server's issuer identifier, exactly one value; a multi-value request is refused with invalid_request, and an audience the IdP has no trust relationship with gets invalid_target (Sections 4.3, 4.3.3). The IdP validates that the subject token really is what it claims to be - a token the IdP itself issued for SSO, not an access token or a grant being recycled (Section 4.3.3) - applies its policy, and returns the signed ID-JAG (Section 4.3.4).

Wire shape · the exchange request
POST /token HTTP/1.1
Host: idp.example
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
&audience=https://chat.example/
&resource=https://api.chat.example/
&scope=chat.read
&subject_token=eyJhbGciOiJSUzI1NiIs...      (the ID Token)
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOiJSUzI1NiIs...   (client authentication)
Wire shape · the response carries the grant itself
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "issued_token_type": "urn:ietf:params:oauth:token-type:id-jag",
  "access_token": "eyJhbGciOiJSUzI1NiIs...  (the ID-JAG itself)",
  "token_type": "N_A",
  "expires_in": 300
}

The ID-JAG rides in access_token because Token Exchange requires the parameter; it is a grant, not an access token - hence token_type=N_A and a distinct issued_token_type (Section 4.3.4).

04 · What the IdP signed

The ID-JAG is a JWT whose header typ MUST be oauth-id-jag+jwt (Section 3.1). RS256 is mandatory to implement for both authorization servers, and Resource Authorization Servers SHOULD also support ES256 (Section 3, RFC 7523 Section 5). Seven claims are REQUIRED (Section 3.1): iss and aud name the two ends of this specific trust relationship, sub and optionally sub_id identify the user,client_id names the client at the Resource AS that will act for the user, andjti, exp, iat make the grant trackable and perishable.

The ID-JAG payload, annotated
{
  "typ": "oauth-id-jag+jwt",                   (header)

  "iss": "https://idp.example/",     REQUIRED  the IdP AS
  "sub": "U019488227",               REQUIRED  the user, in the IdP's namespace
  "aud": "https://chat.example/",    REQUIRED  the Resource AS issuer
  "client_id": "f53f191f9311af35",   REQUIRED  the client at the Resource AS
  "jti": "9e43f81b64a33f20116179",   REQUIRED  unique token id
  "exp": 1311281270,                 REQUIRED  expiry (NumericDate)
  "iat": 1311280970,                 REQUIRED  issued at

  "resource": "...",    OPTIONAL  RFC 8707 resource identifiers
  "scope": "...",       OPTIONAL  space-separated scopes
  "email": "...",       OPTIONAL  RECOMMENDED with aud_sub for subject resolution
  "aud_sub": "...",     OPTIONAL  the Resource AS's own id for the user
  "sub_id": { ... }     OPTIONAL  SAML NameID subject identifier (Section 3.2)
}

Full claims table with every OPTIONAL member: the draft primer. Rule-by-rule validation of each claim: the rules explorer.

Two claims deserve the emphasis they get in the draft. aud MUST carry exactly the receiving Resource AS's issuer - a string or a single-element array, enforced to prevent audience injection (Section 4.4.1). And subject resolution is the point of the whole document: it is RECOMMENDED that the grant carry email and/or aud_sub so the Resource AS can resolve the user to a local account, including just-in-time provisioning when the user has never signed in there before (Section 3.1).

05 · Hop two: the jwt-bearer grant

The client redeems the ID-JAG at the Resource AS's token endpoint as an ordinary RFC 7523 assertion (Section 4.4). The Resource AS SHOULD require client authentication here too; if the client does not authenticate, the request MUST carry a sender-constrained grant instead (Section 4.4). Then the processing rules run: verify typ, verify the signature against the trusted issuer's keys from itsjwks_uri, check aud, check client_id against the authenticated client, check exp - each failure maps to invalid_grant, while a valid grant for an unentitled user maps to access_denied (Sections 4.4.1, 4.4.3).

Wire shape · the grant redemption
POST /token HTTP/1.1
Host: chat.example
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJSUzI1NiIs...          (the ID-JAG)
Wire shape · an ordinary access token
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "token_type": "Bearer",
  "access_token": "2YotnFZFEjr1zCsicMWpAA",  (an ordinary access token)
  "expires_in": 86400,
  "scope": "chat.read"
}

No refresh token SHOULD come back: the ID-JAG replaces it, and the client MAY re-submit the same grant until it expires (Section 4.4.4).

06 · Why the RAS keeps minting

The obvious alternative - the IdP issuing resource access tokens directly - is exactly what the draft does not do. Unlike profiles where the IdP issues the access tokens the Resource Server consumes, XAA preserves the Resource Authorization Server as the issuer of access tokens for its own resources (Section 1). The IdP centralizes identity, SSO trust, and the decision to broker access; the Resource AS keeps independent control of its tokens, its scopes, and its local policy (Section 1). The alternative couples every API to a growing list of foreign token issuers, and the draft pins the restraint down: an IdP MUST NOT honor its own ID-JAG by issuing access tokens unless explicitly configured, and then no more generously than a direct issuance would have been (Section 9.4).

The cost of the shape is the mapping work in Section 5: the IdP must know which client_id at the Resource AS corresponds to this client, and which sub value the Resource AS's own SSO registration would carry - both established out of band. Client ID Metadata Documents are called out as a namespace that removes the client mapping (Section 5.1). Metadata advertisement closes the loop: an IdP SHOULD list the id-jag token type in identity_chaining_requested_token_types_supported, a Resource AS SHOULD list the grant profile in authorization_grant_profiles_supported, and that advertisement MUST be coupled to grant_types_supported carryingurn:ietf:params:oauth:grant-type:jwt-bearer (Sections 7.1, 7.2).

07 · The attack/defense map
If this is triedThe draft's answerCite
Grant minted for a different resource serveraud MUST carry exactly the receiving Resource Authorization Server’s issuer identifier, as a string or a single-element array; anything else is rejected with invalid_grantSection 4.4.1
Forged grantthe signature MUST validate against the trusted issuer’s keys, obtained from the issuer’s jwks_uri; RS256 is mandatory to implementSections 4.4.1 and 3
Wrong token type smuggled in as the grantthe JWT typ header MUST be oauth-id-jag+jwt - explicit typing, as recommended by RFC 8725 Section 3.11Sections 3.1 and 4.4.1
Cross-service identity confusion via SAML NameIDsa pairwise nameid MUST be the value the IdP would issue to the Service Provider matching the target RAS; the RAS MUST NOT resolve on the nameid alone without the qualifying scope members, and transient NameID formats MUST NOT be usedSection 3.2.2
Stolen subject token (ID Token, SAML assertion, or refresh token)client authentication at the IdP is the remaining barrier; stored subject tokens SHOULD sit in a separate protection domain from client credentials, out of logs, dumps, and URIsSections 9.2 and 4.3
Unauthenticated presenter at the Resource ASthe Resource AS MUST reject a request that has neither client authentication nor a sender-constrained ID-JAG; key binding comes via DPoP or mTLS through the cnf claimSections 4.4 and 9.11
Replay of a captured grantthe Resource AS MAY enforce single use by tracking jti, MAY cap exp by policy, and SHOULD NOT return a refresh token - the ID-JAG itself is the re-submittable grantSection 4.4.4
Trust-domain broadening by the IdPan IdP MUST NOT issue access tokens in response to an ID-JAG it issued itself unless explicitly configured, and then no more generously than direct issuance would allowSection 9.4

And the map has honest blanks the draft says out loud: replay tracking is a MAY, so two compliant Resource ASs can differ (Section 4.4.4); subject-uniqueness rules need Resource-AS-side state a token inspector does not have (Section 3.1); and policy - which user may reach which app - is the IdP's administrator-defined judgment, not a protocol outcome (Section 4.3.3). Where the line sits between "the protocol guarantees" and "the deployment decides" is the difference between validating a grant and trusting a system.

08 · References

Normative - the documents this page's cites resolve to:

Informative - status and context: