Enterprise-managed authorization and client credentials for MCP

Enterprise-managed authorization and client credentials for MCP

Core Model Context Protocol (MCP) authorization pictures one person in a browser, approving one client for one server. That fits less well when a company’s information technology (IT) team wants to decide centrally who may use which server, or when the caller is a scheduled job with nobody there to click anything. Two official extensions cover those cases.

This is part 28 of my series on what an MCP server does under the 2026-07-28 specification, after part 27 on Skills over MCP. It builds on part 18 for core authorization and part 23 for how extensions work. The facts are as I read them in October 2026.

In brief

  1. Two official extensions cover what core authorization handles poorly. Enterprise-Managed Authorization (stable) puts MCP access under the company’s identity provider (IdP); OAuth Client Credentials (draft) serves callers with no user present.
  2. In the enterprise flow, the user never sees your authorization server’s login page. The client trades a corporate sign-in for an Identity Assertion JSON Web Token (JWT) Authorization Grant (ID-JAG) at the IdP, then the ID-JAG for an access token.
  3. For client credentials, prefer a signed JWT assertion to a client secret. The private key never leaves the service.
  4. Support is early. As of October 2026, the mainstream chat and integrated development environment (IDE) hosts list neither extension.

Which authorization model fits

Both extensions live in the ext-auth repository:

ExtensionIdentifierStatus, October 2026Solves
Enterprise-Managed Authorizationio.modelcontextprotocol/enterprise-managed-authorizationStableIT controls MCP access centrally through the corporate IdP, with no per-user, per-server consent
OAuth Client Credentialsio.modelcontextprotocol/oauth-client-credentialsDraftMachine-to-machine access with no user present

Both are optional and additive: they don’t change core behaviour, they compose with each other, and they’re versioned independently.

Which one you need depends on who is calling. Core authorization uses a Proof Key for Code Exchange (PKCE), and a local server on standard input and output (stdio), as in part 7, takes its credentials from the environment:

Who is calling the MCP server? → Is a human user present?; Is a human user present? → Is access governed centrally by an enterprise IdP?: yes; Is access governed centrally by an enterprise IdP? → Enterprise-Managed Authorization with an ID-JAG: yes; Is access governed centrally by an enterprise IdP? → Core MCP authorization authorization code + PKCE: no; Is a human user present? → Local process on the user's machine?: no; Local process on the user's machine? → stdio: credentials from the environment: yes; Local process on the user's machine? → OAuth Client Credentials prefer JWT assertions: noWho is calling the MCP server?Is a human user present?Is access governedcentrally by anenterprise IdP?Enterprise-ManagedAuthorizationwith an ID-JAGCore MCP authorizationauthorization code+ PKCELocal process on theuser's machine?stdio: credentialsfrom the environmentOAuth Client Credentialsprefer JWT assertionsyesyesnonoyesno

Enterprise-managed authorization

Letting each user authorize each server is right for consumers. In a company it causes friction and gaps: employees shouldn’t have to understand every server’s authorization details, security teams can’t enforce consistent policy, and onboarding and offboarding mean authorizing, then revoking, dozens of services by hand.

The extension makes the organization’s IdP (Okta, Microsoft Entra ID or a corporate single sign-on (SSO) system) the authoritative decision-maker. Users sign in to the MCP client with corporate SSO. The client gets an ID-JAG from the IdP and exchanges it at your MCP server’s authorization server, which validates it against the IdP’s JSON Web Key Set (JWKS) and issues an access token. The user is never redirected to that authorization server’s login page.

