Problem
Most MCP servers register OAuth clients as public clients — the authorization server issues a client_id but no client secret, and the token endpoint auth method is none. This is the default behaviour of the official MCP SDK and of FastMCP's OAuth proxy (which keeps the real upstream secret to itself and hands out secretless clients), and it is the only mode allowed by the newer client-identity-document approach. Secret-issuing servers (Notion, Snowflake, Clerk-fronted ones) are the minority.
Core already talks to public clients correctly on the wire — the token request carries the client id in the body with no secret. The gaps are around them.
What needs fixing
1. A public client cannot be configured by hand.
Only dynamic registration can produce a public client today. When an operator configures OAuth manually, a client secret is mandatory. So a genuinely secretless client either can't be onboarded at all, or has to be registered with a dummy secret that is stored and never used. Manual configuration should accept a public client: no secret, auth method none.
2. Nothing guarantees PKCE for a public client.
PKCE is enabled only when the authorization server advertises support for it in its metadata. If it doesn't, core silently proceeds with no proof key. For a client that also has no secret, that means the code exchange is protected by nothing at all — anyone who intercepts the authorization code can redeem it.
For a public client, PKCE should not be optional: core should always send a challenge (defaulting to S256), and refuse to complete registration for a secretless client against a server that cannot do PKCE, rather than degrading quietly.
3. The PKCE secret is shared instead of being per sign-in.
The verifier/challenge pair is generated once when the connection is registered and then stored and reused for every subsequent sign-in, by every user of that connection. PKCE is meant to be a fresh, single-use secret per authorization request — reusing one removes most of its value, and for a public client it is the only protection the token exchange has.
Each sign-in should generate its own verifier/challenge pair, bound to that one attempt, and discard it once the code has been exchanged (or the attempt expires).
Impact
Items 2 and 3 are security weaknesses on the most common MCP authorization path. Item 1 is a functional gap that blocks manual onboarding of public-client servers.
Problem
Most MCP servers register OAuth clients as public clients — the authorization server issues a
client_idbut no client secret, and the token endpoint auth method isnone. This is the default behaviour of the official MCP SDK and of FastMCP's OAuth proxy (which keeps the real upstream secret to itself and hands out secretless clients), and it is the only mode allowed by the newer client-identity-document approach. Secret-issuing servers (Notion, Snowflake, Clerk-fronted ones) are the minority.Core already talks to public clients correctly on the wire — the token request carries the client id in the body with no secret. The gaps are around them.
What needs fixing
1. A public client cannot be configured by hand.
Only dynamic registration can produce a public client today. When an operator configures OAuth manually, a client secret is mandatory. So a genuinely secretless client either can't be onboarded at all, or has to be registered with a dummy secret that is stored and never used. Manual configuration should accept a public client: no secret, auth method
none.2. Nothing guarantees PKCE for a public client.
PKCE is enabled only when the authorization server advertises support for it in its metadata. If it doesn't, core silently proceeds with no proof key. For a client that also has no secret, that means the code exchange is protected by nothing at all — anyone who intercepts the authorization code can redeem it.
For a public client, PKCE should not be optional: core should always send a challenge (defaulting to S256), and refuse to complete registration for a secretless client against a server that cannot do PKCE, rather than degrading quietly.
3. The PKCE secret is shared instead of being per sign-in.
The verifier/challenge pair is generated once when the connection is registered and then stored and reused for every subsequent sign-in, by every user of that connection. PKCE is meant to be a fresh, single-use secret per authorization request — reusing one removes most of its value, and for a public client it is the only protection the token exchange has.
Each sign-in should generate its own verifier/challenge pair, bound to that one attempt, and discard it once the code has been exchanged (or the attempt expires).
Impact
Items 2 and 3 are security weaknesses on the most common MCP authorization path. Item 1 is a functional gap that blocks manual onboarding of public-client servers.