Skip to main content
Zero-Touch OAuth for MCP: authorizing AI agents without a browser

Zero-Touch OAuth for MCP: authorizing AI agents without a browser

· 37 min read
Practical guides for developers

Standard OAuth authorization code flow requires a browser redirect and a user to click "Allow." AI agents running headlessly have no UI and no interactive session, so they cannot complete that redirect loop. The Enterprise-Managed Authorization (EMA) extension removes the per-server consent screen entirely by routing authorization through the organization's existing identity provider. Announced stable on June 18, 2026, EMA is already supported by Claude, VS Code 1.123, and a set of MCP servers including Atlassian, Linear, Figma, Asana, Canva, Granola, and Supabase.

Standard OAuth 2.1 vs enterprise-managed authorization

The table below shows what changes between a standard per-user OAuth flow and the EMA flow. The underlying HTTP calls and JWT format remain the same. What changes is where authorization decisions are made and whether a user ever sees a consent screen.

AUTHORIZATION · COMPARISON

Standard OAuth 2.1 vs Enterprise-Managed

Authorization (EMA)

DIMENSIONSTANDARD OAUTH 2.1 (PER-USER)ENTERPRISE-MANAGED (EMA + ID-JAG)Who authorizesEach end user,per serverOrganization administrator,onceAuth promptshown to userBrowser consent screenper serverNone afterinitial IdP loginPolicy enforcementpointIndividual user choiceCentralized IdPadmin consoleRevocationPer-client, per-serverIdP level, immediateacross all clientsWorks forheadless agentsNoYesShadow ITvisibilityNone — admins cannotsee what each userconnectedFull audit trailin IdPBest forConsumer apps,individual developersEnterprise MCPdeployments at scale

Why standard OAuth breaks for headless agents

Traditional OAuth requires "redirecting the user to a browser to get consent," but this doesn't work for AI agents. Agents don't have a browser UI, and you can't expect an autonomous process to wait for a manual consent click.

The redirect problem has a second layer for enterprise deployments. When one application wants to access a user's data at another application through traditional OAuth 2.0, the flow completely bypasses the IdP. The two apps communicate directly, and the enterprise IT admin can neither see nor control this integration. At scale, this creates three concrete problems for MCP:

  • Every employee has to authorize every server individually, so onboarding requires manually connecting service after service.
  • Security teams cannot enforce consistent policy, since access is whatever each user authorized, with no central control or audit trail.
  • Work and personal accounts blur together, with no way to require a corporate identity, so a user can connect a personal account to a work tool.

The Device Authorization Grant (RFC 8628) addresses the no-browser constraint for some scenarios by letting a user complete the auth step on a secondary device. It still requires a human to take an action, which means it doesn't fully work for autonomous agents and doesn't solve the central-policy problem.

Which OAuth grant type to use for which MCP scenario

The MCP spec states that "MCP servers SHOULD support the OAuth grant types that best align with the intended audience." The right grant depends on whether a human is present and whether the deployment is enterprise-managed.

OAUTH 2.1 · MCP AUTHORIZATION

Which OAuth grant type to use

for which MCP scenario

SCENARIORECOMMENDED GRANT TYPENOTESAgent acting on behalfof a logged-in userAuthorization Code(OAuth 2.1 with PKCE)PKCE is required forall MCP clientsFully autonomous agentwith no human contextClient CredentialsThe client is anotherapplication, not a humanEnterprise-provisionedconnectors, zero-touchEMA extension(ID-JAG via RFC 8693+ RFC 7523)Requires Okta at launch;all three parties mustopt inHeadless agent whereinteractive flow isimpossible and noenterprise SSO existsDevice AuthorizationGrant (RFC 8628)Still requires a humanaction on a secondarydevice

The ID-JAG grant type should only be used with confidential clients. Public clients should use the authorization code grant with PKCE and redirect the user to the Resource Authorization Server for interactive consent.

How the EMA token chain works

