Skip to content

Embedded auth server rejects DCR from confidential MCP clients (token_endpoint_auth_method hardcoded to none) #5321

@nuclon

Description

@nuclon

Summary

The embedded authorization server's DCR endpoint hardcodes token_endpoint_auth_method to none and refuses any client that registers with a different method or that expects a client_secret returned in the DCR response. This blocks any MCP client that implements OAuth as a confidential client.

Currently failing in production: Perplexity's MCP integration. ChatGPT's MCP support appears to follow the same confidential-client pattern (unverified in our setup — flagging as likely affected based on its public OAuth UX, would be good to confirm).

Working fine with the current implementation: Claude Code, Claude Desktop, Cursor, MCP Inspector (all public-client + PKCE).

Reproduction

  1. Deploy ToolHive operator v0.27.2 with a VirtualMCPServer that uses authServerConfig (embedded AS, any upstream — e.g. AWS Cognito or Google via OIDC upstream provider).
  2. In Perplexity's MCP server configuration, add the vMCP URL (e.g. https://mcp.example.com/).

First failure (already addressed by adding offline_access to advertised scopes — separate concern, calling out for completeness):

[API_CLIENTS_ERROR] {"error":"invalid_client_metadata","error_description":"default scope not supported by server: offline_access"}

After adding offline_access to incomingAuth.oidcConfigRef.scopes, second failure:

[API_CLIENTS_ERROR] Dynamic client registration did not return a client_secret

Server-side, the AS logs failed to create access request with error: "invalid_client" once Perplexity attempts the token exchange with a client_secret it expected but never received.

Root cause

pkg/authserver/server/registration/dcr.go:219-229 at tag v0.27.2:

// 5. Validate/default token_endpoint_auth_method
authMethod := req.TokenEndpointAuthMethod
if authMethod == "" {
    authMethod = "none"
}
if authMethod != "none" {
    return nil, &DCRError{
        Error:            DCRErrorInvalidClientMetadata,
        ErrorDescription: "token_endpoint_auth_method must be 'none' for public clients",
    }
}

This is reinforced by /.well-known/openid-configuration, which advertises:

"token_endpoint_auth_methods_supported": ["none"]

The intent is clearly to enforce the OAuth 2.1 / RFC 7591 best practice for native clients (no shared secrets in distributed apps), which is correct for many MCP clients. However, some commercial MCP clients (Perplexity, likely ChatGPT) implement their MCP integration as a server-side confidential client and require a client_secret to be issued at DCR time. For those clients, there is no workaround on the client side; the AS must support client_secret_basic or client_secret_post.

Impact

Client Behaviour
Claude Code, Claude Desktop, Cursor, MCP Inspector ✅ Work (public client + PKCE)
Perplexity ❌ Fail at DCR — needs client_secret returned
ChatGPT Likely ❌ — uses confidential-client OAuth in its other integrations
VS Code ✅ (per #2585)
WindSurf Untested per #2585

Without confidential-client support, the embedded AS cannot serve as the auth surface for the full set of MCP-capable clients an enterprise may want to support.

Proposed solution

Add an opt-in flag on EmbeddedAuthServerConfig (e.g. allowConfidentialClients: bool) that, when set:

  1. Accepts token_endpoint_auth_method values of client_secret_basic and client_secret_post (in addition to none) at /oauth/register.
  2. Returns a freshly generated client_secret in the DCR response when the registered method requires one (RFC 7591 §3.2.1).
  3. Stores the secret hashed in the credential store (the work in Persistent DCRCredentialStore backends (memory + Redis) #4979 / Persistent DCRCredentialStore: wire into EmbeddedAuthServer #5185 already provides the persistence layer).
  4. Validates client_secret_basic / client_secret_post at /oauth/token.
  5. Advertises the additional methods in token_endpoint_auth_methods_supported only when the flag is set.

This keeps the safe default (PKCE-only public clients) while letting operators opt into confidential clients when they need to support clients like Perplexity. The field documentation should mention that operators are accepting the risk of leaked secrets in distributed clients.

Alternative: support Client ID Metadata Document (CIMD) as a third path — but CIMD doesn't issue client_secret either, so it doesn't directly help Perplexity unless Perplexity adds CIMD support too.

Workarounds

For users hitting this today:

  • Skip Perplexity — use only public-client MCP clients (Claude Code, Cursor, etc.).
  • Use a different IdP that supports both public and confidential DCR — Keycloak, Authentik, ZITADEL, Auth0 — and point MCPOIDCConfig directly at it without the embedded AS. Adds infra weight and isn't a great fit if Cognito/Google is already the source of truth.

References

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions