MCP OAuth Stage 3: /oauth/mcp/authorize endpoint (PKCE-S256)#99
Open
heskew wants to merge 1 commit into
Open
Conversation
Closes #93. Stage 3 of MCP OAuth support (parent #86). Bridges incoming MCP authorization requests into the existing upstream-IdP flow, mints a single-use authorization code on the upstream callback, and redirects the user-agent back to the MCP client's redirect_uri. - New `mcp_auth_codes` Harper table (`@table(expiration: 300)`) — codes auto-expire after 5 min via native TTL; Stage 4's /token will read-then-delete for one-time use - New MCPAuthCodeStore with the explicit-field-access encode/decode pattern (CLAUDE.md tracked-object spread gotcha) - New handleAuthorize: two-phase validation per OAuth 2.1 §3.1.2.5 — client_id + redirect_uri exact-match before any redirect, everything else returns errors to the verified redirect_uri - RFC 8252 §7.3 loopback port flexibility — native MCP clients (Claude Desktop, mcp-remote) bind a dynamic port at runtime; we accept any port on registered loopback URIs (127.0.0.1 / [::1] / localhost) - PKCE S256 only (OAuth 2.1 §7.5.2 forbids plain); reject `plain` with invalid_request to the client URI - RFC 8707 `resource` parameter required, validated against canonical resource URI from Stage 2 - v1 single-provider constraint: mcp.providers (or the full registry) must resolve to exactly one upstream provider; 0 or >1 returns server_error. Multi-provider chooser is v1.1 - Bridge via CSRF state: the existing CSRFTokenData carries an `mcp` payload that handlers.ts handleCallback detects and detours into handleMCPCallback (callback.ts) — mints auth code, redirects to MCP client. Upstream IdP token never reaches the MCP client (spec MUST NOT, MCP authorization spec 2025-06-18) - handleCallback restructured: verify CSRF state FIRST so MCP-initiated upstream errors route back to the MCP client redirect_uri instead of the Harper app's default path. Legacy human flow behavior preserved for the no-state edge case - onLogin-mapped username (hookData.user) flows through to the issued auth code — Stage 4's JWT inherits the mapped identity, not the raw OAuth username - No Harper session is created on the MCP branch (independent lifecycle per #86 resolved decision) Gemini cross-review caught 4 of the above before push (loopback ports, mapped username, CSRF-before-IdP-error ordering, MCP-aware error redirects); all addressed in this PR before opening. 603 unit tests pass locally (+51 from this PR), including new MCP-branch integration tests in handlers.test.js. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
|
Reviewed; no blockers found. The PR successfully implements the MCP Authorization Server flow (Stage 2 of issue #86), including the core Surfaces verified:
|
|
Reviewed; no blockers found. |
3 tasks
heskew
commented
May 21, 2026
Member
Author
heskew
left a comment
There was a problem hiding this comment.
No blockers.
🤖 Posted by Antigravity on Nathan's behalf
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #93. Sub-issue of #86.
Summary
Stage 3 of MCP OAuth. Adds the user-facing authorize endpoint plus the surrounding plumbing — auth-code table with native TTL, bridge through the existing upstream IdP flow, callback branch that mints the code and redirects to the MCP client. No JWTs yet (that's Stage 4 / #94).
Where to look
src/lib/mcp/authorize.ts—handleAuthorizewith two-phase validation per OAuth 2.1 §3.1.2.5:client_id+redirect_urimust validate before any redirect; everything else routes errors to the verified redirect_uri. The interesting helper isredirectUriMatches— implements RFC 8252 §7.3 loopback port flexibility for127.0.0.1/[::1]/localhost. Native MCP clients bind dynamic ports at runtime and can't pre-register them.src/lib/handlers.ts—handleCallbackrestructured. CSRF state is now verified BEFORE the upstream error param is processed, so MCP-initiated errors (user denied at GitHub, etc.) route back to the MCP client's redirect_uri with OAuth-spec error codes instead of bouncing to the Harper app's default path. The legacy "no state, has error" UX is preserved for human-OAuth flows. On the success path, theonLogin-mapped username (hookData?.user ?? user.username) flows into the auth-code record, so Stage 4's JWT inherits the mapped identity.src/lib/mcp/callback.ts— the MCP branch fromhandleCallback. Mints the code withcrypto.randomBytes(32).toString('base64url'), persists the binding fields (client_id, user, resource, PKCE challenge, redirect_uri, scope), redirects to the client URI. No Harper session is created on this branch (independent MCP/human lifecycle per Add MCP OAuth flow support #86 resolved decision). Asserts no upstream IdP token leaks into the redirect URL.src/lib/mcp/authCodeStore.ts— CRUD against the newmcp_auth_codestable. Explicit field access in encode/decode (CLAUDE.md tracked-object spread gotcha).schema/oauth.graphql— newmcp_auth_codestable with@table(database: "oauth", expiration: 300)for native TTL.src/types.ts—MCPAuthorizeState(carried through CSRF metadata) andMCPAuthCodeRecord. Newmcp.providersconfig field.Design decisions worth flagging
mcp.providers(or, when unset, the full registry) must resolve to exactly one effective provider; 0 or >1 returnsserver_error. Multi-provider chooser UI is v1.1. Documented in theselectMCPProviderhelper.resourceparam exact match. Validated againstresolveResource(request, mcpConfig)from Stage 2 (same Host-header caveat applies — pinmcp.resourcein production).expiration: 300— 5-minute TTL is a safety net; Stage 4's /token will read-then-delete on exchange for one-time use.What was reviewed
Gemini CLI 0.42.0 cross-reviewed the pre-push diff. Four real findings, all addressed before push:
redirect_uri, breaking native clients. Now uses port-flexible matching on loopback hosts. Tests added covering all loopback variants.onLoginusername mapping lost in MCP branch — auth code was binding the raw OAuth username, ignoring the mapped identity from the hook. Now passeshookData?.user ?? user.usernamethrough.handleCallbackerror ordering — upstream IdP error was handled before CSRF verification, so MCP errors bounced to the Harper default path instead of the MCP client. CSRF verify is now first; legacy no-state UX preserved.catchblocks ignoredtokenData.mcpand usedoriginalUrl || postLoginRedirect(undefined for MCP flows). Both branches now route to the MCP client redirect_uri whenmcpStateis present.One Gemini point (resource URI stability via Host header) was informational — already covered by Stage 2's documentation.
Test plan
npm test); +51 from this PRnpm run lintcleannpm run format:checkcleanhandleCallbacktests cover the legacy behavior including the "no state, has error" preservationmcp-remotedriving the full discovery → DCR → authorize → token → bearer call is the conformance proofOut of scope
/oauth/mcp/tokenexchange (Stage 4, MCP OAuth Stage 4: /oauth/mcp/token endpoint + JWT issuer #94) — codes from this PR are unused until that landswithMCPAuthwrapper that consumes Stage 4 tokens (Stage 5, MCP OAuth Stage 5: withMCPAuth wrapper #95)🤖 Generated with Claude Code