Skip to content

OAuth slice 3: dynamic client registration with rate limiting and unapproved-app expiry #797

Description

@mforce

Slice 3 of #788. Depends on #795.

The MCP specification recommends that clients register themselves rather than being pre-configured. #788 chose automatic self-registration, which means an endpoint that any client on the internet can call.

Scope

Why this is safe to leave open

Registering grants zero access. A registered client can do nothing until a user approves it on the consent screen (slice 4), and that approval requires re-entering a password. The real risk is junk rows, not unauthorized access, and the two controls above address it.

State that reasoning in the code, because "anyone on the internet can POST here" reads as alarming without it.

Worth deciding here

Decide whether a farm can opt out of assistant connections entirely. #788 considered an Owner-level switch and did not choose it, because each new required config key means updating two harnesses that CI does not run (the #370 sim harness and the #565 AppHost), and that update is easy to get wrong. If a switch is wanted, it belongs here, and it should be account data rather than configuration.

Done when

A client can register, the server rate-limits a client that hammers the endpoint, and an unapproved registration disappears after the expiry window.

Activity

  1. mforce commented on Sep 13, 2026

    @mforce
    OwnerAuthor

    Correction: DCR is not what the MCP specification recommends.

    This issue's rationale said dynamic client registration is "what the MCP spec recommends". An adversarial review checked the published specification and found that claim wrong.

    The 2025-11-25 specification recommends Client ID Metadata Documents. It describes Dynamic Client Registration as optional, for backward compatibility or specific requirements.

    Left uncorrected, the claim leads someone to build DCR-only support and expect the automatic interoperability the spec describes. A client that implements only the recommended metadata-document mechanism would not interoperate with a DCR-only server. The two are different mechanisms, and supporting one does not imply the other.

    This does not cancel the slice. DCR remains a defensible choice, because clients in the field implement it and OpenIddict supports it well. But it is a deliberate compatibility decision rather than spec compliance, and the slice should record it as one.

    This slice must now decide two things explicitly:

    1. Which protocol version it targets, and which clients it expects to connect.
    2. Whether it includes or defers Client ID Metadata Document support. If it defers that support, say so, so nobody is surprised when a spec-conformant client cannot connect.

    The design document (docs/plans/788-mcp-oauth/02-design.md) now labels DCR as a compatibility choice.

  2. added
    epic-788OAuth 2.1 authorization server for MCP (#788)
    priority:tier3Real product weight, real cost
    size:MA day or two; migration or a multi-state UI
    on Sep 13, 2026
  3. mforce commented on Oct 8, 2026

    @mforce
    OwnerAuthor

    Amendment, 2026-10-08: the expiry sweep also prunes old tokens and authorizations

    The body and the 2026-09-13 correction stay as written. This adds one item to the scope.

    #795 (PR #1135) ships OpenIddict with no cleanup of its tables. Redeemed and expired authorization codes, revoked tokens and revoked authorizations stay in the database indefinitely. Access tokens last until revoked (#788), so a live connection's rows are meant to stay; only dead rows are garbage. The rows grow once #798 turns the server on in Production.

    In this slice:

    • Build the unapproved-application expiry as a recurring sweep under DurableJobWorker's leader gate, the same way RefreshTokenPurgeSweep and IdempotencyRecordPurgeSweep work. At most one instance runs it (Background worker has no single-runner guarantee — double-runs if scaled >1 instance #271).
    • In the same sweep, call OpenIddict's PruneAsync on the token and authorization managers, with a stated retention threshold. OpenIddict's default is 14 days. Pruning deletes only entries that are expired, revoked, redeemed or otherwise invalid, so connected apps are not touched.
    • Do not add OpenIddict's Quartz.NET integration. It would be a second scheduler, outside the leader gate.
    • Tests: a live connection survives a sweep; a revoked token, a redeemed code and an expired unapproved application are removed after the threshold; the sweep runs only on the leader. Each test states its red mutation.

    Done when, in addition to the body's criteria: dead OAuth rows older than the threshold disappear, and live connections remain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiAPI/endpoint layerepic-788OAuth 2.1 authorization server for MCP (#788)priority:tier3Real product weight, real costsize:MA day or two; migration or a multi-state UIsliceThin vertical work item

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions