Skip to content

MCP OAuth Stage 3: /oauth/mcp/authorize endpoint #93

@heskew

Description

@heskew

MCP OAuth Stage 3: /oauth/mcp/authorize endpoint

Sub-issue of #86. Bridges incoming MCP authorization requests into the existing upstream-IdP flow, with PKCE-S256 enforcement and short-lived authorization codes persisted in a TTL table.

Context

PR #89 (Stage 1) shipped the RFC 7591 Dynamic Client Registration endpoint and the persistent harper_oauth_mcp_clients table. PR #90 (Stage 2) shipped the well-known discovery documents that advertise authorization_endpoint. This stage builds the endpoint they point to: POST/GET /oauth/mcp/authorize. It does not mint tokens — that's Stage 4. It mints an authorization code that Stage 4 will exchange.

What this adds

  1. /oauth/mcp/authorize request handler (mounted via handleMCPPost for POST and the OAuthResource dispatcher for GET — TBD which verb per spec)
  2. mcp_auth_codes table declared in schema/oauth.graphql with @table(database: "oauth", expiration: 300) — Harper's native TTL handles auth code expiry; works across replicated nodes (no in-memory store needed)
  3. Request validation:
    • client_id exists in harper_oauth_mcp_clients
    • redirect_uri exact-matches a registered URI for that client (RFC 6749 §3.1.2.4 + MCP 2025-06-18)
    • code_challenge_method is S256 (reject plain with invalid_request; OAuth 2.1 §7.5.2)
    • code_challenge present and well-formed
    • resource parameter present and canonical-form (RFC 8707 §2)
    • response_type=code only
  4. Bridge to existing upstream OAuth flow — extends the existing CSRFTokenManager state payload to carry (client_id, resource, code_challenge, redirect_uri, scope) through the GitHub/Google/etc. callback in handleCallback. On successful upstream login, the callback mints an entry into mcp_auth_codes and redirects the user-agent to the MCP client's redirect_uri with code= and state=.
  5. Error responses per OAuth specinvalid_request, invalid_target, invalid_client, unsupported_response_type returned to the MCP client's redirect_uri as URL fragment params per OAuth 2.1

Spec requirements (all MUST, MCP authorization spec 2025-06-18)

  • PKCE S256 only; plain rejected
  • resource parameter required on /authorize (RFC 8707)
  • Exact redirect_uri matching against registered values
  • HTTPS for all AS endpoints (production)
  • No upstream IdP token passthrough — only the Harper-issued code is returned

Acceptance

  • mcp_auth_codes table declared in schema/oauth.graphql with TTL via expiration: 300
  • /oauth/mcp/authorize validates client_id, exact redirect_uri match, S256 PKCE, canonical resource, response_type=code
  • Reject plain PKCE with invalid_request
  • Reject missing resource with invalid_target
  • Reject unregistered client_id with invalid_client
  • Reject unregistered redirect_uri with invalid_request
  • On valid request, redirect user-agent to the configured upstream provider's authorize URL via the existing flow
  • On upstream callback success, mint a row in mcp_auth_codes bound to (client_id, resource, code_challenge, redirect_uri, user) with expiration: 300
  • On upstream callback success, redirect user-agent back to MCP client redirect_uri with code and state
  • onLogin lifecycle hook fires on MCP-initiated auth, same as for human OAuth
  • Unit tests cover all rejection branches and the happy-path code mint
  • No upstream-provider token returned to MCP client at any point

Dependencies

Out of scope

  • Issuing JWT access tokens (Stage 4)
  • The withMCPAuth wrapper that consumes those tokens (Stage 5)
  • onMCPTokenIssued hook (Stage 6)

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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