Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 61 additions & 14 deletions internal/oauth/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,25 +278,72 @@ func discoverAuthServerURL(env Env, serverURL string, timeout time.Duration, fet

// ExtractResourceMetadataURL parses WWW-Authenticate header to extract resource_metadata URL
// Format: Bearer error="invalid_request", resource_metadata="https://..."
//
// Parameters are scanned per RFC 7235 section 2.1: names are case-insensitive,
// whitespace may surround "=", and quoted values of other parameters are
// skipped rather than searched, so only a real resource_metadata parameter
// with a quoted-string value is returned.
func ExtractResourceMetadataURL(wwwAuthHeader string) string {
// Look for resource_metadata parameter
if !strings.Contains(wwwAuthHeader, "resource_metadata") {
return ""
h := wwwAuthHeader
n := len(h)
skipWS := func(i int) int {
for i < n && (h[i] == ' ' || h[i] == '\t') {
i++
}
return i
}

// Split on resource_metadata=" to find the URL
parts := strings.Split(wwwAuthHeader, "resource_metadata=\"")
if len(parts) < 2 {
return ""
i := 0
for i < n {
for i < n && (h[i] == ' ' || h[i] == '\t' || h[i] == ',') {
i++
}
start := i
for i < n && isAuthTokenChar(h[i]) {
i++
}
name := h[start:i]
if name == "" {
i++
continue
}
j := skipWS(i)
if j >= n || h[j] != '=' {
continue
}
j = skipWS(j + 1)
if j < n && h[j] == '"' {
var b strings.Builder
k := j + 1
for ; k < n && h[k] != '"'; k++ {
if h[k] == '\\' && k+1 < n {
k++
}
b.WriteByte(h[k])
}
if k >= n {
return ""
}
if strings.EqualFold(name, "resource_metadata") {
return b.String()
}
i = k + 1
continue
}
for j < n && h[j] != ',' && h[j] != ' ' && h[j] != '\t' {
j++
}
i = j
}
return ""
}

// Find the closing quote
endIdx := strings.Index(parts[1], "\"")
if endIdx == -1 {
return ""
// isAuthTokenChar reports whether c is an RFC 7230 tchar.
func isAuthTokenChar(c byte) bool {
switch {
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9':
return true
}

return parts[1][:endIdx]
return strings.IndexByte("!#$%&'*+-.^_`|~", c) >= 0
}

// DiscoverProtectedResourceMetadata fetches RFC 9728 Protected Resource Metadata
Expand Down
25 changes: 25 additions & 0 deletions internal/oauth/resource_metadata_param_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package oauth

import "testing"

func TestExtractResourceMetadataURL_RFC7235Params(t *testing.T) {
const want = "https://api.example.com/.well-known/oauth-protected-resource"
tests := []struct {
name string
header string
want string
}{
{"case-insensitive name", `Bearer Resource_Metadata="` + want + `"`, want},
{"whitespace around equals", `Bearer error="invalid_token", resource_metadata = "` + want + `"`, want},
{"other param with similar suffix", `Bearer x_resource_metadata="https://wrong.example", resource_metadata="` + want + `"`, want},
{"mention inside another quoted value", `Bearer error_description="see resource_metadata=\"https://wrong.example\"", resource_metadata="` + want + `"`, want},
{"only a lookalike param", `Bearer x_resource_metadata="https://wrong.example"`, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ExtractResourceMetadataURL(tt.header); got != tt.want {
t.Errorf("ExtractResourceMetadataURL(%q) = %q, want %q", tt.header, got, tt.want)
}
})
}
}
Loading