EMA is built on three existing OAuth RFCs chained together with no new cryptographic techniques, just a well-defined profile. "Technically, ID-JAG is a profile built on existing RFCs (Token Exchange + JWT Bearer Grant) — it doesn't invent new cryptographic techniques or new protocols. What it does is extend the trust relationship already established through SSO to API integration. That's it." The user SSO login already happens, and EMA extends that trust to MCP server access without additional user interaction.

Step 1 — User authenticates with the enterprise IdP

This is a standard OpenID Connect flow with nothing EMA-specific. When the user logs into the MCP client, they are redirected to the enterprise IdP for authentication. The IdP authenticates the user (including MFA if configured) and returns an authorization code, which the client exchanges for an ID Token.

{
"id_token": "eyJraWQiOiJzMTZ0cVNtODhwREo4VGZCXzdrSEtQ...",
"token_type": "Bearer",
"access_token": "7SliwCQP1brGdjBtsaMnXo",
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "openid offline_access"
}

The ID Token obtained here will be used in Step 2 as proof that "this user is who they claim to be." Requesting the offline_access scope to get a Refresh Token makes it easier to re-obtain the ID Token later.

Step 2 — Token exchange: get an ID-JAG from the IdP

The client calls the IdP's token endpoint using RFC 8693 Token Exchange, passing the ID Token and requesting a token of type urn:ietf:params:oauth:token-type:id-jag targeted at the MCP server's authorization server.

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://acme.chat.example/
&resource=https://api.chat.example/
&scope=chat.read+chat.history
&subject_token=eyJraWQiOiJzMTZ0cVNtODhwREo4VGZCXzdrSEtQ...
&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=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0...

The critical difference from traditional OAuth happens inside the IdP at this point. Instead of the user clicking a consent button, access is granted or denied based on policies pre-configured by the IT admin. The response carries the ID-JAG in the access_token field with token_type: N_A because this is an assertion, not an access token.

{
"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
}

The token_type is N_A because this is an assertion, not an OAuth access token. It is placed in the access_token field due to RFC 8693 spec constraints.

Step 3 — Present the ID-JAG to the MCP server's authorization server

The client calls the Resource AS token endpoint using RFC 7523 JWT Bearer Grant, presenting the ID-JAG as the assertion.

POST /oauth2/token HTTP/1.1
Host: acme.chat.example
Authorization: Basic yZS1yYW5kb20tc2VjcmV0v3JOkF0XG5Qx2

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=eyJhbGciOiJIUzI1NiIsI...

The Resource AS validates the ID-JAG before issuing a standard Bearer access token. Validation covers six checks:

  1. JWT typ must be oauth-id-jag+jwt
  2. aud must match the Resource AS's own issuer identifier (if an array, it must contain exactly one element)
  3. Signature must be valid using the IdP's JWKS (public key set)
  4. client_id must match the requesting client's authentication
  5. exp must be valid, and jti must not have been seen before
  6. scope, resource, and authorization_details must be evaluated against policy

One important property of the spec is that the resource authorization server still applies local policy. An enterprise IdP can say "this request is allowed to be considered," but the MCP server's authorization server still decides what token to issue, whether to narrow scopes, and whether the user is valid for the target resource.

Step 4 — Use the access token for every MCP request

The result is a standard Bearer access token. The MCP client must include it in the Authorization header for every HTTP request, even within the same logical session.

GET /api/channels/general/history
Host: api.chat.example
Authorization: Bearer 2YotnFZFEjr1zCsicMWpAA

When the access token expires, re-present the ID-JAG to get a new one. When the ID-JAG expires, request a new one from the IdP using the original ID Token or Refresh Token. The Resource AS should not return a Refresh Token, since the ID-JAG functions as the Refresh Token substitute.

What is inside an ID-JAG

An ID-JAG looks like an ID Token, but the aud (audience) claim is the decisive difference. An ID Token says "To the client app: this user is authentic." An ID-JAG says "To the Resource Authorization Server: the client app is allowed to access the API on behalf of this user."

