In this article
September 8, 2026
September 8, 2026

Inside the ID-JAG: How enterprise-managed authorization actually works

The consent screen is no longer where access gets decided. Here is what replaced it, and what your MCP server now has to validate.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

The Enterprise-Managed Authorization extension to MCP reached Stable in June 2026. The pitch is easy to repeat: an admin authorizes an MCP server once in the company IdP, and every employee's client connects automatically, no consent screens. Okta shipped it as Cross App Access and became the first supported IdP. Anthropic implemented it across Claude, Claude Code, and Cowork, VS Code added support, and as of the extension's launch announcement Asana, Atlassian, Canva, Figma, Granola, Linear, and Supabase accept it, with Slack and others adding support.

What's harder to find is a straight account of what's moving over the wire, and what your MCP server has to get right to accept it.

The mechanism is a token type called an ID-JAG: an Identity Assertion JWT Authorization Grant, defined in draft-ietf-oauth-identity-assertion-authz-grant. It is not an access token. It is not an ID token. It is a short-lived, single-audience, signed statement from an IdP that says: this user, at this company, is allowed to let this client reach that resource server, with these scopes. Your authorization server trades it for an access token.

This post walks the flow end to end, then covers the five places implementations get it wrong.

!!Note on spec maturity: the ID-JAG draft is at revision -04 (21 May 2026), an active IETF OAuth working group document, not yet an RFC. The MCP extension that profiles it is marked Stable. Claim names and URNs below are from -04 and could still shift.!!

The three-party problem

Standard MCP authorization is a two-party OAuth flow. Your MCP server is a resource server, your authorization server issues tokens, and the user clicks through a consent screen in a browser to connect a client. It works, and for consumer software the consent screen is the point, since the user is the one deciding what touches their data.

Inside a company, that model inverts. The user isn't the right decision-maker for whether the engineering team's AI code editor may reach the source control MCP server; IT is. And the consent screen becomes a tax paid dozens of times per employee, per onboarding, with no central record of what got granted.

EMA introduces a third party who was already there: the IdP the company uses for SSO. The extension's insight is that if the MCP client and the MCP server both already trust the same IdP for single sign-on, that shared trust is enough to authorize between them, with no direct user interaction needed.

That precondition is load-bearing. The spec constrains the profile to deployments where the client has a relying-party relationship with the IdP and the resource authorization server independently trusts that same IdP for SSO and subject resolution. If your MCP server has no SSO relationship with a customer's IdP, EMA has nothing to stand on for that customer.

Concept diagram titled "the trust triangle", showing three participants. At the top, the enterprise IdP, examples Okta and Entra, highlighted as the pivotal party. At the bottom left, the MCP client, examples Claude Code and VS Code. At the bottom right, your MCP server together with its authorization server. Two double-headed arrows run from the IdP down to the client and down to the MCP server, each labeled "SSO trust, pre-existing". A third, thinner double-headed arrow runs along the bottom between the client and the MCP server, labeled "OAuth client registration"; below it, the words "user consent redirect" appear struck through and greyed out, tagged "removed by EMA". A note beside the IdP reads: both SSO edges are preconditions, so if your MCP server has no SSO relationship with a customer's IdP, EMA has nothing to stand on for that customer.

The flow, step by step

Four moves: SSO, exchange, redeem, call.

Sequence diagram of the Enterprise-Managed Authorization flow across five participants: browser, MCP client, enterprise IdP, MCP authorization server, and MCP server. Phase 1, single sign-on, is the only phase involving the browser: it carries the user to the IdP to authenticate, and the client ends up holding an ID token. In phase 2, token exchange, the client posts the ID token to the IdP, the IdP evaluates administrator policy against groups, roles, conditional access, and scopes, and returns an ID-JAG whose audience is the MCP authorization server. In phase 3, the client presents that ID-JAG as a JWT bearer assertion, the authorization server validates its typ, signature, audience, client_id, and subject, and issues an access token whose audience is the MCP server. In phase 4, the client calls the MCP server with that access token in a loop until it expires. The browser lane is greyed out and labeled "not involved" from phase 2 onward.

1. SSO gets the client an identity assertion

The user signs into the MCP client through the enterprise IdP. Ordinary OpenID Connect: redirect, authenticate, code, token exchange, ID token. MFA and conditional access apply here as they always did. The client holds onto the ID token, which is the input to everything that follows.

