{
  "type": "guide",
  "id": "agent-auth-and-delegated-access",
  "title": "Agent auth and delegated access, step by step",
  "summary": "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).",
  "author": "Agentica Author",
  "tags": [
    "oauth",
    "mcp",
    "auth",
    "identity",
    "delegation",
    "security"
  ],
  "published": "2026-10-15",
  "last_verified": "2026-10-15",
  "difficulty": "advanced",
  "time_estimate": "40 min",
  "entries": [
    "mcp-authorization",
    "model-context-protocol",
    "oauth-client-id-metadata-document",
    "openid-ciba",
    "cross-app-access",
    "workos-vault",
    "auth0-for-ai-agents",
    "descope-agentic-identity",
    "workos-authkit-mcp",
    "arcade",
    "arcade-mcp",
    "mcp-inspector"
  ],
  "links": {
    "html": "https://indexagentica.com/guides/agent-auth-and-delegated-access/",
    "markdown": "https://indexagentica.com/guides/agent-auth-and-delegated-access.md",
    "json": "https://indexagentica.com/api/longform/guides/agent-auth-and-delegated-access.json",
    "source": "https://github.com/Drudley/indexagentica/blob/main/content-long/guides/agent-auth-and-delegated-access.md"
  },
  "status": "published",
  "prerequisites": [
    "Basic OAuth 2.0 vocabulary (authorization code, access token, refresh token, scope)",
    "An MCP server you control, or a client you are building"
  ],
  "entries_detail": [
    {
      "id": "mcp-authorization",
      "name": "MCP Authorization (OAuth 2.1 for MCP)",
      "summary": "The MCP specification's transport-level authorization flow for HTTP-based MCP servers, based on a subset of OAuth 2.1 and related RFCs.",
      "url": "https://indexagentica.com/entries/mcp-authorization/",
      "json": "https://indexagentica.com/api/entries/mcp-authorization.json"
    },
    {
      "id": "model-context-protocol",
      "name": "Model Context Protocol (MCP)",
      "summary": "Open protocol that standardizes how AI applications connect to external tools, data sources and prompts via MCP servers.",
      "url": "https://indexagentica.com/entries/model-context-protocol/",
      "json": "https://indexagentica.com/api/entries/model-context-protocol.json"
    },
    {
      "id": "oauth-client-id-metadata-document",
      "name": "OAuth Client ID Metadata Document (CIMD)",
      "summary": "IETF OAuth draft letting a client identify itself with a URL as its client_id, pointing to a metadata document, without prior dynamic registration.",
      "url": "https://indexagentica.com/entries/oauth-client-id-metadata-document/",
      "json": "https://indexagentica.com/api/entries/oauth-client-id-metadata-document.json"
    },
    {
      "id": "openid-ciba",
      "name": "OpenID Connect CIBA (Client-Initiated Backchannel Authentication)",
      "summary": "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.",
      "url": "https://indexagentica.com/entries/openid-ciba/",
      "json": "https://indexagentica.com/api/entries/openid-ciba.json"
    },
    {
      "id": "cross-app-access",
      "name": "Cross App Access (ID-JAG)",
      "summary": "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.",
      "url": "https://indexagentica.com/entries/cross-app-access/",
      "json": "https://indexagentica.com/api/entries/cross-app-access.json"
    },
    {
      "id": "workos-vault",
      "name": "WorkOS Vault",
      "summary": "Developer-friendly enterprise key management (EKM) to encrypt and optionally store tokens, passwords, certificates, files and other customer data.",
      "url": "https://indexagentica.com/entries/workos-vault/",
      "json": "https://indexagentica.com/api/entries/workos-vault.json"
    },
    {
      "id": "auth0-for-ai-agents",
      "name": "Auth0 for AI Agents",
      "summary": "Auth0's identity toolkit for AI agents: user authentication, delegated authorization, human-in-the-loop (CIBA) and fine-grained authorization for RAG.",
      "url": "https://indexagentica.com/entries/auth0-for-ai-agents/",
      "json": "https://indexagentica.com/api/entries/auth0-for-ai-agents.json"
    },
    {
      "id": "descope-agentic-identity",
      "name": "Descope Agentic Identity Hub",
      "summary": "Manage authentication, authorization and external credentials for AI agents and MCP servers with Descope.",
      "url": "https://indexagentica.com/entries/descope-agentic-identity/",
      "json": "https://indexagentica.com/api/entries/descope-agentic-identity.json"
    },
    {
      "id": "workos-authkit-mcp",
      "name": "WorkOS AuthKit for MCP",
      "summary": "WorkOS AuthKit guide and support for securing MCP servers that require authentication.",
      "url": "https://indexagentica.com/entries/workos-authkit-mcp/",
      "json": "https://indexagentica.com/api/entries/workos-authkit-mcp.json"
    },
    {
      "id": "arcade",
      "name": "Arcade",
      "summary": "Actions runtime for enterprise AI agents: agent authorization, agent-optimized tools (MCP servers) and governance, plus an open-source MCP framework.",
      "url": "https://indexagentica.com/entries/arcade/",
      "json": "https://indexagentica.com/api/entries/arcade.json"
    },
    {
      "id": "arcade-mcp",
      "name": "arcade-mcp",
      "summary": "Arcade's open-source Python framework for building MCP servers and tools, with authorized tool calling (OAuth, token refresh) and tool-call evals.",
      "url": "https://indexagentica.com/entries/arcade-mcp/",
      "json": "https://indexagentica.com/api/entries/arcade-mcp.json"
    },
    {
      "id": "mcp-inspector",
      "name": "MCP Inspector",
      "summary": "Official interactive developer tool for testing and debugging MCP servers in the browser, on the command line or in the terminal.",
      "url": "https://indexagentica.com/entries/mcp-inspector/",
      "json": "https://indexagentica.com/api/entries/mcp-inspector.json"
    }
  ],
  "related": [
    {
      "type": "guide",
      "id": "add-oauth-to-your-mcp-server",
      "title": "Add OAuth authorization to your own MCP server",
      "url": "https://indexagentica.com/guides/add-oauth-to-your-mcp-server/",
      "json": "https://indexagentica.com/api/longform/guides/add-oauth-to-your-mcp-server.json"
    },
    {
      "type": "comparison",
      "id": "agent-identity-and-mcp-auth",
      "title": "Agent identity and MCP authorization platforms",
      "url": "https://indexagentica.com/compare/agent-identity-and-mcp-auth/",
      "json": "https://indexagentica.com/api/longform/compare/agent-identity-and-mcp-auth.json"
    },
    {
      "type": "guide",
      "id": "connect-an-agent-to-a-remote-mcp-server",
      "title": "Connect an agent to a remote MCP server",
      "url": "https://indexagentica.com/guides/connect-an-agent-to-a-remote-mcp-server/",
      "json": "https://indexagentica.com/api/longform/guides/connect-an-agent-to-a-remote-mcp-server.json"
    },
    {
      "type": "skill",
      "id": "connect-remote-mcp",
      "title": "Connect a remote MCP server",
      "url": "https://indexagentica.com/skills/connect-remote-mcp/",
      "json": "https://indexagentica.com/api/longform/skills/connect-remote-mcp.json"
    },
    {
      "type": "skill",
      "id": "pick-an-mcp-server",
      "title": "Pick an MCP server",
      "url": "https://indexagentica.com/skills/pick-an-mcp-server/",
      "json": "https://indexagentica.com/api/longform/skills/pick-an-mcp-server.json"
    },
    {
      "type": "guide",
      "id": "publish-an-mcp-server",
      "title": "Publish an MCP server so agents can find it",
      "url": "https://indexagentica.com/guides/publish-an-mcp-server/",
      "json": "https://indexagentica.com/api/longform/guides/publish-an-mcp-server.json"
    },
    {
      "type": "comparison",
      "id": "database-mcp-servers",
      "title": "Database MCP servers compared",
      "url": "https://indexagentica.com/compare/database-mcp-servers/",
      "json": "https://indexagentica.com/api/longform/compare/database-mcp-servers.json"
    }
  ],
  "sources": [
    {
      "title": "MCP specification 2026-07-28, Authorization",
      "url": "https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization",
      "accessed": "2026-10-15"
    },
    {
      "title": "MCP specification 2026-07-28, Client registration",
      "url": "https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration",
      "accessed": "2026-10-15"
    },
    {
      "title": "MCP specification 2026-07-28, Security best practices",
      "url": "https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices",
      "accessed": "2026-10-21"
    },
    {
      "title": "OAuth Client ID Metadata Document (draft-ietf-oauth-client-id-metadata-document-02)",
      "url": "https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/",
      "accessed": "2026-10-15"
    },
    {
      "title": "RFC 8707 Resource Indicators for OAuth 2.0",
      "url": "https://www.rfc-editor.org/rfc/rfc8707.html",
      "accessed": "2026-10-15"
    },
    {
      "title": "RFC 9728 OAuth 2.0 Protected Resource Metadata",
      "url": "https://datatracker.ietf.org/doc/html/rfc9728",
      "accessed": "2026-10-15"
    },
    {
      "title": "OpenID Connect CIBA Core 1.0",
      "url": "https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html",
      "accessed": "2026-10-15"
    },
    {
      "title": "Identity Assertion JWT Authorization Grant (draft-ietf-oauth-identity-assertion-authz-grant-04)",
      "url": "https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/",
      "accessed": "2026-10-15"
    },
    {
      "title": "WorkOS Vault",
      "url": "https://workos.com/docs/vault",
      "accessed": "2026-10-15"
    },
    {
      "title": "WorkOS Vault quick start",
      "url": "https://workos.com/docs/vault/quick-start",
      "accessed": "2026-10-15"
    },
    {
      "title": "Auth0 Token Vault",
      "url": "https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault",
      "accessed": "2026-10-21"
    },
    {
      "title": "Auth0 access token exchange with Token Vault",
      "url": "https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/access-token-exchange-with-token-vault",
      "accessed": "2026-10-21"
    },
    {
      "title": "Descope Connections",
      "url": "https://docs.descope.com/agentic-identity-hub/core-components/connections",
      "accessed": "2026-10-15"
    }
  ],
  "front_matter": {
    "id": "agent-auth-and-delegated-access",
    "type": "guide",
    "title": "Agent auth and delegated access, step by step",
    "summary": "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).",
    "description": "A how-to that follows one delegated request end to end, citing the specs directly: the IETF Client ID Metadata Document draft, the MCP 2026-07-28 authorization spec and its security best practices, RFC 8707 and RFC 9728, OpenID CIBA Core 1.0 and the IETF Identity Assertion JWT Authorization Grant draft. The vault section uses the WorkOS, Auth0 and Descope docs. Everything was checked on 2026-10-15. For a vendor-by-vendor comparison, see the agent identity comparison.",
    "author": "Agentica Author",
    "difficulty": "advanced",
    "time_estimate": "40 min",
    "prerequisites": [
      "Basic OAuth 2.0 vocabulary (authorization code, access token, refresh token, scope)",
      "An MCP server you control, or a client you are building"
    ],
    "tags": [
      "oauth",
      "mcp",
      "auth",
      "identity",
      "delegation",
      "security"
    ],
    "entries": [
      "mcp-authorization",
      "model-context-protocol",
      "oauth-client-id-metadata-document",
      "openid-ciba",
      "cross-app-access",
      "workos-vault",
      "auth0-for-ai-agents",
      "descope-agentic-identity",
      "workos-authkit-mcp",
      "arcade",
      "arcade-mcp",
      "mcp-inspector"
    ],
    "sources": [
      {
        "title": "MCP specification 2026-07-28, Authorization",
        "url": "https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization",
        "accessed": "2026-10-15"
      },
      {
        "title": "MCP specification 2026-07-28, Client registration",
        "url": "https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration",
        "accessed": "2026-10-15"
      },
      {
        "title": "MCP specification 2026-07-28, Security best practices",
        "url": "https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices",
        "accessed": "2026-10-21"
      },
      {
        "title": "OAuth Client ID Metadata Document (draft-ietf-oauth-client-id-metadata-document-02)",
        "url": "https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/",
        "accessed": "2026-10-15"
      },
      {
        "title": "RFC 8707 Resource Indicators for OAuth 2.0",
        "url": "https://www.rfc-editor.org/rfc/rfc8707.html",
        "accessed": "2026-10-15"
      },
      {
        "title": "RFC 9728 OAuth 2.0 Protected Resource Metadata",
        "url": "https://datatracker.ietf.org/doc/html/rfc9728",
        "accessed": "2026-10-15"
      },
      {
        "title": "OpenID Connect CIBA Core 1.0",
        "url": "https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html",
        "accessed": "2026-10-15"
      },
      {
        "title": "Identity Assertion JWT Authorization Grant (draft-ietf-oauth-identity-assertion-authz-grant-04)",
        "url": "https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/",
        "accessed": "2026-10-15"
      },
      {
        "title": "WorkOS Vault",
        "url": "https://workos.com/docs/vault",
        "accessed": "2026-10-15"
      },
      {
        "title": "WorkOS Vault quick start",
        "url": "https://workos.com/docs/vault/quick-start",
        "accessed": "2026-10-15"
      },
      {
        "title": "Auth0 Token Vault",
        "url": "https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault",
        "accessed": "2026-10-21"
      },
      {
        "title": "Auth0 access token exchange with Token Vault",
        "url": "https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/access-token-exchange-with-token-vault",
        "accessed": "2026-10-21"
      },
      {
        "title": "Descope Connections",
        "url": "https://docs.descope.com/agentic-identity-hub/core-components/connections",
        "accessed": "2026-10-15"
      }
    ],
    "related": [
      "add-oauth-to-your-mcp-server",
      "agent-identity-and-mcp-auth",
      "connect-an-agent-to-a-remote-mcp-server",
      "connect-remote-mcp",
      "pick-an-mcp-server",
      "publish-an-mcp-server",
      "database-mcp-servers"
    ],
    "last_verified": "2026-10-15",
    "published": "2026-10-15"
  },
  "markdown": "\n## The picture\n\nThere are three separate trust hops. Keep them separate, and most agent-auth mistakes disappear:\n\n1. **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](https://indexagentica.com/entries/mcp-authorization/)).\n2. **MCP server → third-party API.** The server uses a *different* token, issued by the third party for that user, usually fetched from a vault.\n3. **Human approval.** For risky actions, the user approves out of band ([CIBA](https://indexagentica.com/entries/openid-ciba/)). In enterprises, the IdP can broker app-to-app access instead ([Cross App Access](https://indexagentica.com/entries/cross-app-access/)).\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant U as User\n    participant C as MCP client (agent)\n    participant M as MCP server\n    participant AS as MCP authorization server\n    participant V as Token vault\n    participant API as Third-party API\n    C->>M: POST /mcp (no token)\n    M-->>C: 401 WWW-Authenticate resource_metadata=...\n    C->>M: GET /.well-known/oauth-protected-resource\n    C->>AS: Authorization request (client_id = CIMD URL, PKCE, resource=MCP URL)\n    AS->>C: Fetch client metadata document (if not cached)\n    U->>AS: Sign in and consent\n    AS-->>C: Code, then access token (aud = MCP server)\n    C->>M: POST /mcp, Authorization: Bearer (MCP token)\n    M->>M: Validate audience and scopes\n    M->>V: Get this user's third-party token\n    V-->>M: Access token (refreshed if needed)\n    M->>API: Call with the third-party token\n    opt High-risk action\n        M->>AS: CIBA backchannel request (binding_message)\n        AS->>U: Approve on phone or email\n        AS-->>M: Tokens after approval\n    end\n```\n\n## 1. Register the client with CIMD\n\nThe MCP spec says authorization servers and clients SHOULD support [OAuth Client ID Metadata Documents](https://indexagentica.com/entries/oauth-client-id-metadata-document/). 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.\n\nWith CIMD, the `client_id` *is* an HTTPS URL that serves the client's metadata. This example is from the MCP client-registration page:\n\n```json\n{\n  \"client_id\": \"https://app.example.com/oauth/client-metadata.json\",\n  \"client_name\": \"Example MCP Client\",\n  \"client_uri\": \"https://app.example.com\",\n  \"logo_uri\": \"https://app.example.com/logo.png\",\n  \"redirect_uris\": [\n    \"http://127.0.0.1:3000/callback\",\n    \"http://localhost:3000/callback\"\n  ],\n  \"grant_types\": [\"authorization_code\"],\n  \"response_types\": [\"code\"],\n  \"token_endpoint_auth_method\": \"none\"\n}\n```\n\nRules from draft-ietf-oauth-client-id-metadata-document-02 and the MCP spec:\n\n- The URL MUST use `https`, contain a path, and have no fragment, userinfo or dot segments. The document's `client_id` MUST exactly match the URL (simple string comparison).\n- MCP requires at least `client_id`, `client_name` and `redirect_uris`.\n- **No shared secrets.** `client_secret`, `client_secret_basic`/`_post`/`_jwt` MUST NOT be used. A client that can keep a private key becomes confidential by publishing only its public key. The draft's own example:\n\n```json\n{\n  \"token_endpoint_auth_method\": \"private_key_jwt\",\n  \"jwks_uri\": \"https://client.example.com/jwks.json\"\n}\n```\n\n- Authorization servers advertise support with `client_id_metadata_document_supported` in 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.\n- The draft warns about SSRF: an authorization server MUST NOT fetch metadata documents from internal addresses. Loopback is allowed only in local development.\n\n## 2. Run the OAuth flow the MCP way\n\nThese steps follow the MCP 2026-07-28 authorization spec:\n\n1. **Discover.** An unauthenticated request gets `401` with `WWW-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.\n2. **Pick scopes.** Use least privilege: request the scopes in the challenge, not everything in `scopes_supported`.\n3. **Authorize with PKCE and a resource indicator.** Clients MUST send the RFC 8707 `resource` parameter 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 include `https://mcp.example.com/mcp` and `https://mcp.example.com`. Avoid a trailing slash unless it matters.\n4. **Validate the response.** Record the expected `issuer` before redirecting, and validate `iss` per RFC 9207 before redeeming the code. Including `iss` is SHOULD today and is expected to become MUST.\n5. **Use the token.** Send it in the `Authorization: Bearer` header on every request, never in the query string.\n6. **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 an `insufficient_scope` challenge, and the client may step up.\n\nRefresh 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.\n\n## 3. Never pass tokens through\n\nFrom 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.\n\nSo 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_id`s 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).\n\n## 4. Keep third-party tokens in a vault\n\nThe MCP server needs the user's own token for each downstream API, stored securely and refreshed automatically. The products do different jobs here:\n\n- **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:\n\n```bash\ncurl --request POST 'https://{yourDomain}/oauth/token' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\n    \"client_id\": \"<YOUR_CUSTOM_API_CLIENT_ID>\",\n    \"client_secret\": \"<YOUR_CUSTOM_API_CLIENT_SECRET>\",\n    \"subject_token\": \"<YOUR_AUTH0_ACCESS_TOKEN>\",\n    \"grant_type\": \"urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token\",\n    \"subject_token_type\": \"urn:ietf:params:oauth:token-type:access_token\",\n    \"requested_token_type\": \"http://auth0.com/oauth/token-type/federated-connection-access-token\",\n    \"connection\": \"google-oauth2\"\n  }'\n```\n\n  Refresh-token and privileged-worker (JWT bearer) exchanges are also documented.\n- **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.\n- **[WorkOS Vault](https://indexagentica.com/entries/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:\n\n```python\nfrom workos import WorkOSClient\n\nclient = WorkOSClient(api_key=\"sk_example_123456789\", client_id=\"client_123456789\")\n\nclient.vault.create_kv(\n    key_context={\"organization_id\": \"org_01K8ZYT4AWJ6XP0E0S8CTBHE3P\"},\n    name=\"my-secret\",\n    value=\"s3cr3t-v4lu3\",\n)\n# later: client.vault.update_kv(id_=..., value=...) after you refresh\n```\n\nIn 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](https://indexagentica.com/entries/arcade/) is a fourth model: a tool runtime that holds the user tokens and runs the tool call itself.\n\n## 5. Ask the human out of band with CIBA\n\nOpenID 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:\n\n```http\nPOST /bc-authorize HTTP/1.1\nHost: server.example.com\nContent-Type: application/x-www-form-urlencoded\n\nscope=openid%20email%20example-scope&\nclient_notification_token=8d67dc78-7faa-4d41-aabd-67707b374255&\nbinding_message=W4SCT&\nlogin_hint_token=eyJraWQiOiJsdGFjZXNidyIsImFsZyI6IkVTMjU2In0...&\nclient_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&\nclient_assertion=eyJraWQiOiJsdGFjZXNidyIsImFsZyI6IkVTMjU2In0...\n```\n\nThe OP acknowledges with an id to track:\n\n```json\n{\n  \"auth_req_id\": \"1c266114-a1be-4252-8ad1-04986c5b9ac1\",\n  \"expires_in\": 120,\n  \"interval\": 2\n}\n```\n\nThe 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](https://indexagentica.com/compare/agent-identity-and-mcp-auth/).\n\n## 6. Enterprise delegation with Cross App Access (ID-JAG)\n\nIn 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.\n\nStep one: the requesting app exchanges the user's ID token at the IdP for an ID-JAG (RFC 8693 token exchange):\n\n```http\nPOST /oauth2/token HTTP/1.1\nHost: acme.idp.example\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange\n&requested_token_type=urn:ietf:params:oauth:token-type:id-jag\n&audience=https://acme.chat.example/\n&resource=https://api.chat.example/\n&scope=chat.read+chat.history\n&subject_token=eyJraWQiOiJzMTZ0cVNtODhwREo4VGZCXzdrSEtQ...\n&subject_token_type=urn:ietf:params:oauth:token-type:id_token\n&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer\n&client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0...\n```\n\nThe 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.\n\n## Security checklist\n\n- [ ] The MCP server publishes RFC 9728 metadata and returns `401` with `resource_metadata` on missing tokens.\n- [ ] The authorization server supports CIMD (or pre-registration). DCR is enabled only if you need it for older clients.\n- [ ] CIMD fetching blocks private and loopback addresses, follows no redirects, and caches only valid documents.\n- [ ] No shared secrets in metadata documents. Confidential clients use `private_key_jwt` with `jwks_uri`.\n- [ ] PKCE on every authorization code flow. `iss` is validated per RFC 9207.\n- [ ] The `resource` parameter is in both the authorization and the token request.\n- [ ] The server validates `aud` (and issuer, expiry, scopes) on every request, and rejects tokens not issued for it.\n- [ ] No token passthrough in either direction. Downstream calls use vaulted, per-user third-party tokens.\n- [ ] Per-client consent if the server proxies a third-party OAuth flow with a static client ID.\n- [ ] Least-privilege scopes, with step-up on `insufficient_scope` instead of broad upfront grants.\n- [ ] Refresh tokens are kept confidential at rest and in transit. `offline_access` is not challenged by the MCP server.\n- [ ] Third-party tokens live in a vault, scoped per user or tenant, and never reach the model's context or logs.\n- [ ] High-risk actions go through CIBA (or another out-of-band approval), with a specific `binding_message`.\n- [ ] Every token use is logged with user, client and resource, so actions can be traced back.\n\nTest the discovery and sign-in steps with [MCP Inspector](https://indexagentica.com/entries/mcp-inspector/). For client configuration, see [Connect an agent to a remote MCP server](https://indexagentica.com/guides/connect-an-agent-to-a-remote-mcp-server/).\n",
  "raw": "---\nid: agent-auth-and-delegated-access\ntype: guide\ntitle: Agent auth and delegated access, step by step\nsummary: \"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).\"\ndescription: \"A how-to that follows one delegated request end to end, citing the specs directly: the IETF Client ID Metadata Document draft, the MCP 2026-07-28 authorization spec and its security best practices, RFC 8707 and RFC 9728, OpenID CIBA Core 1.0 and the IETF Identity Assertion JWT Authorization Grant draft. The vault section uses the WorkOS, Auth0 and Descope docs. Everything was checked on 2026-10-15. For a vendor-by-vendor comparison, see the agent identity comparison.\"\nauthor: Agentica Author\ndifficulty: advanced\ntime_estimate: 40 min\nprerequisites:\n  - Basic OAuth 2.0 vocabulary (authorization code, access token, refresh token, scope)\n  - An MCP server you control, or a client you are building\ntags: [oauth, mcp, auth, identity, delegation, security]\nentries: [mcp-authorization, model-context-protocol, oauth-client-id-metadata-document, openid-ciba, cross-app-access, workos-vault, auth0-for-ai-agents, descope-agentic-identity, workos-authkit-mcp, arcade, arcade-mcp, mcp-inspector]\nsources:\n  - {title: \"MCP specification 2026-07-28, Authorization\", url: \"https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization\", accessed: 2026-10-15}\n  - {title: \"MCP specification 2026-07-28, Client registration\", url: \"https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization/client-registration\", accessed: 2026-10-15}\n  - {title: \"MCP specification 2026-07-28, Security best practices\", url: \"https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices\", accessed: 2026-10-21}\n  - {title: OAuth Client ID Metadata Document (draft-ietf-oauth-client-id-metadata-document-02), url: \"https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/\", accessed: 2026-10-15}\n  - {title: RFC 8707 Resource Indicators for OAuth 2.0, url: \"https://www.rfc-editor.org/rfc/rfc8707.html\", accessed: 2026-10-15}\n  - {title: RFC 9728 OAuth 2.0 Protected Resource Metadata, url: \"https://datatracker.ietf.org/doc/html/rfc9728\", accessed: 2026-10-15}\n  - {title: OpenID Connect CIBA Core 1.0, url: \"https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html\", accessed: 2026-10-15}\n  - {title: Identity Assertion JWT Authorization Grant (draft-ietf-oauth-identity-assertion-authz-grant-04), url: \"https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/\", accessed: 2026-10-15}\n  - {title: WorkOS Vault, url: \"https://workos.com/docs/vault\", accessed: 2026-10-15}\n  - {title: WorkOS Vault quick start, url: \"https://workos.com/docs/vault/quick-start\", accessed: 2026-10-15}\n  - {title: Auth0 Token Vault, url: \"https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault\", accessed: 2026-10-21}\n  - {title: Auth0 access token exchange with Token Vault, url: \"https://auth0.com/docs/secure/call-apis-on-users-behalf/token-vault/access-token-exchange-with-token-vault\", accessed: 2026-10-21}\n  - {title: Descope Connections, url: \"https://docs.descope.com/agentic-identity-hub/core-components/connections\", accessed: 2026-10-15}\nrelated: [add-oauth-to-your-mcp-server, agent-identity-and-mcp-auth, connect-an-agent-to-a-remote-mcp-server, connect-remote-mcp, pick-an-mcp-server, publish-an-mcp-server, database-mcp-servers]\nlast_verified: 2026-10-15\npublished: 2026-10-15\n---\n\n## The picture\n\nThere are three separate trust hops. Keep them separate, and most agent-auth mistakes disappear:\n\n1. **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](https://indexagentica.com/entries/mcp-authorization/)).\n2. **MCP server → third-party API.** The server uses a *different* token, issued by the third party for that user, usually fetched from a vault.\n3. **Human approval.** For risky actions, the user approves out of band ([CIBA](https://indexagentica.com/entries/openid-ciba/)). In enterprises, the IdP can broker app-to-app access instead ([Cross App Access](https://indexagentica.com/entries/cross-app-access/)).\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant U as User\n    participant C as MCP client (agent)\n    participant M as MCP server\n    participant AS as MCP authorization server\n    participant V as Token vault\n    participant API as Third-party API\n    C->>M: POST /mcp (no token)\n    M-->>C: 401 WWW-Authenticate resource_metadata=...\n    C->>M: GET /.well-known/oauth-protected-resource\n    C->>AS: Authorization request (client_id = CIMD URL, PKCE, resource=MCP URL)\n    AS->>C: Fetch client metadata document (if not cached)\n    U->>AS: Sign in and consent\n    AS-->>C: Code, then access token (aud = MCP server)\n    C->>M: POST /mcp, Authorization: Bearer (MCP token)\n    M->>M: Validate audience and scopes\n    M->>V: Get this user's third-party token\n    V-->>M: Access token (refreshed if needed)\n    M->>API: Call with the third-party token\n    opt High-risk action\n        M->>AS: CIBA backchannel request (binding_message)\n        AS->>U: Approve on phone or email\n        AS-->>M: Tokens after approval\n    end\n```\n\n## 1. Register the client with CIMD\n\nThe MCP spec says authorization servers and clients SHOULD support [OAuth Client ID Metadata Documents](https://indexagentica.com/entries/oauth-client-id-metadata-document/). 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.\n\nWith CIMD, the `client_id` *is* an HTTPS URL that serves the client's metadata. This example is from the MCP client-registration page:\n\n```json\n{\n  \"client_id\": \"https://app.example.com/oauth/client-metadata.json\",\n  \"client_name\": \"Example MCP Client\",\n  \"client_uri\": \"https://app.example.com\",\n  \"logo_uri\": \"https://app.example.com/logo.png\",\n  \"redirect_uris\": [\n    \"http://127.0.0.1:3000/callback\",\n    \"http://localhost:3000/callback\"\n  ],\n  \"grant_types\": [\"authorization_code\"],\n  \"response_types\": [\"code\"],\n  \"token_endpoint_auth_method\": \"none\"\n}\n```\n\nRules from draft-ietf-oauth-client-id-metadata-document-02 and the MCP spec:\n\n- The URL MUST use `https`, contain a path, and have no fragment, userinfo or dot segments. The document's `client_id` MUST exactly match the URL (simple string comparison).\n- MCP requires at least `client_id`, `client_name` and `redirect_uris`.\n- **No shared secrets.** `client_secret`, `client_secret_basic`/`_post`/`_jwt` MUST NOT be used. A client that can keep a private key becomes confidential by publishing only its public key. The draft's own example:\n\n```json\n{\n  \"token_endpoint_auth_method\": \"private_key_jwt\",\n  \"jwks_uri\": \"https://client.example.com/jwks.json\"\n}\n```\n\n- Authorization servers advertise support with `client_id_metadata_document_supported` in 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.\n- The draft warns about SSRF: an authorization server MUST NOT fetch metadata documents from internal addresses. Loopback is allowed only in local development.\n\n## 2. Run the OAuth flow the MCP way\n\nThese steps follow the MCP 2026-07-28 authorization spec:\n\n1. **Discover.** An unauthenticated request gets `401` with `WWW-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.\n2. **Pick scopes.** Use least privilege: request the scopes in the challenge, not everything in `scopes_supported`.\n3. **Authorize with PKCE and a resource indicator.** Clients MUST send the RFC 8707 `resource` parameter 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 include `https://mcp.example.com/mcp` and `https://mcp.example.com`. Avoid a trailing slash unless it matters.\n4. **Validate the response.** Record the expected `issuer` before redirecting, and validate `iss` per RFC 9207 before redeeming the code. Including `iss` is SHOULD today and is expected to become MUST.\n5. **Use the token.** Send it in the `Authorization: Bearer` header on every request, never in the query string.\n6. **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 an `insufficient_scope` challenge, and the client may step up.\n\nRefresh 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.\n\n## 3. Never pass tokens through\n\nFrom 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.\n\nSo 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_id`s 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).\n\n## 4. Keep third-party tokens in a vault\n\nThe MCP server needs the user's own token for each downstream API, stored securely and refreshed automatically. The products do different jobs here:\n\n- **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:\n\n```bash\ncurl --request POST 'https://{yourDomain}/oauth/token' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\n    \"client_id\": \"<YOUR_CUSTOM_API_CLIENT_ID>\",\n    \"client_secret\": \"<YOUR_CUSTOM_API_CLIENT_SECRET>\",\n    \"subject_token\": \"<YOUR_AUTH0_ACCESS_TOKEN>\",\n    \"grant_type\": \"urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token\",\n    \"subject_token_type\": \"urn:ietf:params:oauth:token-type:access_token\",\n    \"requested_token_type\": \"http://auth0.com/oauth/token-type/federated-connection-access-token\",\n    \"connection\": \"google-oauth2\"\n  }'\n```\n\n  Refresh-token and privileged-worker (JWT bearer) exchanges are also documented.\n- **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.\n- **[WorkOS Vault](https://indexagentica.com/entries/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:\n\n```python\nfrom workos import WorkOSClient\n\nclient = WorkOSClient(api_key=\"sk_example_123456789\", client_id=\"client_123456789\")\n\nclient.vault.create_kv(\n    key_context={\"organization_id\": \"org_01K8ZYT4AWJ6XP0E0S8CTBHE3P\"},\n    name=\"my-secret\",\n    value=\"s3cr3t-v4lu3\",\n)\n# later: client.vault.update_kv(id_=..., value=...) after you refresh\n```\n\nIn 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](https://indexagentica.com/entries/arcade/) is a fourth model: a tool runtime that holds the user tokens and runs the tool call itself.\n\n## 5. Ask the human out of band with CIBA\n\nOpenID 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:\n\n```http\nPOST /bc-authorize HTTP/1.1\nHost: server.example.com\nContent-Type: application/x-www-form-urlencoded\n\nscope=openid%20email%20example-scope&\nclient_notification_token=8d67dc78-7faa-4d41-aabd-67707b374255&\nbinding_message=W4SCT&\nlogin_hint_token=eyJraWQiOiJsdGFjZXNidyIsImFsZyI6IkVTMjU2In0...&\nclient_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&\nclient_assertion=eyJraWQiOiJsdGFjZXNidyIsImFsZyI6IkVTMjU2In0...\n```\n\nThe OP acknowledges with an id to track:\n\n```json\n{\n  \"auth_req_id\": \"1c266114-a1be-4252-8ad1-04986c5b9ac1\",\n  \"expires_in\": 120,\n  \"interval\": 2\n}\n```\n\nThe 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](https://indexagentica.com/compare/agent-identity-and-mcp-auth/).\n\n## 6. Enterprise delegation with Cross App Access (ID-JAG)\n\nIn 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.\n\nStep one: the requesting app exchanges the user's ID token at the IdP for an ID-JAG (RFC 8693 token exchange):\n\n```http\nPOST /oauth2/token HTTP/1.1\nHost: acme.idp.example\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange\n&requested_token_type=urn:ietf:params:oauth:token-type:id-jag\n&audience=https://acme.chat.example/\n&resource=https://api.chat.example/\n&scope=chat.read+chat.history\n&subject_token=eyJraWQiOiJzMTZ0cVNtODhwREo4VGZCXzdrSEtQ...\n&subject_token_type=urn:ietf:params:oauth:token-type:id_token\n&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer\n&client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0...\n```\n\nThe 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.\n\n## Security checklist\n\n- [ ] The MCP server publishes RFC 9728 metadata and returns `401` with `resource_metadata` on missing tokens.\n- [ ] The authorization server supports CIMD (or pre-registration). DCR is enabled only if you need it for older clients.\n- [ ] CIMD fetching blocks private and loopback addresses, follows no redirects, and caches only valid documents.\n- [ ] No shared secrets in metadata documents. Confidential clients use `private_key_jwt` with `jwks_uri`.\n- [ ] PKCE on every authorization code flow. `iss` is validated per RFC 9207.\n- [ ] The `resource` parameter is in both the authorization and the token request.\n- [ ] The server validates `aud` (and issuer, expiry, scopes) on every request, and rejects tokens not issued for it.\n- [ ] No token passthrough in either direction. Downstream calls use vaulted, per-user third-party tokens.\n- [ ] Per-client consent if the server proxies a third-party OAuth flow with a static client ID.\n- [ ] Least-privilege scopes, with step-up on `insufficient_scope` instead of broad upfront grants.\n- [ ] Refresh tokens are kept confidential at rest and in transit. `offline_access` is not challenged by the MCP server.\n- [ ] Third-party tokens live in a vault, scoped per user or tenant, and never reach the model's context or logs.\n- [ ] High-risk actions go through CIBA (or another out-of-band approval), with a specific `binding_message`.\n- [ ] Every token use is logged with user, client and resource, so actions can be traced back.\n\nTest the discovery and sign-in steps with [MCP Inspector](https://indexagentica.com/entries/mcp-inspector/). For client configuration, see [Connect an agent to a remote MCP server](https://indexagentica.com/guides/connect-an-agent-to-a-remote-mcp-server/).\n",
  "html": "<h2>The picture</h2>\n<p>There are three separate trust hops. Keep them separate, and most agent-auth mistakes disappear:</p>\n<ol><li><strong>Client → MCP server.</strong> The agent&#39;s MCP client gets a token from the <em>MCP server&#39;s</em> authorization server, for that MCP server only (<a href=\"/entries/mcp-authorization/\">MCP authorization</a>).</li><li><strong>MCP server → third-party API.</strong> The server uses a <em>different</em> token, issued by the third party for that user, usually fetched from a vault.</li><li><strong>Human approval.</strong> For risky actions, the user approves out of band (<a href=\"/entries/openid-ciba/\">CIBA</a>). In enterprises, the IdP can broker app-to-app access instead (<a href=\"/entries/cross-app-access/\">Cross App Access</a>).</li></ol>\n<figure class=\"diagram\"><pre class=\"mermaid\">sequenceDiagram\n    autonumber\n    participant U as User\n    participant C as MCP client (agent)\n    participant M as MCP server\n    participant AS as MCP authorization server\n    participant V as Token vault\n    participant API as Third-party API\n    C-&gt;&gt;M: POST /mcp (no token)\n    M--&gt;&gt;C: 401 WWW-Authenticate resource_metadata=...\n    C-&gt;&gt;M: GET /.well-known/oauth-protected-resource\n    C-&gt;&gt;AS: Authorization request (client_id = CIMD URL, PKCE, resource=MCP URL)\n    AS-&gt;&gt;C: Fetch client metadata document (if not cached)\n    U-&gt;&gt;AS: Sign in and consent\n    AS--&gt;&gt;C: Code, then access token (aud = MCP server)\n    C-&gt;&gt;M: POST /mcp, Authorization: Bearer (MCP token)\n    M-&gt;&gt;M: Validate audience and scopes\n    M-&gt;&gt;V: Get this user&#39;s third-party token\n    V--&gt;&gt;M: Access token (refreshed if needed)\n    M-&gt;&gt;API: Call with the third-party token\n    opt High-risk action\n        M-&gt;&gt;AS: CIBA backchannel request (binding_message)\n        AS-&gt;&gt;U: Approve on phone or email\n        AS--&gt;&gt;M: Tokens after approval\n    end</pre><figcaption>Mermaid diagram (source shown; not rendered client-side)</figcaption></figure>\n<h2>1. Register the client with CIMD</h2>\n<p>The MCP spec says authorization servers and clients SHOULD support <a href=\"/entries/oauth-client-id-metadata-document/\">OAuth Client ID Metadata Documents</a>. 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.</p>\n<p>With CIMD, the <code>client_id</code> <em>is</em> an HTTPS URL that serves the client&#39;s metadata. This example is from the MCP client-registration page:</p>\n<pre><code class=\"language-json\">{\n  &quot;client_id&quot;: &quot;https://app.example.com/oauth/client-metadata.json&quot;,\n  &quot;client_name&quot;: &quot;Example MCP Client&quot;,\n  &quot;client_uri&quot;: &quot;https://app.example.com&quot;,\n  &quot;logo_uri&quot;: &quot;https://app.example.com/logo.png&quot;,\n  &quot;redirect_uris&quot;: [\n    &quot;http://127.0.0.1:3000/callback&quot;,\n    &quot;http://localhost:3000/callback&quot;\n  ],\n  &quot;grant_types&quot;: [&quot;authorization_code&quot;],\n  &quot;response_types&quot;: [&quot;code&quot;],\n  &quot;token_endpoint_auth_method&quot;: &quot;none&quot;\n}</code></pre>\n<p>Rules from draft-ietf-oauth-client-id-metadata-document-02 and the MCP spec:</p>\n<ul><li>The URL MUST use <code>https</code>, contain a path, and have no fragment, userinfo or dot segments. The document&#39;s <code>client_id</code> MUST exactly match the URL (simple string comparison).</li><li>MCP requires at least <code>client_id</code>, <code>client_name</code> and <code>redirect_uris</code>.</li><li><strong>No shared secrets.</strong> <code>client_secret</code>, <code>client_secret_basic</code>/<code>_post</code>/<code>_jwt</code> MUST NOT be used. A client that can keep a private key becomes confidential by publishing only its public key. The draft&#39;s own example:</li></ul>\n<pre><code class=\"language-json\">{\n  &quot;token_endpoint_auth_method&quot;: &quot;private_key_jwt&quot;,\n  &quot;jwks_uri&quot;: &quot;https://client.example.com/jwks.json&quot;\n}</code></pre>\n<ul><li>Authorization servers advertise support with <code>client_id_metadata_document_supported</code> in 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.</li><li>The draft warns about SSRF: an authorization server MUST NOT fetch metadata documents from internal addresses. Loopback is allowed only in local development.</li></ul>\n<h2>2. Run the OAuth flow the MCP way</h2>\n<p>These steps follow the MCP 2026-07-28 authorization spec:</p>\n<ol><li><strong>Discover.</strong> An unauthenticated request gets <code>401</code> with <code>WWW-Authenticate: Bearer resource_metadata=&quot;https://mcp.example.com/.well-known/oauth-protected-resource&quot;, scope=&quot;files:read&quot;</code>. MCP servers MUST implement RFC 9728 Protected Resource Metadata, and clients MUST use it to find the authorization server.</li><li><strong>Pick scopes.</strong> Use least privilege: request the scopes in the challenge, not everything in <code>scopes_supported</code>.</li><li><strong>Authorize with PKCE and a resource indicator.</strong> Clients MUST send the RFC 8707 <code>resource</code> parameter in <strong>both</strong> the authorization and the token request, set to the MCP server&#39;s canonical URI, for example <code>&amp;resource=https%3A%2F%2Fmcp.example.com</code>. They must send it even if the authorization server ignores it. Valid canonical forms include <code>https://mcp.example.com/mcp</code> and <code>https://mcp.example.com</code>. Avoid a trailing slash unless it matters.</li><li><strong>Validate the response.</strong> Record the expected <code>issuer</code> before redirecting, and validate <code>iss</code> per RFC 9207 before redeeming the code. Including <code>iss</code> is SHOULD today and is expected to become MUST.</li><li><strong>Use the token.</strong> Send it in the <code>Authorization: Bearer</code> header on every request, never in the query string.</li><li><strong>Server side: check the audience.</strong> MCP servers MUST validate that tokens were issued specifically for them, and must answer invalid or expired tokens with <code>401</code>. For missing scope, answer with an <code>insufficient_scope</code> challenge, and the client may step up.</li></ol>\n<p>Refresh tokens: clients MUST keep them confidential and MUST NOT assume they will get one. MCP servers SHOULD NOT add <code>offline_access</code> to the scopes they challenge for.</p>\n<h2>3. Never pass tokens through</h2>\n<p>From the spec: <em>&quot;MCP clients MUST NOT send tokens to the MCP server other than ones issued by the MCP server&#39;s authorization server&quot;</em> and <em>&quot;MCP servers MUST NOT accept or transit any other tokens.&quot;</em> 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.</p>\n<p>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 <strong>per-client consent</strong>: a registry of approved <code>client_id</code>s per user, a consent page that names the client, its scopes and its <code>redirect_uri</code>, CSRF protection, <code>frame-ancestors</code>/<code>X-Frame-Options: DENY</code>, and <code>__Host-</code> cookies bound to the client. Without it, an existing consent cookie can be replayed (the confused deputy attack).</p>\n<h2>4. Keep third-party tokens in a vault</h2>\n<p>The MCP server needs the user&#39;s own token for each downstream API, stored securely and refreshed automatically. The products do different jobs here:</p>\n<ul><li><strong>Auth0 Token Vault</strong> 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&#39;s Auth0 token for the provider&#39;s token:</li></ul>\n<pre><code class=\"language-bash\">curl --request POST &#39;https://{yourDomain}/oauth/token&#39; \\\n  --header &#39;Content-Type: application/json&#39; \\\n  --data &#39;{\n    &quot;client_id&quot;: &quot;&lt;YOUR_CUSTOM_API_CLIENT_ID&gt;&quot;,\n    &quot;client_secret&quot;: &quot;&lt;YOUR_CUSTOM_API_CLIENT_SECRET&gt;&quot;,\n    &quot;subject_token&quot;: &quot;&lt;YOUR_AUTH0_ACCESS_TOKEN&gt;&quot;,\n    &quot;grant_type&quot;: &quot;urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token&quot;,\n    &quot;subject_token_type&quot;: &quot;urn:ietf:params:oauth:token-type:access_token&quot;,\n    &quot;requested_token_type&quot;: &quot;http://auth0.com/oauth/token-type/federated-connection-access-token&quot;,\n    &quot;connection&quot;: &quot;google-oauth2&quot;\n  }&#39;</code></pre>\n<p>  Refresh-token and privileged-worker (JWT bearer) exchanges are also documented.</p>\n<ul><li><strong>Descope Connections</strong> (also called Outbound Apps) is a vault for OAuth tokens <em>and</em> 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.</li><li><strong><a href=\"/entries/workos-vault/\">WorkOS Vault</a></strong> is encrypted key-value storage (EKM): each object gets its own key, isolated by a context you supply, with BYOK support. It does <strong>not</strong> run the third-party OAuth dance or refresh tokens for you. You store and update what your own OAuth code obtains:</li></ul>\n<pre><code class=\"language-python\">from workos import WorkOSClient\n\nclient = WorkOSClient(api_key=&quot;sk_example_123456789&quot;, client_id=&quot;client_123456789&quot;)\n\nclient.vault.create_kv(\n    key_context={&quot;organization_id&quot;: &quot;org_01K8ZYT4AWJ6XP0E0S8CTBHE3P&quot;},\n    name=&quot;my-secret&quot;,\n    value=&quot;s3cr3t-v4lu3&quot;,\n)\n# later: client.vault.update_kv(id_=..., value=...) after you refresh</code></pre>\n<p>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. <a href=\"/entries/arcade/\">Arcade</a> is a fourth model: a tool runtime that holds the user tokens and runs the tool call itself.</p>\n<h2>5. Ask the human out of band with CIBA</h2>\n<p>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&#39;s <code>backchannel_authentication_endpoint</code>, identifying the user with a hint, optionally with a <code>binding_message</code> shown on both devices:</p>\n<pre><code class=\"language-http\">POST /bc-authorize HTTP/1.1\nHost: server.example.com\nContent-Type: application/x-www-form-urlencoded\n\nscope=openid%20email%20example-scope&amp;\nclient_notification_token=8d67dc78-7faa-4d41-aabd-67707b374255&amp;\nbinding_message=W4SCT&amp;\nlogin_hint_token=eyJraWQiOiJsdGFjZXNidyIsImFsZyI6IkVTMjU2In0...&amp;\nclient_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&amp;\nclient_assertion=eyJraWQiOiJsdGFjZXNidyIsImFsZyI6IkVTMjU2In0...</code></pre>\n<p>The OP acknowledges with an id to track:</p>\n<pre><code class=\"language-json\">{\n  &quot;auth_req_id&quot;: &quot;1c266114-a1be-4252-8ad1-04986c5b9ac1&quot;,\n  &quot;expires_in&quot;: 120,\n  &quot;interval&quot;: 2\n}</code></pre>\n<p>The client gets the result in one of three modes, fixed at registration. In <strong>poll</strong> mode it calls the token endpoint with <code>grant_type=urn:openid:params:grant-type:ciba</code> and the <code>auth_req_id</code>, waiting <code>interval</code> seconds between calls. In <strong>ping</strong> mode the OP notifies a callback and the client then fetches the tokens. In <strong>push</strong> mode the OP sends the tokens to the callback. Put the action and amount in <code>binding_message</code> (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 <a href=\"/compare/agent-identity-and-mcp-auth/\">comparison</a>.</p>\n<h2>6. Enterprise delegation with Cross App Access (ID-JAG)</h2>\n<p>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 <strong>Cross-App Access (XAA)</strong>. The IdP, not the user clicking through a consent screen per app, brokers one app&#39;s access to another app&#39;s API. This lets admins set policy centrally.</p>\n<p>Step one: the requesting app exchanges the user&#39;s ID token at the IdP for an ID-JAG (RFC 8693 token exchange):</p>\n<pre><code class=\"language-http\">POST /oauth2/token HTTP/1.1\nHost: acme.idp.example\nContent-Type: application/x-www-form-urlencoded\n\ngrant_type=urn:ietf:params:oauth:grant-type:token-exchange\n&amp;requested_token_type=urn:ietf:params:oauth:token-type:id-jag\n&amp;audience=https://acme.chat.example/\n&amp;resource=https://api.chat.example/\n&amp;scope=chat.read+chat.history\n&amp;subject_token=eyJraWQiOiJzMTZ0cVNtODhwREo4VGZCXzdrSEtQ...\n&amp;subject_token_type=urn:ietf:params:oauth:token-type:id_token\n&amp;client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer\n&amp;client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0...</code></pre>\n<p>The result is a JWT with <code>typ: oauth-id-jag+jwt</code>, whose <code>aud</code> is the target app&#39;s authorization server, plus <code>client_id</code>, <code>resource</code> and <code>scope</code>. Step two: the app presents it to that authorization server with <code>grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer</code> (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.</p>\n<h2>Security checklist</h2>\n<ul><li>☐ The MCP server publishes RFC 9728 metadata and returns <code>401</code> with <code>resource_metadata</code> on missing tokens.</li><li>☐ The authorization server supports CIMD (or pre-registration). DCR is enabled only if you need it for older clients.</li><li>☐ CIMD fetching blocks private and loopback addresses, follows no redirects, and caches only valid documents.</li><li>☐ No shared secrets in metadata documents. Confidential clients use <code>private_key_jwt</code> with <code>jwks_uri</code>.</li><li>☐ PKCE on every authorization code flow. <code>iss</code> is validated per RFC 9207.</li><li>☐ The <code>resource</code> parameter is in both the authorization and the token request.</li><li>☐ The server validates <code>aud</code> (and issuer, expiry, scopes) on every request, and rejects tokens not issued for it.</li><li>☐ No token passthrough in either direction. Downstream calls use vaulted, per-user third-party tokens.</li><li>☐ Per-client consent if the server proxies a third-party OAuth flow with a static client ID.</li><li>☐ Least-privilege scopes, with step-up on <code>insufficient_scope</code> instead of broad upfront grants.</li><li>☐ Refresh tokens are kept confidential at rest and in transit. <code>offline_access</code> is not challenged by the MCP server.</li><li>☐ Third-party tokens live in a vault, scoped per user or tenant, and never reach the model&#39;s context or logs.</li><li>☐ High-risk actions go through CIBA (or another out-of-band approval), with a specific <code>binding_message</code>.</li><li>☐ Every token use is logged with user, client and resource, so actions can be traced back.</li></ul>\n<p>Test the discovery and sign-in steps with <a href=\"/entries/mcp-inspector/\">MCP Inspector</a>. For client configuration, see <a href=\"/guides/connect-an-agent-to-a-remote-mcp-server/\">Connect an agent to a remote MCP server</a>.</p>"
}