MCP client → Enterprise IdP: browser redirect, user signs in (corporate SSO); Enterprise IdP → MCP client: authorization code, via the browser; MCP client → Enterprise IdP: token request; Enterprise IdP → MCP client: ID token (client stores it); Note (MCP client): User is signed in to the client; MCP client → Enterprise IdP: exchange ID token for an ID-JAG (target: this MCP server); Note (Enterprise IdP): Evaluate policy: groups, roles, conditional access; Enterprise IdP → MCP client: ID-JAG; MCP client → MCP authorization server: token request with ID-JAG; Note (MCP authorization server): Validate ID-JAG signature (IdP JWKS), issuer, audience, expiry; MCP authorization server → MCP client: MCP access token; loop; MCP client → MCP server (resource): MCP request + access token; MCP server (resource) → MCP client: responseMCP server(resource)MCP authorizationserverEnterprise IdPMCP clientUser is signed into the clientEvaluate policy:groups, roles,conditional accessValidate ID-JAGsignature (IdP JWKS),issuer, audience,expiryloop​browser redirect,user signs in(corporate SSO)authorization code,via the browsertoken requestID token(client stores it)exchange ID tokenfor an ID-JAG (target:this MCP server)ID-JAGtoken requestwith ID-JAGMCP access tokenMCP request+ access tokenresponse

What the organization gets:

  • Central policy. The IdP holds the approved MCP servers and each one’s access rules.
  • One sign-in. No per-server consent prompts.
  • Enforcement before issuance. Users without access never receive a token.
  • Revocation in one place. Removing access at the IdP covers every client.
  • An audit trail for compliance.

What each party does

PartyDuties
MCP clientDeclare the extension on each request. Support SSO with the enterprise IdP and keep the identity assertion, an OpenID Connect (OIDC) ID token or a Security Assertion Markup Language (SAML) assertion. Exchange it for an ID-JAG, then redeem that at the MCP authorization server. Support IdP configuration at the organization level. Handle scope errors gracefully
MCP serverDeclare the extension in its authorization metadata to require the enterprise flow. Optionally publish a resource descriptor, so admins can configure policy in the IdP console
MCP authorization serverValidate ID-JAGs: signature against the IdP’s JWKS, issuer, audience, expiry. Map claims to permissions. Link accounts using sub as the stable key, falling back to email for accounts created before enterprise management

In Python

In the official Python software development kit (SDK), mcp 2.3.0, IdentityAssertionOAuthProvider (in mcp.client.auth.extensions.identity_assertion) covers the last exchange, presenting the ID-JAG with the JWT-bearer grant of Request for Comments (RFC) 7523. Getting the ID-JAG is left to you, through an assertion_provider callback. It serves confidential clients only, so it needs a client secret. It also takes your authorization server’s issuer as configuration, so the MCP server can’t redirect the ID-JAG.

OAuth client credentials

Background services, scheduled jobs, continuous integration and delivery (CI/CD) pipelines, server-to-server integrations and daemons have no human present and can’t open a browser. This extension adds the OAuth 2.0 client credentials grant, so they authenticate with the application’s own credentials instead of a user’s. If a human should explicitly authorize access, use the standard flow.

It allows two credential formats:

JWT bearer assertion, RFC 7523Client secret
Secret on the wireNo, the private key stays localYes
LifetimeShort, minutesLong-lived until rotated
Risk if leakedA limited windowSilent impersonation until rotation
GuidancePreferredStore in a secrets manager, rotate regularly, scope minimally
  • Clients declare the extension on each request, obtain a token before connecting, send it as a bearer token, and refresh it before expiry. These tokens often live shorter than user tokens.
  • Servers validate the JWT’s signature and claims against the authorization server’s JWKS, check scopes per operation and, as a recommendation, advertise the extension in server/discover.

In Python

mcp 2.3.0 has a provider for each format, in mcp.client.auth.extensions.client_credentials: ClientCredentialsOAuthProvider for a client ID and secret, and PrivateKeyJWTOAuthProvider for a signed assertion (the TypeScript SDK’s are ClientCredentialsProvider and PrivateKeyJwtProvider). Each plugs into httpx2, the SDK’s Hypertext Transfer Protocol (HTTP) client. The assertion comes from your assertion_provider callback: in production, the SDK says, usually from a workload identity provider. For testing and simple setups, SignedJWTParameters signs it with your private key, as here:

import httpx2

from mcp.client import Client, advertise
from mcp.client.auth.extensions.client_credentials import (
    PrivateKeyJWTOAuthProvider,
    SignedJWTParameters,
)
from mcp.client.streamable_http import streamable_http_client
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken

MCP_URL = "https://mcp.example.com/mcp"
CLIENT_ID = "report-service"  # registered with the authorization server, with its public key


