Proxy RFC 7591 Dynamic Client Registration to the OIDC issuer
MCP clients (confirmed against Claude Code) exhibit the same origin assumption this proxy's Protected Resource Metadata (RFC 9728, #3488) already works around for /authorize and /token: even after correctly discovering the real authorization server, they still POST their Dynamic Client Registration (RFC 7591) request to this proxy's own /register instead of the issuer's real advertised registration_endpoint. Adds a /register route at the host root (same placement rationale as protectedResourceMetadataPath - MCP clients build this URL themselves, without knowledge of ProxyPrefix) that discovers the issuer's real registration_endpoint from its .well-known/openid-configuration document and proxies the request there, relaying the response verbatim - including the Authorization header, so Initial-Access-Token- gated registration works too, not just anonymous. Depends on the oidcIssuerURL field added in #3488. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Mike Costas <mike.costas@gmail.com>
This commit is contained in:
parent
4906620241
commit
b026a6eba8
|
|
@ -1,12 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
|
@ -48,6 +50,7 @@ const (
|
|||
|
||||
robotsPath = "/robots.txt"
|
||||
protectedResourceMetadataPath = "/.well-known/oauth-protected-resource"
|
||||
dynamicClientRegistrationPath = "/register"
|
||||
signInPath = "/sign_in"
|
||||
signOutPath = "/sign_out"
|
||||
oauthStartPath = "/start"
|
||||
|
|
@ -339,6 +342,14 @@ func (p *OAuthProxy) buildServeMux(proxyPrefix string) {
|
|||
// ProxyPrefix. See ProtectedResourceMetadata.
|
||||
r.Path(protectedResourceMetadataPath).HandlerFunc(p.ProtectedResourceMetadata)
|
||||
|
||||
// Also host root, not under proxyPrefix, for the same reason - MCP
|
||||
// clients (confirmed against Claude Code) request RFC 7591 Dynamic
|
||||
// Client Registration at <this proxy's own origin>/register rather
|
||||
// than the authorization server's real advertised
|
||||
// registration_endpoint, even after correctly resolving that server
|
||||
// via RFC 9728/8414 discovery. See DynamicClientRegistration.
|
||||
r.Path(dynamicClientRegistrationPath).HandlerFunc(p.DynamicClientRegistration)
|
||||
|
||||
// The authonly path should be registered separately to prevent it from getting no-cache headers.
|
||||
// We do this to allow users to have a short cache (via nginx) of the response to reduce the
|
||||
// likelihood of multiple requests trying to refresh sessions simultaneously.
|
||||
|
|
@ -1401,6 +1412,78 @@ func (p *OAuthProxy) ProtectedResourceMetadata(rw http.ResponseWriter, req *http
|
|||
rw.Write(body)
|
||||
}
|
||||
|
||||
// DynamicClientRegistration proxies RFC 7591 Dynamic Client Registration
|
||||
// requests to the configured OIDC issuer's own registration_endpoint
|
||||
// (discovered from its .well-known/openid-configuration document) and
|
||||
// relays the response verbatim, including its status code.
|
||||
//
|
||||
// MCP clients need this because of the same origin-assumption behavior
|
||||
// ProtectedResourceMetadata's WWW-Authenticate header is meant to correct
|
||||
// for authorization/token requests: confirmed against Claude Code, once it
|
||||
// receives Protected Resource Metadata pointing at an external
|
||||
// authorization server, it correctly directs the user's browser to that
|
||||
// server's own /authorize and posts to its own /token - but it still POSTs
|
||||
// its DCR request to THIS proxy's own /register instead of the issuer's
|
||||
// real advertised registration_endpoint. Forwarding the Authorization
|
||||
// header lets Initial-Access-Token-gated ("authenticated" RFC 7591)
|
||||
// registration work too, not just anonymous.
|
||||
func (p *OAuthProxy) DynamicClientRegistration(rw http.ResponseWriter, req *http.Request) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
discoveryResp, err := client.Get(strings.TrimSuffix(p.oidcIssuerURL, "/") + "/.well-known/openid-configuration")
|
||||
if err != nil {
|
||||
logger.Errorf("error fetching issuer discovery document for dynamic client registration: %v", err)
|
||||
http.Error(rw, "failed to reach issuer", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer discoveryResp.Body.Close()
|
||||
|
||||
var discovery struct {
|
||||
RegistrationEndpoint string `json:"registration_endpoint"`
|
||||
}
|
||||
if err := json.NewDecoder(discoveryResp.Body).Decode(&discovery); err != nil || discovery.RegistrationEndpoint == "" {
|
||||
logger.Errorf("issuer discovery document has no registration_endpoint: %v", err)
|
||||
http.Error(rw, "issuer does not support dynamic client registration", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
http.Error(rw, "invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
upstreamReq, err := http.NewRequest(http.MethodPost, discovery.RegistrationEndpoint, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
logger.Errorf("error building dynamic client registration request: %v", err)
|
||||
http.Error(rw, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
upstreamReq.Header.Set("Content-Type", applicationJSON)
|
||||
if auth := req.Header.Get("Authorization"); auth != "" {
|
||||
upstreamReq.Header.Set("Authorization", auth)
|
||||
}
|
||||
|
||||
upstreamResp, err := client.Do(upstreamReq)
|
||||
if err != nil {
|
||||
logger.Errorf("error forwarding dynamic client registration request: %v", err)
|
||||
http.Error(rw, "failed to reach issuer", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer upstreamResp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(upstreamResp.Body)
|
||||
if err != nil {
|
||||
logger.Errorf("error reading dynamic client registration response: %v", err)
|
||||
http.Error(rw, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", applicationJSON)
|
||||
rw.WriteHeader(upstreamResp.StatusCode)
|
||||
rw.Write(respBody)
|
||||
}
|
||||
|
||||
// LoggingCSRFCookiesInOAuthCallback Log all CSRF cookies found in HTTP request OAuth callback,
|
||||
// which were successfully parsed
|
||||
func LoggingCSRFCookiesInOAuthCallback(req *http.Request, cookieName string) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue