Skip to content

Architecture: Phase 2 - OAuth authorization code flow for claude.ai web connector #6

Description

@syswave-dev

Context

Anthropic's Claude products use different auth mechanisms for MCP
connectors:

This means: even with Phase 1 (#5) shipped, claude.ai web users still
can't connect to PullMD. Phase 2 closes that gap.

Why phase this separately

OAuth on top of a real user system is straightforward. OAuth without
a user system underneath (which is what was attempted in #4) leads to
inconsistencies and security issues. Sequencing matters:

  1. Phase 1 (Architecture: Phase 1 - Multi-user system with sessions + API keys #5): user system, sessions, API keys
  2. Phase 2 (this): OAuth flow that authenticates against the same user
    system

By the time we tackle Phase 2, the heavy lifting is done. OAuth becomes
a thin layer that:

  • Asks the user to log in (existing /login flow from Architecture: Phase 1 - Multi-user system with sessions + API keys #5)
  • Shows a consent screen ("claude.ai wants to access PullMD as you,
    approve?")
  • Returns an authorization code to claude.ai
  • Exchanges code for access token at /oauth/token
  • Token represents the user, just like an API key from Phase 1

Three integration options with Anthropic

From their docs:

Option A: Dynamic Client Registration (DCR)

  • Anthropic registers a new OAuth client on each fresh user connection
  • Requires registration_endpoint in our authorization server metadata
  • Pro: no manual coordination with Anthropic
  • Con: can result in many client registrations on our side

Option B: Client ID Metadata Document (CIMD)

  • A single static client identified by a URL pointing to client metadata
  • Pro: stable, no proliferation of client records
  • Con: requires our auth server to support CIMD (a newer pattern)

Option C: oauth_anthropic_creds

  • We register manually with Anthropic by emailing them client_id and
    client_secret
  • Anthropic stores these and uses them for token exchange on user's behalf
  • Pro: stable, simple on our side
  • Con: requires manual coordination with Anthropic, doesn't scale to
    forks/multiple deployments

For PullMD as a self-hosted project where users run their own instances,
Option B (CIMD) is probably the right fit - it's stable, scales to
many deployments, and doesn't require each PullMD operator to email
Anthropic.

But this needs validation. Anthropic's docs recommend CIMD or
oauth_anthropic_creds over DCR for high-traffic directory entries,
which suggests CIMD is the modern preferred path.

Required server-side components

On top of Phase 1's user system:

  • /.well-known/oauth-authorization-server (RFC 8414 metadata)
  • /.well-known/oauth-protected-resource (MCP spec requirement)
  • /oauth/authorize (with user-consent screen)
  • /oauth/token (authorization code exchange + refresh)
  • /oauth/revoke (token revocation)
  • Audience-bound tokens (resource claim per RFC 8707)
  • PKCE with S256 (already correct in feat(auth): OAuth2 security for MCP endpoint — bearer token + client credentials + PKCE #4's implementation)
  • Redirect URI allowlist (claude.ai callbacks:
    https://claude.ai/api/... and https://claude.com/api/...)
  • Scoped consent ("This app will be able to read URLs and access your
    history")

Security considerations (from #4 review)

The earlier OAuth attempt in #4 had legitimate code structure but
several blockers that we want to avoid repeating:

  • /authorize must require user authentication (Phase 1 enables this)
  • redirect_uri allowlist with exact match (claude.ai's published URLs)
  • Tokens must carry audience/resource claim
  • WWW-Authenticate header must include resource_metadata
  • Trust proxy properly (or use PUBLIC_URL env var for baseUrl)
  • timingSafeEqual without short-circuit
  • Test coverage for all auth paths
  • Rate limiting on /oauth/token and /oauth/authorize

The PKCE implementation skeleton from #4 (S256-only, one-time auth
codes, separate signing keys) is reusable. The flow logic is what
needs to be rebuilt on top of Phase 1's user model.

Open questions

  1. CIMD vs oauth_anthropic_creds: which integration path? Probably
    CIMD, but want to validate by looking at how other self-hosted
    MCP servers solved this.

  2. Refresh token strategy: rotation on every refresh, or stable
    refresh token? Anthropic docs mention "Honor refresh-token rotation
    correctly if you rotate tokens." Suggests rotation is supported but
    not required.

  3. Token TTL: Phase 1 API keys can be long-lived because they're
    user-controlled. OAuth access tokens should be short-lived (e.g. 1
    hour) with refresh. Need to settle on numbers.

  4. Multi-instance / horizontal scaling: if PullMD is ever scaled
    horizontally, OAuth state (auth codes, refresh tokens) needs shared
    storage. SQLite isn't sufficient for that. But probably not a
    Phase 2 blocker - single-instance is the realistic deployment model
    for now.

Dependencies

Hard dependency on #5 (Phase 1). Don't start Phase 2 work before
Phase 1 is merged and stable.

Scope of work

Rough breakdown (after Phase 1 ships):

  • AS metadata endpoints (/.well-known/*)
  • /oauth/authorize endpoint with user-consent screen
  • /oauth/token endpoint (code exchange + refresh)
  • /oauth/revoke endpoint
  • Audience-bound token signing
  • Redirect URI allowlist
  • PKCE flow (reuse skeleton from feat(auth): OAuth2 security for MCP endpoint — bearer token + client credentials + PKCE #4)
  • Consent screen UI in PWA
  • Integration with Anthropic (CIMD or oauth_anthropic_creds)
  • Testing against actual claude.ai web connector
  • Documentation: README, OAuth setup guide
  • Tests covering all OAuth flows

Estimated effort: ~1-2 weeks once Phase 1 is in place. Most of the
hard work (user system, sessions) is done by Phase 1.

Discussion

Less urgent than #5. Tracking here so it's not lost. Comments and
prior-art links welcome - if anyone has shipped MCP OAuth against
claude.ai successfully, would love to hear about your setup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    architectureArchitecture decisions and big-picture designdeferredTracked but not actively worked onenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions