You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
/oauth/mcp/token endpoint — dispatched via handleMCPPost when providerName === 'mcp' && action === 'token'
Signing key persistence — harper_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
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
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
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
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
MCP OAuth Stage 4:
/oauth/mcp/tokenendpoint + JWT issuerSub-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 themcp_auth_codestable. This stage exchanges those codes for JWT access tokens that MCP clients use asAuthorization: Bearer ...on the actual MCP endpoint.What this adds
/oauth/mcp/tokenendpoint — dispatched viahandleMCPPostwhenproviderName === 'mcp' && action === 'token'harper_oauth_mcp_keystable declared inschema/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 viamcp.signingAlgorithmconfigtokenIssuer.ts— JWT mint and verify usingjsonwebtoken(already a runtime dep). Claims:iss(frommcp.issuer),sub(Harper user),aud(resource URI from RFC 8707),client_id,exp,iat,jtimcp_refresh_tokenstable with single-use rotation per OAuth 2.1. Replay of a used refresh token returnsinvalid_grantand revokes the entire family/.well-known/jwks.jsonplaceholder is filled in fromharper_oauth_mcp_keys(public key only). The advertisement in AS metadata matches what's actually servedcode_verifieragainst the storedcode_challengefrommcp_auth_codesSpec requirements (all MUST, MCP 2025-06-18)
audclaim must equal the canonical resource URIapplication/jsonPOST per RFC 6749 §3.2Acceptance
harper_oauth_mcp_keystable declared inschema/oauth.graphqlmcp.signingAlgorithm: "EdDSA"config/oauth/mcp/tokenexchanges anauthorization_codefor an audience-bound JWTcode_verifiermust hash to the storedcode_challenge(elseinvalid_grant)mcp_auth_codes(one-time use)audclaim matches the resource URI from the original authorize requestissclaim matchesmcp.issuer(pinned at startup, not derived per-request — addresses Stage 2's Host-header concern for signing)refresh_tokengrant type: presenting a refresh token issues a new access token; the old refresh token is invalidated and a new one issued (rotation)invalid_grantand revokes the family/.well-known/jwks.jsonis updated to return the current public key(s); Stage 2's placeholder is removedDependencies
withMCPAuthverifies tokens this stage issues)Out of scope
🤖 Generated with Claude Code