Skip to content

fix(oauth): accept public-client authorization_code exchange (PKCE, no secret)#159

Merged
EsTharian merged 1 commit intomainfrom
fix/token-public-client-auth-code
Apr 24, 2026
Merged

fix(oauth): accept public-client authorization_code exchange (PKCE, no secret)#159
EsTharian merged 1 commit intomainfrom
fix/token-public-client-auth-code

Conversation

@EsTharian
Copy link
Copy Markdown
Member

Summary

  • Every POST /oauth/token with grant_type=authorization_code from a public client (dyn-reg, token_endpoint_auth_method=none) was being rejected with invalid_client — the handler only routed refresh_token through the public-client-capable authenticator
  • OAuth 2.1 §4.1.3: for public clients, the PKCE code_verifier + client_id binds the code to the client; no client_secret is required
  • Rename authenticateClientForRefreshauthenticateClientPublicOrConfidential (logic was already grant-agnostic; name was misleading) and use it for authorization_code too
  • client_credentials stays confidential-only by definition

Hit in production when Claude Code's dyn-reg flow completed the browser step and the callback exchange blew up silently. Every real MCP 2025-06-18 client that uses dyn-reg + PKCE was broken by this.

Test plan

  • New test: public client with token_endpoint_auth_method=none gets tokens from /oauth/token with client_id alone (no client_secret, no Basic header); passwordHasher.verifyPassword never called
  • Existing confidential-client authorization_code tests still pass (body client_secret path + Basic auth path)
  • End-to-end: Claude Code's browser flow now completes the token exchange and the MCP session unblocks

🤖 Generated with Claude Code

…o secret)

POST /oauth/token was rejecting every authorization_code grant from
public clients (token_endpoint_auth_method=none) with invalid_client.
The handler only routed refresh_token through the public-client-capable
authenticator; authorization_code fell through to authenticateClient
which demands a client_secret.

OAuth 2.1 §4.1.3: PKCE code_verifier + client_id binds the code to the
client; no client_secret is needed for public clients. Claude Code and
every other dynamic-reg PKCE client was broken by this.

Fix:
- Rename authenticateClientForRefresh → authenticateClientPublicOrConfidential
  (the logic was already grant-agnostic; the name was misleading)
- Use it for authorization_code grants too
- client_credentials stays confidential-only (cannot be a public grant)
- Add test covering public client by client_id alone

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 24, 2026

View your CI Pipeline Execution ↗ for commit f149a1b

Command Status Duration Result
nx affected -t lint test build ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-24 18:04:08 UTC

@EsTharian EsTharian merged commit 315f67c into main Apr 24, 2026
3 checks passed
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the OAuth token endpoint to support public clients for the authorization_code grant type, aligning with OAuth 2.1 specifications. The function authenticateClientForRefresh has been renamed to authenticateClientPublicOrConfidential and is now utilized for both authorization_code and refresh_token grants. This allows public clients (where token_endpoint_auth_method is 'none') to authenticate using only their client_id. A corresponding test case was added to ensure correct behavior for public clients during the authorization code flow. I have no feedback to provide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant