From 19cc372c7279dd950ca05615122aba7789ea3a4a Mon Sep 17 00:00:00 2001 From: Akshita kumari <110122283+akshita317@users.noreply.github.com> Date: Wed, 7 Oct 2026 21:10:12 +0530 Subject: [PATCH] oauthex: accept empty list elements in WWW-Authenticate RFC 9110, section 5.6.1.2, requires a recipient to accept empty list elements, such as the extra comma in "a=1,, b=2". ParseWWWAuthenticate did not: after a second comma, splitChallenges saw ", b=2" as the start of a new challenge, so Bearer realm="x",, resource_metadata="https://..." parsed as a Bearer challenge without resource_metadata plus a challenge with scheme ",". An empty element right after the scheme ("Bearer ,realm=x") produced a parameter named ",realm". Skip empty list elements both where splitChallenges looks ahead after a comma and where parseSingleChallenge consumes one. Co-Authored-By: Claude Opus 5.5 --- oauthex/resource_meta.go | 14 ++++++++++-- oauthex/resource_meta_test.go | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/oauthex/resource_meta.go b/oauthex/resource_meta.go index 31d6b37c9..880182bc2 100644 --- a/oauthex/resource_meta.go +++ b/oauthex/resource_meta.go @@ -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 @@ -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 != "" { @@ -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) @@ -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) + }) +} diff --git a/oauthex/resource_meta_test.go b/oauthex/resource_meta_test.go index dcac67407..c262bffc7 100644 --- a/oauthex/resource_meta_test.go +++ b/oauthex/resource_meta_test.go @@ -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: "", @@ -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") @@ -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=",