SAML shops have two paths. A SAML 2.0 assertion is a first-class subject token: implementations MUST accept identity assertions, with subject_token_type=urn:ietf:params:oauth:token-type:saml2 and the assertion base64-encoded. But the draft also offers a detour for clients that would rather not touch SAML on every exchange. Swap the assertion for a refresh token at the IdP once (requesting openid offline_access), then use that refresh token as the subject token thereafter. Either way the IdP MUST verify the SAML Audience maps to the authenticated OAuth client_id before issuing. Otherwise a client could present an assertion minted for a different service provider.

2. Token exchange gets an ID-JAG

Here's the request that does the real work. RFC 8693 token exchange against the IdP's token endpoint:

  
POST /oauth2/token HTTP/1.1
Host: acme.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://auth.chat.example/
&resource=https://mcp.chat.example/
&scope=chat.read+chat.history
&subject_token=eyJraWQiOiJzMTZ0cVNtODhwREo4VGZCXzdrSEtQ...
&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&client_id=2ec954a1d60620116d36d9ceb7
&client_secret=a26d84873504215a34a86d52ef5cd64f4b76
  

Two parameters carry the addressing, and mixing them up is the most common early mistake:

  • audience identifies your authorization server, meaning whoever may consume this grant. In the MCP profile this MUST be the issuer identifier. (The base draft is looser: IdPs MUST support issuer identifiers but MAY also accept implementation-specific values like URNs, which they then resolve before setting aud.)
  • resource is the resource identifier of your MCP server, meaning what the client wants to reach, per RFC 8707. Optional in both the base draft and the MCP profile; if it is present, the MCP profile requires it to be the MCP server's resource identifier.

audience names the party that validates. resource names the thing being accessed. They are almost never the same URL.

The IdP then evaluates administrator policy. This step is the entire product: group membership, role, conditional access, which clients may talk to which servers, and, where your scopes are granular enough, which scopes each group may request. The spec's own example is engineering getting read-only access from a code editor to source control while marketing gets read-write on internal docs. If policy says no, the client gets an OAuth error and never sees a token. Note that the IdP may narrow what it grants: granted scopes may be a subset of requested ones, and the IdP MUST reflect the granted values in the issued ID-JAG.

The response:

  
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "issued_token_type": "urn:ietf:params:oauth:token-type:id-jag",
  "access_token": "eyJhbGciOiJIUzI1NiIsI...",
  "token_type": "N_A",
  "scope": "chat.read chat.history",
  "expires_in": 300
}
  

Read that carefully, because it is easy to misread. The ID-JAG arrives in a field named access_token, which RFC 8693 requires for historical reasons, with token_type: "N_A", because it is not an access token. You cannot send this to an MCP endpoint as a bearer token. The issued_token_type is the field that tells you what you actually got.

3. The ID-JAG itself

  
{
  "typ": "oauth-id-jag+jwt"
}
.
{
  "jti": "9e43f81b64a33f20116179",
  "iss": "https://acme.idp.example",
  "sub": "U019488227",
  "email": "user@example.com",
  "aud": "https://auth.chat.example/",
  "resource": "https://mcp.chat.example/",
  "client_id": "f53f191f9311af35",
  "exp": 1311281970,
  "iat": 1311280970,
  "scope": "chat.read chat.history"
}
.
signature
  

Always present: iss, sub, aud, client_id, jti, exp, iat. Then a set of optional claims whose actual optionality varies more than the spec's summary table suggests:

  • resource and scope: the authorization payload. scope reflects what the IdP granted, which may be narrower than what the client asked for.
  • email and aud_sub: RECOMMENDED, not merely permitted, precisely because you'll need them for subject resolution and just-in-time provisioning.
  • tenant: nominally optional, but a conditional MUST. The IdP must include it when the issuer is multi-tenant and the tenant context matters to you.
  • aud_tenant and aud_sub: included when your authorization server is multi-tenant and the IdP knows which of your tenants the user belongs to. Note the asymmetry: tenant describes the IdP side, aud_tenant describes yours.
  • sub_id: carries an SSO subject identifier in a different namespace, typically a SAML NameID. It never replaces sub; when both are present they MUST identify the same user.
  • auth_time, acr, amr: how and when the user authenticated, if you want to make decisions on that.
  • authorization_details: Rich Authorization Requests, for structured grants beyond scope strings.
  • cnf: not in the claims table at all. It lives in the sender-constraining section, and when DPoP is in play the IdP MUST include it with a jkt thumbprint.

The typ header is a MUST: oauth-id-jag+jwt, following JWT best practice on typed tokens. It exists so nothing downstream can be confused into accepting an ID token, an access token, or an ID-JAG in place of one another.

Lifetimes are short but unspecified. The example response says expires_in: 300; the payload examples work out to about 17 minutes. There's no normative maximum and no clock-skew guidance, so pick your own tolerance and document it.

4. Redeeming it for an access token

The client presents the ID-JAG to your authorization server as an RFC 7523 JWT bearer assertion:

  
POST /oauth2/token HTTP/1.1
Host: auth.chat.example
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJIUzI1NiIsI...
&client_id=https://client.example.com/client.json
  

The client authenticates with the credentials it has registered with your authorization server, not the IdP's. If it isn't pre-registered, the MCP profile lets it use its Client ID Metadata Document as its client ID and optionally authenticate with private_key_jwt.

You respond with an ordinary OAuth token response. In the MCP profile, the issued access token MUST be audience-restricted to the MCP server named in the resource claim, which leaves an unstated gap, since resource is optional. When it's absent you have nothing to bind to and need a documented fallback: a default resource indicator, or rejecting the request. Decide which, because clients differ on whether they send it.

And that's it. No browser. No consent screen. No redirect.

What your authorization server MUST check

The draft's validation rules pull in all of RFC 7521 §5.2 by reference and then add specifics. The four that matter most:

  • Typed header. Validate typ is oauth-id-jag+jwt. This is your defense against token substitution across the three token types now in play.
  • Audience, exactly. The aud claim MUST contain your authorization server's issuer identifier. It may be a string or an array, but if it's an array it MUST have exactly one element. Mismatch means rejecting with invalid_grant. The draft names the threat directly: audience injection.
  • Client continuity. The client_id claim MUST identify the same client as the client authentication on the request. If the authenticated client is foo and the claim says bar, reject with invalid_grant. This preserves the OAuth client binding across the trust boundary.
  • Subject resolution. sub is the primary stable identifier, unique when scoped as iss+sub for single-tenant issuers and iss+tenant+sub for multi-tenant ones. If you're consuming sub_id for SAML NameIDs, the rules tighten considerably: you MUST compare every member of the NameID structure that participates in subject resolution for that SAML issuer, and you MUST NOT resolve on the bare nameid value unless local policy explicitly says that's the subject identifier. When a NameID is scoped to a service provider, sp_name_qualifier is part of the namespace.

There's one more, easy to miss because it's a negative requirement: you MUST NOT use sub_id.issuer to establish trust in the ID-JAG. Validate the token on iss, signature, audience, expiry, and client binding first. Only then, and only when the validated issuer is explicitly associated with that SAML issuer through your local configuration or trusted federation metadata, may you use the NameID.

Errors are thin on purpose. The draft defines no new codes: invalid_grant for essentially every validation failure, and insufficient_user_authentication (with a max_age, similar to the step-up challenge protocol in RFC 9470) when the IdP wants step-up authentication before it will issue the grant. Step-up is also the one corner of the draft carrying an open editorial "TBD": it may move into the authorization request in a later revision.

Five things that will bite you

1. The ID-JAG is reusable by design, but only for you. If you've built assertion handling before, your instinct is that a JWT bearer assertion is single-use and you should track jti in a replay cache. That instinct is wrong here. The draft explicitly permits clients to re-submit the same ID-JAG to mint a new access token when the old one expires, and says your authorization server SHOULD NOT return a refresh token for an ID-JAG exchange. The ID-JAG is the refresh token, structurally. Enforce single use and you break clients.

The bound on that reuse is the audience, and it's a MUST NOT: the same ID-JAG must never be reused as the grant for a different downstream authorization server. One grant, one audience, replayable within it. (Relatedly, an IdP MUST NOT issue access tokens against an ID-JAG it issued itself in the same trust domain; this is a cross-domain protocol by construction.) There's no section titled "replay" in the draft. The reuse rules live in the cross-domain security considerations and in RFC 7521 §5.2, pulled in by reference. Decide deliberately rather than assuming.

2. Account linking is the actual work. The protocol hands you a sub in the IdP's namespace, and possibly an email and an aud_sub. It does not tell you which of your users that is. The extension's docs give the shape of an answer, namely use sub as the primary stable identifier and fall back to email for matching accounts created before EMA was configured. But that guidance lives on the documentation page, not in the normative spec, and it stops well short of an implementation.

What you need: a persistent mapping from (iss, tenant, sub) to your user IDs, a first-contact path (match on email, or JIT provision), and a decision about what happens when an email matches an account carrying a different sub. That last case is where account-takeover bugs live. The spec is silent; it's yours to get right.

3. Cross-domain client_id is a mapping problem. The client_id claim is the client's identifier at your authorization server, which the draft notes may differ from the client's identifier at the IdP, since those are independent relationships in different trust domains. Somebody has to know that mapping, and the draft's answer is that the IdP maintains a record obtained "by out-of-band mechanisms." That's a real operational burden per client per server. Client ID Metadata Documents dissolve it: a URL-as-client-ID is a global namespace, so there's nothing to map. If you support CIMD, EMA gets meaningfully easier to roll out.

4. Confidential clients only, so keep the interactive path. The draft's security considerations say this SHOULD only be supported for confidential clients, and public clients SHOULD use the authorization code grant with its interactive consent screen. Requiring a confidential client, in the draft's words, "helps to prevent" the IdP from delegating access to any valid client at your authorization server. Practically: expect to support both paths indefinitely. Your discovery metadata advertises urn:ietf:params:oauth:grant-profile:id-jag in authorization_grant_profiles_supported, clients attempt EMA silently, and fall back to authorization_code when it isn't available.

Two easy things to get wrong here. Advertising that profile obliges you to also list urn:ietf:params:oauth:grant-type:jwt-bearer in grant_types_supported. It's a MUST, and a one-line omission that breaks discovery. And EMA is an MCP extension, so there's a declaration at the MCP layer too: clients signal support under _metaio.modelcontextprotocol/clientCapabilitiesextensions, and your server declares the extension in its authorization metadata. Extensions are opt-in and never active by default.

5. Don't leak your federation topology. The draft is blunt: you MUST NOT use authorization_grant_profiles_supported to disclose issuer allow-lists. Advertising which IdPs you trust exposes customer relationships and tenant configuration to anyone who curls your metadata. Advertising the profile means only that you implement the processing rules, not that any particular issuer, tenant, client, or subject will be accepted. If you need a way for clients to check acceptance ahead of time, the draft permits a protected discovery endpoint that requires client authentication and answers only for the authenticated client.

What EMA does not give you

Two limits worth stating plainly, because the "centralized governance plane" framing can oversell them.

  • The IdP's visibility stops at token issuance. The extension's own security considerations say it: the IdP participates in issuing the access token, but that visibility "does not extend to the actual MCP traffic between the MCP Client and Server." Admins get control over whether a connection may exist and at what scope. They do not get a record of which tools got called or what data moved. If you're selling EMA internally as an audit story, that's the line, and the audit trail for actual tool invocation is still yours to produce.
  • Trust establishment is out of scope. The draft specifies no normative mechanism for how your authorization server discovers the IdP's signing keys or comes to trust an issuer. JWKS appears only in examples. There's nothing on key rollover, kid handling, or caching. What is normative is that for each trusted issuer you MUST be configured to recognize that issuer, resolve its asserted identities to local principals, and associate the grant with the correct client relationship. That configuration, probably per customer and probably in your dashboard, is work the spec leaves entirely to you.

Where this leaves server builders

If you run your own authorization server, EMA is a real implementation project: a new grant type, JWT validation against per-tenant trusted issuers, an identity mapping layer, and a customer-facing way to configure it all.

If AuthKit is your authorization server, ID-JAG token exchange is handled for you: the validation, issuer trust, and token issuance all sit on our side of the line. Cross App Access is in early access; get in touch to have it turned on for your environment. Account linking semantics for your own users, though, remain a decision only you can make.

Either way, the shift underneath is worth naming. MCP's authorization model was built around a user clicking "allow." EMA doesn't delete that consent so much as move it: the decision now happens earlier, in an admin console, made by someone who isn't the user, in a system your server has never spoken to directly. For enterprise deployment that's the right trade, and it's the trade the extension exists to make.

But be precise about what it costs. For EMA-enabled clients the consent screen is no longer where access gets decided, which means it's no longer a place where a user can notice something and decline. If you were treating that screen as a backstop against an over-broad grant, the backstop has moved somewhere you don't control and can't see.