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
14 changes: 12 additions & 2 deletions oauthex/resource_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func splitChallenges(header string) ([]string, error) {
// This is a potential challenge separator.
// A new challenge does not start with `key=value`.
// We check if the part after the comma looks like a parameter.
lookahead := strings.TrimSpace(header[i+1:])
lookahead := trimEmptyListElements(header[i+1:])
eqPos := strings.Index(lookahead, "=")

isParam := false
Expand Down Expand Up @@ -239,6 +239,7 @@ func parseSingleChallenge(s string) (Challenge, error) {
}

params := make(map[string]string)
paramsStr = trimEmptyListElements(paramsStr)

// Parse the key-value parameters.
for paramsStr != "" {
Expand Down Expand Up @@ -301,7 +302,7 @@ func parseSingleChallenge(s string) (Challenge, error) {

// If there is a comma, consume it and continue to the next parameter.
if strings.HasPrefix(paramsStr, ",") {
paramsStr = strings.TrimSpace(paramsStr[1:])
paramsStr = trimEmptyListElements(paramsStr[1:])
} else if paramsStr != "" {
// If there's content but it's not a new parameter, the format is wrong.
return Challenge{}, fmt.Errorf("malformed auth parameter: expected comma after value, but got %q", paramsStr)
Expand All @@ -311,3 +312,12 @@ func parseSingleChallenge(s string) (Challenge, error) {
// Per RFC 9110, the scheme is case-insensitive.
return Challenge{Scheme: strings.ToLower(scheme), Params: params}, nil
}

// trimEmptyListElements removes leading whitespace and any empty list
// elements, such as the extra comma in "a=1,, b=2". RFC 9110, section 5.6.1.2,
// requires recipients to accept them.
func trimEmptyListElements(s string) string {
return strings.TrimLeftFunc(s, func(r rune) bool {
return r == ',' || unicode.IsSpace(r)
})
}
41 changes: 41 additions & 0 deletions oauthex/resource_meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ func TestSplitChallenges(t *testing.T) {
input: `Basic realm="C:\\", Bearer error="insufficient_scope"`,
want: []string{`Basic realm="C:\\"`, ` Bearer error="insufficient_scope"`},
},
{
name: "empty list element between params",
input: `Bearer realm="example",, error="invalid_token"`,
want: []string{`Bearer realm="example",, error="invalid_token"`},
},
{
name: "empty list element between challenges",
input: `Basic, , Bearer realm="example"`,
want: []string{`Basic`, ` `, ` Bearer realm="example"`},
},
{
name: "empty input",
input: "",
Expand Down Expand Up @@ -117,6 +127,28 @@ func TestParseWWWAuthenticateEmptyQuotedValue(t *testing.T) {
}
}

// RFC 9110, section 5.6.1.2, requires recipients to accept empty list
// elements. An extra comma must not move the parameters after it into a
// separate challenge, or the auth handlers never see resource_metadata.
func TestParseWWWAuthenticateEmptyListElements(t *testing.T) {
got, err := ParseWWWAuthenticate([]string{
`Bearer , realm="example",, resource_metadata="https://example.com/.well-known/oauth-protected-resource" ,, Basic realm="other",`,
})
if err != nil {
t.Fatal(err)
}
want := []Challenge{
{Scheme: "bearer", Params: map[string]string{
"realm": "example",
"resource_metadata": "https://example.com/.well-known/oauth-protected-resource",
}},
{Scheme: "basic", Params: map[string]string{"realm": "other"}},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("ParseWWWAuthenticate() = %+v, want %+v", got, want)
}
}

func TestSplitChallengesError(t *testing.T) {
if _, err := splitChallenges(`"Bearer"`); err == nil {
t.Fatal("got nil, want error")
Expand Down Expand Up @@ -209,6 +241,15 @@ func TestParseSingleChallenge(t *testing.T) {
},
wantErr: false,
},
{
name: "empty list elements around params",
input: `Bearer , realm="example",, error=invalid_token, ,`,
want: Challenge{
Scheme: "bearer",
Params: map[string]string{"realm": "example", "error": "invalid_token"},
},
wantErr: false,
},
{
name: "malformed param - no value",
input: "Bearer realm=",
Expand Down
Loading