Rightshold™ Sign up

Answers · MCP security

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?

Last updated

TL;DR: 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.

What should I check, step by step?

1. 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.

  1. 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.
  2. 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).
  3. Validate redirect_uri by exact string match against the registered value; reject any change without re-registration. No wildcards or prefix matches.
  4. 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.
  5. 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.
  6. 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.
  7. 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.

How does a static client ID turn an MCP proxy server into a confused deputy?

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.

How do per-client consent, exact redirect URIs and state after consent protect an MCP proxy?

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.

How do per-user, per-tool downstream tokens with audience checks prevent MCP token passthrough?

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.

How do I ask Rightshold about this (free, no key)?

Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then 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.”

One-click setup

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

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.

What are the sources?

Related

Not legal advice. JSON · Markdown · All answers

Get a free API key in one call

No card and no email needed. 100 free credits; the license and policy checks stay free without a key.

curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_...   # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
  -H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'