Agent auth and delegated access, step by step
How an agent acts for a user without holding their passwords: CIMD client registration, the MCP 2026-07-28 OAuth flow with resource indicators and audience checks, no token passthrough, vaulted third-party tokens, CIBA approvals and Cross App Access (ID-JAG).
Prerequisites
- Basic OAuth 2.0 vocabulary (authorization code, access token, refresh token, scope)
- An MCP server you control, or a client you are building
The picture
There are three separate trust hops. Keep them separate, and most agent-auth mistakes disappear:
- Client → MCP server. The agent's MCP client gets a token from the MCP server's authorization server, for that MCP server only (MCP authorization).
- MCP server → third-party API. The server uses a different token, issued by the third party for that user, usually fetched from a vault.
- Human approval. For risky actions, the user approves out of band (CIBA). In enterprises, the IdP can broker app-to-app access instead (Cross App Access).
sequenceDiagram
autonumber
participant U as User
participant C as MCP client (agent)
participant M as MCP server
participant AS as MCP authorization server
participant V as Token vault
participant API as Third-party API
C->>M: POST /mcp (no token)
M-->>C: 401 WWW-Authenticate resource_metadata=...
C->>M: GET /.well-known/oauth-protected-resource
C->>AS: Authorization request (client_id = CIMD URL, PKCE, resource=MCP URL)
AS->>C: Fetch client metadata document (if not cached)
U->>AS: Sign in and consent
AS-->>C: Code, then access token (aud = MCP server)
C->>M: POST /mcp, Authorization: Bearer (MCP token)
M->>M: Validate audience and scopes
M->>V: Get this user's third-party token
V-->>M: Access token (refreshed if needed)
M->>API: Call with the third-party token
opt High-risk action
M->>AS: CIBA backchannel request (binding_message)
AS->>U: Approve on phone or email
AS-->>M: Tokens after approval
end1. Register the client with CIMD
The MCP spec says authorization servers and clients SHOULD support OAuth Client ID Metadata Documents. Dynamic Client Registration is kept for backwards compatibility. The client-side priority order is: pre-registered credentials if you have them, then CIMD, then DCR as a fallback.
With CIMD, the client_id is an HTTPS URL that serves the client's metadata. This example is from the MCP client-registration page:
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example MCP Client",
"client_uri": "https://app.example.com",
"logo_uri": "https://app.example.com/logo.png",
"redirect_uris": [
"http://127.0.0.1:3000/callback",
"http://localhost:3000/callback"
],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
Rules from draft-ietf-oauth-client-id-metadata-document-02 and the MCP spec:
- The URL MUST use
https, contain a path, and have no fragment, userinfo or dot segments. The document'sclient_idMUST exactly match the URL (simple string comparison). - MCP requires at least
client_id,client_nameandredirect_uris. - No shared secrets.
client_secret,client_secret_basic/_post/_jwtMUST NOT be used. A client that can keep a private key becomes confidential by publishing only its public key. The draft's own example:
{
"token_endpoint_auth_method": "private_key_jwt",
"jwks_uri": "https://client.example.com/jwks.json"
}
- Authorization servers advertise support with
client_id_metadata_document_supportedin their RFC 8414 metadata. They MUST serve only 200 responses, MUST NOT follow redirects when fetching, MUST NOT cache errors, and SHOULD respect HTTP cache headers. - The draft warns about SSRF: an authorization server MUST NOT fetch metadata documents from internal addresses. Loopback is allowed only in local development.
2. Run the OAuth flow the MCP way
These steps follow the MCP 2026-07-28 authorization spec:
- Discover. An unauthenticated request gets
401withWWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="files:read". MCP servers MUST implement RFC 9728 Protected Resource Metadata, and clients MUST use it to find the authorization server. - Pick scopes. Use least privilege: request the scopes in the challenge, not everything in
scopes_supported. - Authorize with PKCE and a resource indicator. Clients MUST send the RFC 8707
resourceparameter in both the authorization and the token request, set to the MCP server's canonical URI, for example&resource=https%3A%2F%2Fmcp.example.com. They must send it even if the authorization server ignores it. Valid canonical forms includehttps://mcp.example.com/mcpandhttps://mcp.example.com. Avoid a trailing slash unless it matters. - Validate the response. Record the expected
issuerbefore redirecting, and validateissper RFC 9207 before redeeming the code. Includingissis SHOULD today and is expected to become MUST. - Use the token. Send it in the
Authorization: Bearerheader on every request, never in the query string. - Server side: check the audience. MCP servers MUST validate that tokens were issued specifically for them, and must answer invalid or expired tokens with
401. For missing scope, answer with aninsufficient_scopechallenge, and the client may step up.
Refresh tokens: clients MUST keep them confidential and MUST NOT assume they will get one. MCP servers SHOULD NOT add offline_access to the scopes they challenge for.
3. Never pass tokens through
From the spec: "MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server's authorization server" and "MCP servers MUST NOT accept or transit any other tokens." The security best-practices page calls token passthrough an anti-pattern. It lists four risks: it bypasses controls such as rate limits that depend on audience, it breaks the audit trail, it lets a stolen token use your server as an exfiltration proxy, and it breaks trust boundaries.
So the MCP token never goes to Google, and a Google token never comes in as the MCP bearer. If your MCP server proxies a third-party OAuth flow with a single static client ID, the same page requires per-client consent: a registry of approved client_ids per user, a consent page that names the client, its scopes and its redirect_uri, CSRF protection, frame-ancestors/X-Frame-Options: DENY, and __Host- cookies bound to the client. Without it, an existing consent cookie can be replayed (the confused deputy attack).
4. Keep third-party tokens in a vault
The MCP server needs the user's own token for each downstream API, stored securely and refreshed automatically. The products do different jobs here:
- Auth0 Token Vault is an OAuth token broker. When a user connects a provider (Google, Microsoft, Slack, GitHub, Box, custom OIDC and others) through Connected Accounts, Auth0 stores the access and refresh tokens. Your backend then trades the user's Auth0 token for the provider's token:
curl --request POST 'https://{yourDomain}/oauth/token' \
--header 'Content-Type: application/json' \
--data '{
"client_id": "<YOUR_CUSTOM_API_CLIENT_ID>",
"client_secret": "<YOUR_CUSTOM_API_CLIENT_SECRET>",
"subject_token": "<YOUR_AUTH0_ACCESS_TOKEN>",
"grant_type": "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token",
"subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
"requested_token_type": "http://auth0.com/oauth/token-type/federated-connection-access-token",
"connection": "google-oauth2"
}'
Refresh-token and privileged-worker (JWT bearer) exchanges are also documented.
- Descope Connections (also called Outbound Apps) is a vault for OAuth tokens and API keys, per user or per tenant, with Descope handling storage and refresh. Descope recommends putting an MCP server or gateway between the agent and the vault, so long-lived third-party secrets never reach the agent.
- WorkOS Vault is encrypted key-value storage (EKM): each object gets its own key, isolated by a context you supply, with BYOK support. It does not run the third-party OAuth dance or refresh tokens for you. You store and update what your own OAuth code obtains:
from workos import WorkOSClient
client = WorkOSClient(api_key="sk_example_123456789", client_id="client_123456789")
client.vault.create_kv(
key_context={"organization_id": "org_01K8ZYT4AWJ6XP0E0S8CTBHE3P"},
name="my-secret",
value="s3cr3t-v4lu3",
)
# later: client.vault.update_kv(id_=..., value=...) after you refresh
In short: Auth0 or Descope if you want the provider connection and refresh handled for you; WorkOS Vault if you run the OAuth clients yourself and need strong, tenant-scoped encryption. Arcade is a fourth model: a tool runtime that holds the user tokens and runs the tool call itself.
5. Ask the human out of band with CIBA
OpenID CIBA Core 1.0 lets a client start authentication without a browser redirect. The user approves on another device. This fits an agent that needs sign-off before a payment or deletion. The client posts to the OP's backchannel_authentication_endpoint, identifying the user with a hint, optionally with a binding_message shown on both devices:
POST /bc-authorize HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
scope=openid%20email%20example-scope&
client_notification_token=8d67dc78-7faa-4d41-aabd-67707b374255&
binding_message=W4SCT&
login_hint_token=eyJraWQiOiJsdGFjZXNidyIsImFsZyI6IkVTMjU2In0...&
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&
client_assertion=eyJraWQiOiJsdGFjZXNidyIsImFsZyI6IkVTMjU2In0...
The OP acknowledges with an id to track:
{
"auth_req_id": "1c266114-a1be-4252-8ad1-04986c5b9ac1",
"expires_in": 120,
"interval": 2
}
The client gets the result in one of three modes, fixed at registration. In poll mode it calls the token endpoint with grant_type=urn:openid:params:grant-type:ciba and the auth_req_id, waiting interval seconds between calls. In ping mode the OP notifies a callback and the client then fetches the tokens. In push mode the OP sends the tokens to the callback. Put the action and amount in binding_message (or in RAR details, where your provider supports them), so the user knows exactly what they are approving. Auth0 (with the AI Agents add-on) and Descope (confidential clients, off by default) implement CIBA; see the comparison.
6. Enterprise delegation with Cross App Access (ID-JAG)
In a company where every app uses SSO through one IdP, the Identity Assertion JWT Authorization Grant (draft-ietf-oauth-identity-assertion-authz-grant-04) defines Cross-App Access (XAA). The IdP, not the user clicking through a consent screen per app, brokers one app's access to another app's API. This lets admins set policy centrally.
Step one: the requesting app exchanges the user's ID token at the IdP for an ID-JAG (RFC 8693 token exchange):
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 result is a JWT with typ: oauth-id-jag+jwt, whose aud is the target app's authorization server, plus client_id, resource and scope. Step two: the app presents it to that authorization server with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer (RFC 7523) and gets a normal access token for the target API. Auth0 and Stytch both document Cross App Access support. Check that your IdP and the target app both implement the current draft.
Security checklist
- ☐ The MCP server publishes RFC 9728 metadata and returns
401withresource_metadataon missing tokens. - ☐ The authorization server supports CIMD (or pre-registration). DCR is enabled only if you need it for older clients.
- ☐ CIMD fetching blocks private and loopback addresses, follows no redirects, and caches only valid documents.
- ☐ No shared secrets in metadata documents. Confidential clients use
private_key_jwtwithjwks_uri. - ☐ PKCE on every authorization code flow.
issis validated per RFC 9207. - ☐ The
resourceparameter is in both the authorization and the token request. - ☐ The server validates
aud(and issuer, expiry, scopes) on every request, and rejects tokens not issued for it. - ☐ No token passthrough in either direction. Downstream calls use vaulted, per-user third-party tokens.
- ☐ Per-client consent if the server proxies a third-party OAuth flow with a static client ID.
- ☐ Least-privilege scopes, with step-up on
insufficient_scopeinstead of broad upfront grants. - ☐ Refresh tokens are kept confidential at rest and in transit.
offline_accessis not challenged by the MCP server. - ☐ Third-party tokens live in a vault, scoped per user or tenant, and never reach the model's context or logs.
- ☐ High-risk actions go through CIBA (or another out-of-band approval), with a specific
binding_message. - ☐ Every token use is logged with user, client and resource, so actions can be traced back.
Test the discovery and sign-in steps with MCP Inspector. For client configuration, see Connect an agent to a remote MCP server.
Directory entries in this guide
- MCP Authorization (OAuth 2.1 for MCP): The MCP specification's transport-level authorization flow for HTTP-based MCP servers, based on a subset of OAuth 2.1 and related RFCs.
- Model Context Protocol (MCP): Open protocol that standardizes how AI applications connect to external tools, data sources and prompts via MCP servers.
- OAuth Client ID Metadata Document (CIMD): IETF OAuth draft letting a client identify itself with a URL as its client_id, pointing to a metadata document, without prior dynamic registration.
- OpenID Connect CIBA (Client-Initiated Backchannel Authentication): OpenID flow where a relying party obtains tokens after the user authenticates and consents on a separate device, with no redirects through the user's browser.
- Cross App Access (ID-JAG): IETF OAuth draft (Identity Assertion JWT Authorization Grant) letting an app use an SSO identity assertion, via the trusted IdP, to get access tokens for a third-party API.
- WorkOS Vault: Developer-friendly enterprise key management (EKM) to encrypt and optionally store tokens, passwords, certificates, files and other customer data.
- Auth0 for AI Agents: Auth0's identity toolkit for AI agents: user authentication, delegated authorization, human-in-the-loop (CIBA) and fine-grained authorization for RAG.
- Descope Agentic Identity Hub: Manage authentication, authorization and external credentials for AI agents and MCP servers with Descope.
- WorkOS AuthKit for MCP: WorkOS AuthKit guide and support for securing MCP servers that require authentication.
- Arcade: Actions runtime for enterprise AI agents: agent authorization, agent-optimized tools (MCP servers) and governance, plus an open-source MCP framework.
- arcade-mcp: Arcade's open-source Python framework for building MCP servers and tools, with authorized tool calling (OAuth, token refresh) and tool-call evals.
- MCP Inspector: Official interactive developer tool for testing and debugging MCP servers in the browser, on the command line or in the terminal.
Related
- Add OAuth authorization to your own MCP server (Guide)
- Agent identity and MCP authorization platforms (Comparison)
- Connect an agent to a remote MCP server (Guide)
- Connect a remote MCP server (Skill)
- Pick an MCP server (Skill)
- Publish an MCP server so agents can find it (Guide)
- Database MCP servers compared (Comparison)
Sources
- MCP specification 2026-07-28, Authorization, accessed
- MCP specification 2026-07-28, Client registration, accessed
- MCP specification 2026-07-28, Security best practices, accessed
- OAuth Client ID Metadata Document (draft-ietf-oauth-client-id-metadata-document-02), accessed
- RFC 8707 Resource Indicators for OAuth 2.0, accessed
- RFC 9728 OAuth 2.0 Protected Resource Metadata, accessed
- OpenID Connect CIBA Core 1.0, accessed
- Identity Assertion JWT Authorization Grant (draft-ietf-oauth-identity-assertion-authz-grant-04), accessed
- WorkOS Vault, accessed
- WorkOS Vault quick start, accessed
- Auth0 Token Vault, accessed
- Auth0 access token exchange with Token Vault, accessed
- Descope Connections, accessed