{
  "question": "How do I stop an MCP proxy server from acting as a confused deputy, using per-client consent and per-user, per-tool scoped tokens?",
  "slug": "how-to-stop-an-mcp-proxy-server-acting-as-a-confused-deputy-with-per-client-consent-and-per-tool-tokens",
  "url": "https://www.rightshold.com/answers/how-to-stop-an-mcp-proxy-server-acting-as-a-confused-deputy-with-per-client-consent-and-per-tool-tokens",
  "short_answer": "An MCP proxy server sits between many MCP clients and one third-party API, usually with a single static OAuth client ID. That makes it a deputy: if it skips its own consent step, an attacker can dynamically register a client with their own redirect URI, ride the user's existing third-party consent cookie, and receive an authorization code for the user's account. The MCP security best practices make per-client consent a MUST for proxy servers: keep a per-user registry of approved client_ids, show a server-owned consent page naming the client, scopes and redirect URI, match redirect URIs exactly, and set single-use state only after consent. Then close the second half of the problem — token passthrough — by never forwarding the client's token: accept only tokens whose audience is your server, and for each tool call mint or select a downstream token bound to that user, that tool's minimum scope and that one API. One user's token never serves another user, and a token minted for read_issues cannot be used by delete_repo.",
  "checklist": [
    "Find out whether you are a proxy server in the spec's sense: one static client_id at a third-party authorization server, shared by every MCP client that connects to you. If yes, the confused-deputy mitigations below are mandatory, not optional.",
    "Keep a per-user registry of approved MCP client_ids. Before redirecting to the third-party authorization server, check it; if this client has not been approved by this user, show your own consent page first.",
    "Make the consent page name the requesting client, list the third-party scopes, show the exact registered redirect_uri, carry CSRF protection, and refuse framing (frame-ancestors or X-Frame-Options: DENY).",
    "Validate redirect_uri by exact string match against the registered value; reject any change without re-registration. No wildcards or prefix matches.",
    "Generate a random, single-use, short-lived state value and store it only after the user approves consent; reject callbacks with a missing or mismatched state.",
    "Bind consent cookies (if you use them) to the specific client_id with the __Host- prefix, Secure, HttpOnly and SameSite=Lax — not a generic \"user has consented\" flag.",
    "On every tools/call, reject tokens not issued for your server (check aud / resource), then use a downstream token keyed by (user, tool, API) with only that tool's scopes. Never hand the inbound token to the downstream API.",
    "Log consent grants, token mints and tool calls with the MCP client_id and user id so a stolen code or token can be traced and revoked per client, not by rotating everything."
  ],
  "sections": [
    {
      "heading": "How a static client ID turns your proxy into a confused deputy",
      "points": [
        "The MCP security best practices describe the attack: the user once approves your proxy at the third-party authorization server, which sets a consent cookie for your static client ID. An attacker dynamically registers a new MCP client with redirect_uri pointing at their own site and sends the user a crafted link.",
        "Because the cookie is for your static client ID, the third-party server skips its consent screen. Your proxy exchanges the third-party code, mints an MCP authorization code and redirects it to the attacker's registered URI. The attacker trades it for an MCP token and acts as the user.",
        "Nothing was \"hacked\" at the third-party API; your proxy used its own authority on someone else's behalf. That is the confused deputy, and it is why the spec puts the fix — per-client consent — on the proxy."
      ]
    },
    {
      "heading": "Per-client consent, exact redirect URIs and state after consent",
      "points": [
        "Per-client consent: approval is stored per user and per MCP client_id, checked before any redirect to the third party. A new client — even with the same user and the same third-party cookie — gets your consent page first.",
        "Exact redirect URI matching stops the code from going anywhere but the URI the client registered. State values that exist only after the user clicks approve stop an attacker from pre-building a callback that bypasses your consent page.",
        "Clickjacking defenses matter because the consent page is the control: if it can be framed, a user can be tricked into approving a client they never saw."
      ]
    },
    {
      "heading": "Per-user, per-tool downstream tokens with audience checks",
      "points": [
        "Token passthrough is forbidden by the MCP authorization specification: a server must not accept tokens that were not issued for it, and must not forward an inbound token to an upstream API. Validate the inbound token's audience (RFC 8707 resource / RFC 9068 aud) on every request.",
        "For the downstream call, keep tokens keyed by user and tool: read_issues gets a token with issues:read for that user's grant; create_pr gets a separate token with pull-request write scope, minted only when that tool is first used (the spec's progressive, least-privilege scope model). A token never crosses users, and a read tool's token cannot perform a write.",
        "Where the upstream supports it, prefer OAuth 2.0 Token Exchange (RFC 8693) with a resource parameter so each downstream token names one API and expires in minutes; otherwise keep per-user refresh tokens server-side and mint narrow access tokens per tool from them."
      ]
    }
  ],
  "checks": [],
  "ask_rightshold": {
    "start": "https://www.rightshold.com/start?ref=seo",
    "mcp_endpoint": "https://www.rightshold.com/mcp",
    "ask": "Issue a short-lived credential for my MCP proxy's GitHub read tool, scoped to one user and one tool, so the downstream gateway can verify which user and tool a call belongs to before using that user's token.",
    "mcp_call": {
      "tool": "issue_agent_credential",
      "arguments": {
        "agent": "mcp-proxy:github:read_issues:user-123",
        "public_key_pem": "<Ed25519 PEM>",
        "config": {
          "model_id": "provider/model@version",
          "system_prompt_sha256": "<hex>",
          "tools": [
            "read_issues"
          ],
          "code_version": "v1.0.0"
        },
        "scope": [
          "github:issues:read",
          "user:user-123"
        ],
        "ttl_hours": 1,
        "signature": "<proxy key signs the CredentialRequest>"
      }
    },
    "also": [
      "Needs your API key. verify_agent_credential is free and keyless: the gateway in front of the downstream API checks the credential's scope covers this tool and user before releasing the per-user token.",
      "revoke_agent_credential ends one user-and-tool credential without touching others, which keeps a leaked credential's blast radius to a single tool.",
      "precall_check is a free read on an MCP server's registry status, live tool-definition integrity and name collisions before you route a client to it.",
      "Rightshold does not run your OAuth flows, consent page or token exchange; per-client consent and audience checks are implemented in your proxy and authorization server."
    ],
    "scope": "Rightshold issues and verifies agent credentials you request and records decisions you send it. It is not an OAuth authorization server and does not hold your users' third-party tokens. Not legal advice."
  },
  "sources": [
    {
      "title": "MCP specification: Security Best Practices (Confused Deputy Problem, Token Passthrough, Scope Minimization)",
      "url": "https://modelcontextprotocol.io/specification/draft/basic/security_best_practices"
    },
    {
      "title": "MCP specification: Authorization",
      "url": "https://modelcontextprotocol.io/specification/draft/basic/authorization"
    },
    {
      "title": "RFC 8707: Resource Indicators for OAuth 2.0",
      "url": "https://www.rfc-editor.org/rfc/rfc8707"
    },
    {
      "title": "RFC 9068: JWT Profile for OAuth 2.0 Access Tokens",
      "url": "https://www.rfc-editor.org/rfc/rfc9068"
    },
    {
      "title": "RFC 8693: OAuth 2.0 Token Exchange",
      "url": "https://www.rfc-editor.org/rfc/rfc8693"
    },
    {
      "title": "RFC 9700: Best Current Practice for OAuth 2.0 Security",
      "url": "https://www.rfc-editor.org/rfc/rfc9700"
    },
    {
      "title": "Agent credentials on Rightshold",
      "url": "https://www.rightshold.com/agent-credentials"
    }
  ],
  "related": [
    {
      "title": "What is a wrong-audience OAuth token in MCP, and how do I defend against it?",
      "url": "https://www.rightshold.com/answers/mcp-oauth-wrong-audience-tokens"
    },
    {
      "title": "How to do OAuth token exchange so an MCP server never forwards user tokens",
      "url": "https://www.rightshold.com/answers/how-to-do-oauth-token-exchange-so-an-mcp-server-never-forwards-user-tokens"
    },
    {
      "title": "How to keep MCP as a thin adapter and recheck authorization on every tool call",
      "url": "https://www.rightshold.com/answers/how-to-keep-mcp-as-a-thin-adapter-and-recheck-authorization-on-every-tool-call"
    },
    {
      "title": "How to share one MCP server across a company with per-user auth",
      "url": "https://www.rightshold.com/answers/how-to-share-one-mcp-server-across-a-company-with-per-user-auth"
    }
  ],
  "computed_from": "Curated guidance; license verdicts computed from Rightshold's license profiles at render time. Not legal advice.",
  "updated": "2026-11-07"
}