class MemoryTokenStorage:
    """The SDK's TokenStorage protocol, kept in memory."""

    def __init__(self) -> None:
        self.tokens: OAuthToken | None = None
        self.client_info: OAuthClientInformationFull | None = None

    async def get_tokens(self) -> OAuthToken | None:
        return self.tokens

    async def set_tokens(self, tokens: OAuthToken) -> None:
        self.tokens = tokens

    async def get_client_info(self) -> OAuthClientInformationFull | None:
        return self.client_info

    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
        self.client_info = client_info


def make_provider(private_key_pem: str) -> PrivateKeyJWTOAuthProvider:
    """The private key signs assertions here and never leaves this service."""
    assertions = SignedJWTParameters(issuer=CLIENT_ID, subject=CLIENT_ID, signing_key=private_key_pem)
    return PrivateKeyJWTOAuthProvider(
        server_url=MCP_URL,
        storage=MemoryTokenStorage(),
        client_id=CLIENT_ID,
        assertion_provider=assertions.create_assertion_provider(),
        issuer="https://auth.example.com",  # the only server an assertion is sent to
    )


async def nightly_report(http: httpx2.AsyncClient) -> str:
    """Runs with no user present; the provider gets the token."""
    declared = advertise("io.modelcontextprotocol/oauth-client-credentials")
    async with Client(streamable_http_client(MCP_URL, http_client=http), extensions=[declared]) as client:
        result = await client.call_tool("list_files", {})
        return result.content[0].text

I ran it with httpx2.AsyncClient(auth=make_provider(key)), in one process, against the SDK’s own MCP server, a stand-in authorization server and a throwaway key pair. The requests:

POST mcp.example.com/mcp -> 401
GET mcp.example.com/.well-known/oauth-protected-resource/mcp -> 200
GET auth.example.com/.well-known/oauth-authorization-server -> 200
POST auth.example.com/token -> 200
POST mcp.example.com/mcp -> 200
POST mcp.example.com/mcp -> 200
POST mcp.example.com/mcp -> 200

What the token endpoint received:

grant_type: client_credentials
client_assertion_type: urn:ietf:params:oauth:client-assertion-type:jwt-bearer
resource: https://mcp.example.com/mcp
scope: files:read
client_secret sent: False
assertion claims: {'iss': 'report-service', 'sub': 'report-service', 'aud': 'https://auth.example.com'} lifetime: 300 s

What the run showed:

  • No secret on the wire. The JWT travels as a client assertion beside grant_type=client_credentials and lives 300 seconds, the SignedJWTParameters default. The scope came from the server’s Protected Resource Metadata.
  • One line declares the extension. advertise(...) put it in the client capabilities of every request’s _meta.
  • issuer= pins where credentials go. Without it, 2.3.0 warns that 3.0 will require it.
  • Tokens come after a 401. The first token came only after a 401 and a discovery of both metadata documents. When the two-second demo token expired, the next request went the same way. That isn’t yet what the extension asks of clients: a token before connecting, and a refresh before expiry.

Support, and my advice for Entra ID

The official client matrix lists Enterprise-Managed Authorization in Archestra.AI and in Apify’s mcpc command-line client, and Client Credentials in mcpc. As of October 2026, the mainstream chat and IDE hosts don’t list either. Enterprise-Managed Authorization also needs support on the IdP’s side, so check with your IdP vendor.

What follows is my advice, not part of either extension. If your MCP servers sit behind Entra ID in a large organization, keep Enterprise-Managed Authorization on your roadmap. It removes the per-user consent friction that Entra’s lack of dynamic registration otherwise forces you to work around (part 19 covers what to expect from Entra today). Until hosts support it, pre-registered clients with core authorization, and Client Credentials for automation, are the practical options.

Method and caveats

  • Built from my guide’s chapter on authorization extensions, against the 2026-07-28 specification; the status and support facts are as of October 2026.
  • The Python sample ran against mcp 2.3.0 on 9 October 2026, in one process with no network; the output is what it printed.
  • Where my notes were less precise about the Python SDK (which classes it ships, and how its providers renew a token), this article follows the installed package.