You must never pass an ID Token directly to another domain's authorization server. Per the OpenID Connect spec, only the Relying Party specified in the aud claim may receive and process an ID Token. ID-JAG exists precisely to work around this constraint.

The typ header must be oauth-id-jag+jwt, a typed JWT as recommended by RFC 8725.

Decoded example:

// Header
{
"alg": "ES256",
"typ": "oauth-id-jag+jwt"
}
// Payload
{
"jti": "9e43f81b64a33f20116179",
"iss": "https://acme.idp.example/",
"sub": "U019488227",
"aud": "https://acme.chat.example/",
"client_id": "f53f191f9311af35",
"exp": 1311281970,
"iat": 1311280970,
"resource": "https://api.chat.example/",
"scope": "chat.read chat.history",
"auth_time": 1311280970,
"amr": ["mfa", "phrh", "hwk", "user"],
"email": "alice@acme.example"
}

Claim reference:

ID-JAG · JWT CLAIM REFERENCE

ID-JAG JWT Claim Reference

ClaimRequired / OptionalWhat it meansissRequiredIdP identifier — who issued this tokensubRequiredUser ID at the Resource AS — must matchthe identifier used during SSOaudRequiredResource AS identifier — the intended recipientclient_idRequiredClient identifier — which app will usethis to get an access tokenjtiRequiredUnique JWT ID — prevents replay attacksexp / iatRequiredExpiration and issued-at timestampsresourceOptionalTarget API URL (single URI or URI array)scopeOptionalRequested permissions as a space-separated stringemailOptional (recommended)User email address for accountresolution with pre-existing accountsauth_time / acr / amrOptionalAuthentication time, context class,and methods

The sub claim should be an opaque ID, as iss + sub is unique per user. Including email as an additional claim helps link the enterprise identity to existing accounts that were created before EMA was configured.

How to add EMA support to your MCP implementation

EMA requires all three parties to opt in. If the client, IdP, or MCP server doesn't support it, the standard OAuth flow is the fallback. Extensions are opt-in and never active by default.

What an MCP client must do

Declare EMA support in the initialize request capabilities block:

{
"capabilities": {
"extensions": {
"io.modelcontextprotocol/enterprise-managed-authorization": {}
}
}
}

Beyond declaration, the client must:

  • Support SSO by saving the Identity Assertion (ID Token or SAML assertion) issued during login for later use.
  • Handle ID-JAGs by requesting an ID-JAG from the IdP and exchanging it for an access token when the server indicates that enterprise-managed auth is required. Do not redirect the user to the MCP server's authorization endpoint.
  • Support organization configuration by allowing administrators to configure the enterprise IdP's endpoints at the org level, not per user.
  • Handle scope errors gracefully, since tokens issued by enterprise IdPs may carry scope restrictions that differ from standard MCP authorization.

What an MCP server must do

Declare the extension in your server's authorization server metadata, indicating that clients must use the enterprise-managed flow. Optionally publish a resource descriptor to IdP app catalogs so administrators can configure access policies in their IdP admin console without manual setup.

What an authorization server must do

The authorization server needs to validate incoming ID-JAGs, map IdP claims to permissions, and handle account linking. For validation, apply all six checks listed in Step 3 above. For account linking, use the sub claim as the primary stable identifier and fall back to the email claim for matching against pre-existing accounts that predate EMA configuration.

There is also a cross-domain client_id problem. The client_id inside the ID-JAG must be the identifier your authorization server recognizes for the client, which means the IdP must know the client's client_id at your authorization server, typically managed through an out-of-band mapping table or a Client ID Metadata Document.

How VS Code and Claude implement EMA today

VS Code 1.123 (released June 3, 2026) added EMA as a preview feature. The release notes describe it as: "If your organization centralizes sign-in at a single identity provider (like Entra, Okta, or Auth0), you probably don't want every MCP server running its own Dynamic Client Registration against its own authorization server. This release adds a cross-app authorization (XAA) flow that lets you authenticate once against your enterprise IdP, and then VS Code mints a per-resource token for each MCP server on your behalf."

Setup has two parts. Admins configure the IdP through the policy-managed mcp.enterpriseManagedAuth.idp setting, delivered through Windows Group Policy, macOS managed preferences, or Linux /etc/vscode/policy.json, and it never syncs to the cloud. Individual MCP servers opt in by setting "enterpriseManaged": true in their oauth block in mcp.json.

For Claude, EMA is implemented in the shared MCP layer, covering Claude chat, Claude Code, and Cowork. It is currently in beta for Team and Enterprise plan customers. Cameron Leavenworth, Staff IT Engineer at Ramp, described the result: "Before enterprise-managed auth, onboarding a new hire to their full toolkit meant a queue of per-connector OAuth approvals. Now they log in to Claude on day one already connected — 2,000 employees, provisioned through Okta, zero extra steps."

The VS Code release notes include one honest caveat: "ID-JAG is still an emerging standard and isn't widely adopted yet, but we believe it's the future of cross-app authorization."

What EMA does not solve

EMA is narrowly scoped by design. Four gaps are worth knowing:

Tool-level permissions are still your problem. EMA answers whether a given user can access a given MCP server. It does not address finer-grained controls, like "can this user call this tool." Tool-level access control, audit logging, and observability still require a gateway layer sitting between users and servers.

Azure Entra ID is not supported. MCP requires Dynamic Client Registration (RFC 7591), which enterprise IdPs like Microsoft Entra don't natively support. EMA does not bridge this gap. A gateway control plane fills it without requiring changes to existing identity infrastructure.

Only Okta is supported at launch. Okta is the first and only supported identity provider as of June 2026, using its Cross App Access (XAA) feature. Support for additional providers is described as "coming soon."

All three parties must opt in. If the client, IdP, or MCP server lacks EMA support, the standard OAuth flow is the fallback. With EMA still in beta and Okta as the only IdP, many deployments will hit this constraint for the near term.

RFC stack behind ID-JAG

ID-JAG is a profile that combines existing OAuth building blocks, with no new cryptography. The full specification is available on the IETF datatracker.

OAUTH · ID-JAG

RFC Stack Behind ID-JAG

RFC / DRAFTROLE IN ID-JAGRFC 8693(Token Exchange)Step 2: ID-JAG request to IdPRFC 7523(JWT Bearer Grant)Step 3: presenting ID-JAGto Resource ASOpenID Connect CoreStep 1: obtaining the ID TokenRFC 8414(AS Metadata)IdP and Resource AS discoveryRFC 9728(Protected Resource Metadata)Resource Server discoveryRFC 7519(JWT)ID-JAG formatRFC 8707(Resource Indicators)resource parameterRFC 8725(JWT Best Practices)Typed JWT (oauth-id-jag+jwt)RFC 9396(RAR)authorization_details parameter(draft-02+)RFC 9449(DPoP)Sender-constrained tokens(draft-02+)

The IETF draft is still evolving. VS Code 1.123 references draft-03, so use the canonical datatracker URL rather than a draft-specific link.

EMA ecosystem support as of June 2026

Identity providers

  • Okta (Cross App Access / XAA), the only supported IdP at launch

Clients

  • Claude, Claude Code, Cowork (Team and Enterprise plans, beta)
  • VS Code 1.123 (preview)

MCP servers

  • Asana, Atlassian Rovo, Canva, Figma, Granola, Linear, Supabase (at launch)
  • Slack (coming soon)

Early enterprise customers include Hubspot, Ramp, and Webflow. If you're implementing EMA or want to track progress, the EMA Interest Group is the place to share compatibility reports and follow the extension as it evolves.

About the author

ST
Simple Tech GuidesPractical guides for developers

Simple Tech Guides publishes practical, developer-focused content on frameworks, tools, and platforms.