Skip to content
This repository was archived by the owner on Apr 29, 2026. It is now read-only.

Commit da47e79

Browse files
salmon-21claude
andcommitted
fix: address review feedback for auth server URL rewriting
- Handle url.Parse error in authorization redirect instead of ignoring - Move rewriteMetadataURLs inside authorizationProxyEnabled guard so URLs are only rewritten when the proxy routes exist - Restrict metadata URL rewriting to known endpoint fields (allowlist) to avoid rewriting issuer which would break token validation - Use URL parsing for scheme/host comparison instead of string prefix matching to prevent false matches on similar hostnames - Use url.JoinPath for JWKS URI construction and return errors for unparseable URIs instead of silently falling back - Validate paths start with "/" before registering with ServeMux to prevent panics on relative URIs - Add test case for relative URI rejection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0286cbf commit da47e79

4 files changed

Lines changed: 76 additions & 16 deletions

File tree

oauth/authorization.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ func NewAuthorizationHandler(config *config.Config, meta map[string]any) (http.H
2626
return nil, fmt.Errorf("could not parse authorization endpoint: %w", err)
2727
} else {
2828
// Rewrite the authorization endpoint to use the public host URL
29-
publicURL, _ := url.Parse(config.Host.String())
29+
publicURL, err := url.Parse(config.Host.String())
30+
if err != nil {
31+
return nil, fmt.Errorf("could not parse public host URL: %w", err)
32+
}
3033
publicURL.Path = authEndpointURL.Path
3134

3235
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

oauth/authorization_server_metadata.go

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ func NewAuthorizationServerMetadataHandler(config *config.Config) http.Handler {
4747
metadata["authorization_endpoint"] = authorizationURI.String()
4848
log.Get(r.Context()).Info("Adding authorization endpoint to authorization server metadata",
4949
"url", metadata["authorization_endpoint"])
50-
}
5150

52-
// Rewrite all URL fields pointing to internal auth server to public host
53-
rewriteMetadataURLs(metadata, config.Authorization.Server, config.Host.String())
51+
// Rewrite endpoint URL fields pointing to internal auth server
52+
// to public host so external clients can reach them through the
53+
// gateway's reverse proxy.
54+
rewriteMetadataURLs(metadata, config.Authorization.Server, config.Host.String())
55+
}
5456

5557
w.Header().Set("Content-Type", "application/json")
5658
if err := json.NewEncoder(w).Encode(metadata); err != nil {
@@ -97,14 +99,48 @@ func GetMedatata(server string) (map[string]any, error) {
9799
return nil, err
98100
}
99101

100-
// rewriteMetadataURLs rewrites all string values in metadata that start with
101-
// the internal auth server URL to use the public host URL instead.
102+
// endpointURLFields lists the metadata fields that contain endpoint URLs
103+
// eligible for rewriting. Fields like "issuer" are intentionally excluded
104+
// because rewriting them would make the metadata inconsistent with the "iss"
105+
// claim in tokens issued by the auth server.
106+
var endpointURLFields = []string{
107+
"authorization_endpoint",
108+
"token_endpoint",
109+
"jwks_uri",
110+
"userinfo_endpoint",
111+
"registration_endpoint",
112+
"introspection_endpoint",
113+
"revocation_endpoint",
114+
"device_authorization_endpoint",
115+
"end_session_endpoint",
116+
}
117+
118+
// rewriteMetadataURLs rewrites known endpoint URL fields in metadata from the
119+
// internal auth server URL to the public host URL. Only fields whose
120+
// scheme and host match the internal server are rewritten.
102121
func rewriteMetadataURLs(metadata map[string]any, internalServer string, publicHost string) {
103-
internal := strings.TrimRight(internalServer, "/")
104-
public := strings.TrimRight(publicHost, "/")
105-
for key, value := range metadata {
106-
if s, ok := value.(string); ok && strings.HasPrefix(s, internal) {
107-
metadata[key] = strings.Replace(s, internal, public, 1)
122+
internalURL, err := url.Parse(strings.TrimRight(internalServer, "/"))
123+
if err != nil || internalURL.Scheme == "" || internalURL.Host == "" {
124+
return
125+
}
126+
publicURL, err := url.Parse(strings.TrimRight(publicHost, "/"))
127+
if err != nil || publicURL.Scheme == "" || publicURL.Host == "" {
128+
return
129+
}
130+
131+
for _, key := range endpointURLFields {
132+
s, ok := metadata[key].(string)
133+
if !ok {
134+
continue
135+
}
136+
u, err := url.Parse(s)
137+
if err != nil || u.Scheme == "" || u.Host == "" {
138+
continue
139+
}
140+
if u.Scheme == internalURL.Scheme && u.Host == internalURL.Host {
141+
u.Scheme = publicURL.Scheme
142+
u.Host = publicURL.Host
143+
metadata[key] = u.String()
108144
}
109145
}
110146
}

oauth/oauth.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,21 @@ func NewManager(ctx context.Context, config *config.Config) (*Manager, error) {
5555
// circular dependency when the auth server's issuer is set to the
5656
// public gateway URL (the metadata would advertise the gateway's own
5757
// URL as jwks_uri, causing the gateway to fetch from itself).
58-
internalJWKSURI := strings.TrimRight(config.Authorization.Server, "/") + "/keys"
59-
if u, err := url.Parse(jwksURIRaw); err == nil {
60-
internalJWKSURI = strings.TrimRight(config.Authorization.Server, "/") + u.Path
58+
baseURL, err := url.Parse(config.Authorization.Server)
59+
if err != nil {
60+
return nil, fmt.Errorf("invalid authorization server URL %q: %w", config.Authorization.Server, err)
61+
}
62+
jwksURL, err := url.Parse(jwksURIRaw)
63+
if err != nil {
64+
return nil, fmt.Errorf("invalid jwks_uri %q: %w", jwksURIRaw, err)
65+
}
66+
jwksPath := jwksURL.Path
67+
if jwksPath == "" || jwksPath == "/" {
68+
jwksPath = "/keys"
69+
}
70+
internalJWKSURI, err := url.JoinPath(baseURL.String(), jwksPath)
71+
if err != nil {
72+
return nil, fmt.Errorf("failed to construct internal JWKS URI: %w", err)
6173
}
6274

6375
if err := cache.Register(timeoutCtx, internalJWKSURI,
@@ -176,7 +188,7 @@ func authServerProxyPaths(meta map[string]any) []string {
176188

177189
for _, key := range endpointKeys {
178190
if s, ok := meta[key].(string); ok {
179-
if u, err := url.Parse(s); err == nil {
191+
if u, err := url.Parse(s); err == nil && strings.HasPrefix(u.Path, "/") {
180192
add(u.Path)
181193
}
182194
}
@@ -186,7 +198,7 @@ func authServerProxyPaths(meta map[string]any) []string {
186198
// prefix pattern so that connector-specific sub-paths (e.g.
187199
// /auth/github) are also proxied.
188200
if s, ok := meta["authorization_endpoint"].(string); ok {
189-
if u, err := url.Parse(s); err == nil {
201+
if u, err := url.Parse(s); err == nil && strings.HasPrefix(u.Path, "/") {
190202
add(u.Path)
191203
add(strings.TrimRight(u.Path, "/") + "/")
192204
}

oauth/oauth_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ func TestAuthServerProxyPaths(t *testing.T) {
6060
},
6161
contains: []string{"/keys", "/callback", "/approval"},
6262
},
63+
{
64+
name: "skips relative URIs that would panic ServeMux",
65+
meta: map[string]any{
66+
"token_endpoint": "token", // relative, no leading /
67+
"jwks_uri": "https://example.com/keys",
68+
},
69+
contains: []string{"/keys", "/callback", "/approval"},
70+
excludes: []string{"token"},
71+
},
6372
{
6473
name: "registers authorization endpoint prefix for sub-paths",
6574
meta: map[string]any{

0 commit comments

Comments
 (0)