Skip to content

feat: add OIDC Dynamic Client Registration (RFC 7591/7592) - #2

Open
jfroy wants to merge 17 commits into
pocket-id:mainfrom
jfroy:mcp
Open

feat: add OIDC Dynamic Client Registration (RFC 7591/7592)#2
jfroy wants to merge 17 commits into
pocket-id:mainfrom
jfroy:mcp

Conversation

@jfroy

@jfroy jfroy commented Aug 7, 2026

Copy link
Copy Markdown

What does this PR do?

This PR supports pocket-id/pocket-id#1672 which adds Dynamic Client Registration (RFC 7591/7592) and OAuth 2.0 Authorization Server Metadata (RFC 8414), with the goal of supporting popular cloud-based MCP client registration for MCP servers using OIDC (either directly or via a proxy like ToolHive).

@jfroy jfroy changed the title feat: OIDC Dynamic Client Registration (RFC 7591/7592) and support feat: add OIDC Dynamic Client Registration (RFC 7591/7592) Aug 7, 2026
@jfroy
jfroy force-pushed the mcp branch 3 times, most recently from 7501083 to 3ea4c21 Compare August 7, 2026 15:47
jfroy added 2 commits August 7, 2026 13:33
An authorization server fetches several client-supplied URLs: a client ID
metadata document, a logo_uri, a JWKS URI. Each needs the same protection, so
export the guard the CIMD fetcher already used instead of leaving every caller
to rebuild it.

SSRFGuardedTransport hardens a transport so requests cannot reach loopback,
private, link-local or other special-use addresses. A pre-flight DNS check alone
is insufficient, because a name can resolve to a public address when it is
validated and an internal one when the connection is made. The dialer's Control
hook runs after resolution on the concrete address, closing that window on the
initial request and on every redirect hop.

It also clears three settings that would otherwise defeat the check:

- Proxy, because the dialer would connect to the proxy while the proxy reaches
  the real destination, so only the proxy's address would ever be inspected
- DialTLS and DialTLSContext, because they bypass DialContext entirely

Because the guard works by owning the dialer, it can only be applied to an
*http.Transport. A base of any other type, such as an instrumentation wrapper,
hides the transport that actually dials and cannot be guarded. Returning it
unchanged would hand back something that looks guarded and is not, so the guard
fails closed and derives a fresh transport instead; wrappers belong around the
result rather than underneath it. A round tripper that genuinely never dials,
such as an in-memory test double, opts out explicitly via SSRFGuardExempt.

The CIMD fetcher now uses the shared implementation, so its behaviour is
unchanged and there is a single place to harden.
Add the pieces of dynamic client registration that are defined entirely by the
spec, so an authorization server implementing RFC 7591 does not have to
redeclare them.

Message types: ClientRegistrationRequest and ClientRegistrationResponse model
the client information request and response (RFC 7591 sections 2 and 3.2.1),
which are also the bodies of an RFC 7592 update. These mirror the existing
ClientMetadataDocument, whose members are nearly identical because a client ID
metadata document carries the same metadata a registration request does. Only
the members Fosite has an opinion about are modelled; per section 3.1 a server
must ignore metadata it does not understand rather than reject the request.

Validation: ValidateRegistrationRedirectURIs enforces the redirect_uri rules on
a registration request. Because the request comes from an untrusted caller,
these hold regardless of any deployment-specific allowlist:

- at least one redirect_uri must be present
- each must be an absolute URI with no fragment (RFC 6749 section 3.1.2)
- each must not use an active-content scheme

IsActiveContentRedirectURI rejects javascript, vbscript, data, blob, file, about
and filesystem. Redirecting to one of these executes script in the browsing
context performing the redirect, or exposes attacker-controlled inline or local
content, so it is an XSS or local-file-disclosure primitive rather than a real
callback. This matters most for dynamic registration, where an operator
allowlist may legitimately contain a scheme wildcard and would otherwise match
them. Private-use schemes stay valid so native apps following RFC 8252 can
register.

Failures use the RFC 7591 invalid_redirect_uri error code, which Fosite did not
previously define.

client_secret_expires_at is modelled as a pointer. RFC 7591 section 3.2.1
requires it whenever a client_secret is issued, and 0 (the secret never expires)
is exactly the value a plain omitempty int would drop from the response.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants