Skip to content

Commit 6d30217

Browse files
committed
internal/oauthex: fix GetProtectedResourceMetadataFromHeader validation
GetProtectedResourceMetadataFromHeader was validating the resource field against the metadata endpoint URL instead of the original server URL. Per RFC 9728 section 3.3, the resource field must match the URL that the client used to make the request to the resource server. Fixes: #560 This fix adds a serverURL parameter and validates against the correct value.
1 parent 547b5c1 commit 6d30217

File tree

2 files changed

+62
-13
lines changed

2 files changed

+62
-13
lines changed

internal/oauthex/oauth2_test.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,15 @@ func TestGetProtectedResourceMetadata(t *testing.T) {
210210
server := httptest.NewTLSServer(h)
211211
h.installHandlers(server.URL)
212212
client := server.Client()
213-
res, err := client.Get(server.URL + "/resource")
213+
serverURL := server.URL + "/resource"
214+
res, err := client.Get(serverURL)
214215
if err != nil {
215216
t.Fatal(err)
216217
}
217218
if res.StatusCode != http.StatusUnauthorized {
218219
t.Fatal("want unauth")
219220
}
220-
prm, err := GetProtectedResourceMetadataFromHeader(ctx, res.Header, client)
221+
prm, err := GetProtectedResourceMetadataFromHeader(ctx, serverURL, res.Header, client)
221222
if err != nil {
222223
t.Fatal(err)
223224
}
@@ -238,11 +239,53 @@ func TestGetProtectedResourceMetadata(t *testing.T) {
238239
t.Fatal("nil prm")
239240
}
240241
})
242+
// Test that metadata URL and resource identifier are properly distinguished (issue #560)
243+
t.Run("FromHeaderValidatesAgainstServerURL", func(t *testing.T) {
244+
h := &fakeResourceHandler{serveWWWAuthenticate: true}
245+
server := httptest.NewTLSServer(h)
246+
h.installHandlers(server.URL)
247+
client := server.Client()
248+
serverURL := server.URL + "/resource"
249+
res, err := client.Get(serverURL)
250+
if err != nil {
251+
t.Fatal(err)
252+
}
253+
// This should succeed because we validate against serverURL, not metadataURL
254+
prm, err := GetProtectedResourceMetadataFromHeader(ctx, serverURL, res.Header, client)
255+
if err != nil {
256+
t.Fatalf("Expected validation to succeed, got error: %v", err)
257+
}
258+
if prm == nil {
259+
t.Fatal("Expected non-nil prm")
260+
}
261+
if prm.Resource != serverURL {
262+
t.Errorf("Expected resource %q, got %q", serverURL, prm.Resource)
263+
}
264+
})
265+
t.Run("FromHeaderRejectsImpersonation", func(t *testing.T) {
266+
h := &fakeResourceHandler{serveWWWAuthenticate: true, resourceOverride: "https://attacker.com/evil"}
267+
server := httptest.NewTLSServer(h)
268+
h.installHandlers(server.URL)
269+
client := server.Client()
270+
serverURL := server.URL + "/resource"
271+
res, err := client.Get(serverURL)
272+
if err != nil {
273+
t.Fatal(err)
274+
}
275+
prm, err := GetProtectedResourceMetadataFromHeader(ctx, serverURL, res.Header, client)
276+
if err == nil {
277+
t.Fatal("Expected validation error for mismatched resource, got nil")
278+
}
279+
if prm != nil {
280+
t.Fatal("Expected nil prm on validation failure")
281+
}
282+
})
241283
}
242284

243285
type fakeResourceHandler struct {
244286
http.ServeMux
245287
serveWWWAuthenticate bool
288+
resourceOverride string // If set, use this instead of correct resource (for testing validation)
246289
}
247290

248291
func (h *fakeResourceHandler) installHandlers(serverURL string) {
@@ -256,11 +299,16 @@ func (h *fakeResourceHandler) installHandlers(serverURL string) {
256299
}))
257300
h.Handle("GET "+path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
258301
w.Header().Set("Content-Type", "application/json")
259-
// If there is a WWW-Authenticate header, the resource field is the value of that header.
260-
// If not, it's the server URL without the "/.well-known/..." part.
302+
// Per RFC 9728 section 3.3, the resource field should contain the actual resource identifier,
303+
// which is the URL the client uses to access the resource (serverURL + "/resource" for WWW-Authenticate case).
304+
// For the FromID test case, it's just the serverURL.
261305
resource := serverURL
262306
if h.serveWWWAuthenticate {
263-
resource = url
307+
resource = serverURL + "/resource"
308+
}
309+
// Allow testing with custom resource values (e.g., impersonation attacks)
310+
if h.resourceOverride != "" {
311+
resource = h.resourceOverride
264312
}
265313
prm := &ProtectedResourceMetadata{Resource: resource}
266314
if err := json.NewEncoder(w).Encode(prm); err != nil {

internal/oauthex/resource_meta.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,11 @@ func GetProtectedResourceMetadataFromID(ctx context.Context, resourceID string,
137137
// GetProtectedResourceMetadataFromHeader retrieves protected resource metadata
138138
// using information in the given header, using the given client (or the default
139139
// client if nil).
140-
// It issues a GET request to a URL discovered by parsing the WWW-Authenticate headers in the given request,
141-
// It then validates the resource field of the resulting metadata against the given URL.
142-
// If there is no URL in the request, it returns nil, nil.
143-
func GetProtectedResourceMetadataFromHeader(ctx context.Context, header http.Header, c *http.Client) (_ *ProtectedResourceMetadata, err error) {
140+
// It issues a GET request to a URL discovered by parsing the WWW-Authenticate headers in the given request.
141+
// Per RFC 9728 section 3.3, it validates that the resource field of the resulting metadata
142+
// matches the serverURL (the URL that the client used to make the original request to the resource server).
143+
// If there is no metadata URL in the header, it returns nil, nil.
144+
func GetProtectedResourceMetadataFromHeader(ctx context.Context, serverURL string, header http.Header, c *http.Client) (_ *ProtectedResourceMetadata, err error) {
144145
defer util.Wrapf(&err, "GetProtectedResourceMetadataFromHeader")
145146
headers := header[http.CanonicalHeaderKey("WWW-Authenticate")]
146147
if len(headers) == 0 {
@@ -150,11 +151,11 @@ func GetProtectedResourceMetadataFromHeader(ctx context.Context, header http.Hea
150151
if err != nil {
151152
return nil, err
152153
}
153-
url := ResourceMetadataURL(cs)
154-
if url == "" {
154+
metadataURL := ResourceMetadataURL(cs)
155+
if metadataURL == "" {
155156
return nil, nil
156157
}
157-
return getPRM(ctx, url, c, url)
158+
return getPRM(ctx, metadataURL, c, serverURL)
158159
}
159160

160161
// getPRM makes a GET request to the given URL, and validates the response.
@@ -167,7 +168,7 @@ func getPRM(ctx context.Context, purl string, c *http.Client, wantResource strin
167168
if err != nil {
168169
return nil, err
169170
}
170-
// Validate the Resource field to thwart impersonation attacks (section 3.3).
171+
// Validate the Resource field (see RFC 9728, section 3.3) as well as issue #560
171172
if prm.Resource != wantResource {
172173
return nil, fmt.Errorf("got metadata resource %q, want %q", prm.Resource, wantResource)
173174
}

0 commit comments

Comments
 (0)