Why is a static client_id unsafe in an MCP server that proxies OAuth?
answer
- upstream sees one client for everyone
- approval is remembered, prompt is skipped
- nobody's secret was stolen
- the callback decides who gets the code
- consent per downstream client, not once
basics
~20 sBecause the upstream provider sees one client for every user. Once any user has approved it, the provider skips its consent screen on later requests, so an attacker who gets a callback registered can have a victim's authorization code delivered to them.
solid answer
~60 sAn MCP server that fronts a third-party provider is acting as an OAuth client to that provider while accepting its own clients downstream. If it uses a single static `client_id` upstream and lets downstream clients supply their own redirect URIs, the upstream provider cannot tell those downstream clients apart — it sees one client. Providers remember a user's approval of a client, typically in a cookie, and skip the consent screen on subsequent authorization requests for it. So an attacker who has registered a callback with the proxy can craft a link that reaches the provider under the same static `client_id`; the victim, already having approved it once, sees no prompt, and the code is redirected to the attacker's URI. The proxy is the confused deputy: its authority is used by a caller who should not have it. MCP's guidance is to obtain explicit user consent for each dynamically registered downstream client before forwarding, and to give clients distinct identities through pre-registration or a Client ID Metadata Document rather than sharing one.
go deeper
Know the headline: sharing one client_id across all users means the provider sees a single application, so an approval given once can be silently reused by someone else.
Walk the sequence — remembered approval, skipped consent screen, attacker-controlled redirect URI, code delivered to the attacker — and name the required control: explicit user consent at the proxy for each dynamically registered client.
Show that no credential was stolen: the proxy's authority was borrowed, not taken. Be ready to reject the wrong fixes (secret rotation, PKCE alone) and to specify exact-match redirect validation plus per-client consent state.
Own the design decision of whether to proxy at all. Argue when a per-user upstream registration or direct client-to-provider authorization is safer than one shared identity, and how you would give downstream clients real identities across your platform.
## The topology that creates the problem A common deployment has a remote MCP server sitting in front of some third-party API. To the outside world it is an MCP server that clients authorize against. To the third-party provider it is an ordinary OAuth client. That double role is the setup: it holds credentials and authority upstream, and it accepts requests from parties it did not vet downstream. The tempting implementation is to register once with the provider, hardcode that `client_id` and secret in the proxy, and accept whatever downstream clients turn up — including dynamically registered ones that supply their own redirect URIs. Everything works in testing. ## What actually goes wrong The provider's consent screen is per client. Once a user has approved a given `client_id`, the provider records that approval — most often as a cookie in the user's browser — and on subsequent authorization requests for the same client it will not ask again. That behaviour is deliberate and normally desirable: users should not re-consent to the same application on every login. But with a static upstream `client_id`, *every* downstream client is that same application from the provider's point of view. Distinctions the proxy makes internally are invisible upstream. The attack follows directly. An attacker registers a client with the proxy, supplying a redirect URI they control. They then get a victim who has previously used the proxy to visit a crafted authorization link that goes to the provider under the shared static `client_id`, with the attacker's redirect URI carried through. The provider sees a client this user already approved, skips the consent screen entirely, issues an authorization code and redirects it to the attacker's URI. There was no moment at which the victim could notice: the prompt they would have refused never appeared. The attacker then exchanges the code through the proxy and obtains access as the victim. ## Why it is called a confused deputy The proxy is the deputy. It holds authority with the provider that its downstream callers do not have, and it exercises that authority on their behalf without distinguishing whose authority is really in play. The attacker never obtains the proxy's credentials; they get the proxy to use them. That is why the mitigation cannot be 'protect the secret better' — the secret was never exposed. ## The fixes **Consent per downstream client.** MCP's security guidance is explicit: an MCP server that forwards authorization to a third-party provider on behalf of dynamically registered clients must obtain the user's explicit consent for each such client, at the proxy, before forwarding the request upstream. This restores the prompt the upstream provider skipped. Crucially it must be consent the proxy tracks per downstream client identity, not a single blanket approval reused for all of them — otherwise the same skip reappears one layer down. **Distinct client identity.** The deeper fix is to stop having one identity stand in for many. Where the architecture allows it, downstream clients should carry their own identity — a pre-registered `client_id`, or a Client ID Metadata Document whose HTTPS URL is the `client_id` — so consent binds to something specific rather than to a shared placeholder. Note that revision 2026-07-28 deprecated RFC 7591 dynamic client registration in favour of CIMD precisely because self-published, self-describing identity is easier to reason about than a stream of identical-looking registrations. **Strict redirect URI handling.** Any redirect URI the proxy forwards or accepts must be validated against the metadata for the specific client that registered it, with exact matching rather than prefix or wildcard rules. A permissive redirect check turns a survivable mistake into an open redirect for authorization codes. ## What does not fix it PKCE does not fix it: the attacker can run their own complete flow, so they will happily supply their own verifier. Rotating the upstream secret does not fix it, because the secret was never the leaked item. Rate limiting and short code lifetimes shrink the window but do not remove the missing consent step. And 'only allowlisted redirect hosts' helps only until the allowlist has a genuinely dynamic member. ## How to talk about it The distinguishing signal in an interview is whether a candidate identifies the *missing prompt* as the vulnerability rather than a leaked credential. Say plainly what the provider sees (one client), what it therefore skips (consent), and who ends up holding the code (whoever registered the callback). Then give the two-part remedy: consent at the proxy for each downstream client, and distinct client identity so that consent means something.
- Does mandatory PKCE close this hole?No. PKCE binds an authorization code to the party that started that particular flow, and here the attacker is that party — they generate their own verifier and challenge quite legitimately. The defect is a consent screen that never appeared, which no proof-of-possession on the code can restore.
- The proxy already shows its own consent screen once, at first connection. Is that enough?Not if that approval is reused for every downstream client. The whole failure is one approval standing in for many distinct callers, so repeating the pattern at the proxy just moves it. Consent has to be recorded against the specific downstream client identity and re-obtained when a client the user has not approved appears.
- How does using a Client ID Metadata Document change the picture?It gives each downstream client an identity of its own — an HTTPS URL that names it and resolves to its metadata, including the redirect URIs it may use. Consent can then bind to that identifier and be shown to the user with the actual domain attached, instead of being granted to an opaque shared client_id that means nothing specific.
saying these in an interview costs you the question
- Says the fix is protecting the upstream client secret
- Claims PKCE prevents the attack entirely
- Treats one blanket consent as covering every downstream client
- Validates redirect URIs by prefix or wildcard match
- Assumes the provider distinguishes downstream callers