Skip to content

MCP OAuth Stage 4: /oauth/mcp/token endpoint + JWT issuer #94

@heskew

Description

@heskew

MCP OAuth Stage 4: /oauth/mcp/token endpoint + JWT issuer

Sub-issue of #86. Mints audience-bound JWTs in exchange for authorization codes, with refresh-token rotation per OAuth 2.1.

Context

PR #89 shipped DCR (Stage 1); PR #90 ships the well-known docs (Stage 2) including a placeholder JWKS that returns {"keys": []}. Stage 3 (/oauth/mcp/authorize) will mint authorization codes into the mcp_auth_codes table. This stage exchanges those codes for JWT access tokens that MCP clients use as Authorization: Bearer ... on the actual MCP endpoint.

What this adds

  1. /oauth/mcp/token endpoint — dispatched via handleMCPPost when providerName === 'mcp' && action === 'token'
  2. Signing key persistenceharper_oauth_mcp_keys table declared in schema/oauth.graphql. On first boot if no key is configured, generate one and persist; the table is the single source of truth across replicated Harper nodes (file storage would diverge). RS256 (2048-bit RSA) as default, EdDSA (Ed25519) optional via mcp.signingAlgorithm config
  3. tokenIssuer.ts — JWT mint and verify using jsonwebtoken (already a runtime dep). Claims: iss (from mcp.issuer), sub (Harper user), aud (resource URI from RFC 8707), client_id, exp, iat, jti
  4. Refresh-token store — new mcp_refresh_tokens table with single-use rotation per OAuth 2.1. Replay of a used refresh token returns invalid_grant and revokes the entire family
  5. JWKS publication — Stage 2's /.well-known/jwks.json placeholder is filled in from harper_oauth_mcp_keys (public key only). The advertisement in AS metadata matches what's actually served
  6. PKCE verification — verify the presented code_verifier against the stored code_challenge from mcp_auth_codes

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

  • Access tokens MUST be audience-bound (RFC 8707) — JWT aud claim must equal the canonical resource URI
  • PKCE S256 verification on every code exchange
  • Refresh tokens MUST rotate for public clients
  • Tokens MUST be issued via application/json POST per RFC 6749 §3.2
  • Short-lived access tokens (default 1h, configurable)
  • HTTPS for the token endpoint

Acceptance

  • harper_oauth_mcp_keys table declared in schema/oauth.graphql
  • On first boot, signing keypair is generated and persisted if not provided via config; reused on subsequent boots
  • Default algorithm RS256; EdDSA supported via mcp.signingAlgorithm: "EdDSA" config
  • /oauth/mcp/token exchanges an authorization_code for an audience-bound JWT
  • PKCE S256 verification: presented code_verifier must hash to the stored code_challenge (else invalid_grant)
  • Exchanged code is deleted from mcp_auth_codes (one-time use)
  • JWT aud claim matches the resource URI from the original authorize request
  • JWT iss claim matches mcp.issuer (pinned at startup, not derived per-request — addresses Stage 2's Host-header concern for signing)
  • refresh_token grant type: presenting a refresh token issues a new access token; the old refresh token is invalidated and a new one issued (rotation)
  • Presenting a previously-used refresh token returns invalid_grant and revokes the family
  • /.well-known/jwks.json is updated to return the current public key(s); Stage 2's placeholder is removed
  • Unit tests cover code exchange (success + each rejection branch), PKCE verify, refresh rotation, replay detection, key generation, JWKS serialization
  • No upstream IdP token included in MCP token claims or response

Dependencies

Out of scope

  • Bearer-token validation on the MCP endpoint (Stage 5)
  • Audit hook for token issuance (Stage 6)
  • Token introspection / revocation endpoints (RFC 7662 / 7009 — out of scope for v1 per Add MCP OAuth flow support #86)

🤖 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