fix: restore OAuth AS metadata endpoint for Claude Code DCR flow#96
fix: restore OAuth AS metadata endpoint for Claude Code DCR flow#96masnwilliams merged 4 commits intomainfrom
Conversation
The /.well-known/oauth-authorization-server route was accidentally deleted in 9991d7e, causing Claude Code to set oauthMetadataFound:false and fail the token exchange after DCR. Restoring it with our proxied endpoints (not Clerk's directly) so the full OAuth 2.1 flow completes. Also fixes registration_endpoint in the protected-resource metadata to point at our /register (which enforces openid-only scope) instead of Clerk's /oauth/register (which defaults to email+profile+offline). Made-with: Cursor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
I'll monitor this deployment which adds the What I'll watch for:
Note: The kernel-mcp-server doesn't emit telemetry to our observability platform, so monitoring is limited to deployment status and downstream effects. I'll verify the deployment succeeds and watch for any anomalies in the main Kernel API that could indicate MCP auth issues. This is a low-risk change with straightforward rollback if needed. |
Claude Code sends client_id and client_secret via HTTP Basic Auth (Authorization: Basic base64(client_id:client_secret)) when DCR returns a client_secret. Our /token route only checked the form body, causing "missing client_id" 400 errors on every token exchange. Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 071b81e. Configure here.
| const colonIdx = decoded.indexOf(":"); | ||
| if (colonIdx !== -1) { | ||
| clientId = decodeURIComponent(decoded.slice(0, colonIdx)); | ||
| clientSecret = decodeURIComponent(decoded.slice(colonIdx + 1)); |
There was a problem hiding this comment.
Basic auth decoding mishandles form-encoded + as space
Low Severity
RFC 6749 Section 2.3.1 requires client_id and client_secret to be application/x-www-form-urlencoded before base64-encoding in the Basic auth header. Form encoding converts spaces to +, but decodeURIComponent does not decode + back to space — it only handles %XX sequences. If a credential ever contains a space, + would be kept literally instead of being decoded. The correct approach is to replace + with space before calling decodeURIComponent.
Reviewed by Cursor Bugbot for commit 071b81e. Configure here.
|
Monitoring Complete - Successful The deployment to Production has completed successfully with no issues detected. Summary:
Note on verification: As identified in the monitoring plan, the kernel-mcp-server doesn't emit telemetry to our observability platform. We monitored for downstream effects on the main Kernel API (which would surface if MCP OAuth flows broke), and found none. Manual verification of the new This change appears to be working correctly. |


Summary
Fixes MCP OAuth flow for Claude Code (and likely other MCP clients that use
client_secret_basicauth).Two bugs fixed:
Token endpoint doesn't handle Basic auth — Our
/register(DCR) returns aclient_secret(Clerk returns one even forpublic: trueapps). Claude Code usesclient_secret_basicauth method, sendingAuthorization: Basic base64(client_id:client_secret)on the token exchange. Our/tokenroute only readclient_idfrom the form body → 400 "missing client_id" on every token exchange. Customers complete the entire OAuth flow and silently fail at the last step, ending up withaccessToken: "".Missing
/.well-known/oauth-authorization-serverendpoint — Accidentally deleted in9991d7e(Aug 2025 refactor). CausesoauthMetadataFound: falsein Claude Code. Not the direct cause of failure (Claude Code falls back to path guessing), but spec-noncompliant and may break other MCP clients.Also fixed:
registration_endpointin protected-resource metadata pointed at Clerk's/oauth/register(which defaults toemail+profile+offlinescopes) instead of our/register(which enforcesopenid-only).Customer impact
Any user connecting Claude Code to the Kernel MCP server via OAuth would hit this. DCR succeeds, authorization completes, but the token exchange silently fails. The credential keychain shows
accessToken: "",expiresAt: 0.Workaround for existing users: After deploying this fix, customers need to clear stale credentials:
security delete-generic-password -s "Claude Code-credentials"Then reconnect to the MCP server.
Changes
src/app/token/route.ts— Extractclient_id/client_secretfromAuthorization: Basicheader when not present in form bodysrc/app/.well-known/oauth-authorization-server/route.ts— Restore RFC 8414 AS metadata endpoint with our proxied endpointssrc/app/.well-known/oauth-protected-resource/mcp/route.ts— Pointregistration_endpointat our/registerinstead of Clerk'sTest plan
POST /tokenwith Basic auth header returns 200 (verified onmcp.dev.onkernel.com)GET /.well-known/oauth-authorization-serverreturns valid metadataGET /.well-known/oauth-protected-resource/mcpshowsregistration_endpoint→ our/registerNote
Medium Risk
Touches OAuth discovery and
/tokenexchange logic; while changes are small, parsingAuthorization: Basicand altering metadata endpoints can impact interoperability and auth flows if incorrect.Overview
Restores the RFC 8414 authorization server discovery endpoint at
/.well-known/oauth-authorization-server, returning proxied issuer/endpoint metadata with CORS enabled.Fixes MCP protected-resource metadata to advertise the local
/registerDCR endpoint (instead of Clerk’s), aligning the registration flow withopenid-only expectations.Updates
/tokento acceptclient_secret_basicby extractingclient_id/client_secretfrom theAuthorization: Basicheader when missing from the form body and forwarding them to the Clerk token exchange.Reviewed by Cursor Bugbot for commit 071b81e. Bugbot is set up for automated code reviews on this repo. Configure here.