diff --git a/oauthex/resource_meta.go b/oauthex/resource_meta.go index 31d6b37c9..77e68a74a 100644 --- a/oauthex/resource_meta.go +++ b/oauthex/resource_meta.go @@ -198,7 +198,8 @@ 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:]) + // Empty list elements are skipped (RFC 9110, section 5.6.1). + lookahead := strings.TrimLeft(header[i+1:], ", \t") eqPos := strings.Index(lookahead, "=") isParam := false @@ -240,7 +241,8 @@ func parseSingleChallenge(s string) (Challenge, error) { params := make(map[string]string) - // Parse the key-value parameters. + // Parse the key-value parameters, skipping any leading empty list elements. + paramsStr = strings.TrimLeft(paramsStr, ", \t") for paramsStr != "" { // Find the end of the parameter key. keyEnd := strings.Index(paramsStr, "=") @@ -299,9 +301,10 @@ func parseSingleChallenge(s string) (Challenge, error) { // Per RFC 9110, parameter keys are case-insensitive. params[strings.ToLower(key)] = value - // If there is a comma, consume it and continue to the next parameter. + // If there is a comma, consume it, along with any empty list elements + // that follow it, and continue to the next parameter. if strings.HasPrefix(paramsStr, ",") { - paramsStr = strings.TrimSpace(paramsStr[1:]) + paramsStr = strings.TrimLeft(paramsStr, ", \t") } 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) diff --git a/oauthex/resource_meta_test.go b/oauthex/resource_meta_test.go index dcac67407..794f714c2 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="insufficient_scope"`, + want: []string{`Bearer realm="example",, error="insufficient_scope"`}, + }, + { + 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) { } } +// Recipients must accept empty list elements (RFC 9110, section 5.6.1). +// An extra comma must not move error="insufficient_scope" out of the Bearer +// challenge, or the auth handler would not start step-up authorization. +func TestParseWWWAuthenticateEmptyListElements(t *testing.T) { + got, err := ParseWWWAuthenticate([]string{ + `, Basic realm="a",, Bearer , realm="b",, error="insufficient_scope",`, + }) + if err != nil { + t.Fatal(err) + } + want := []Challenge{ + {Scheme: "basic", Params: map[string]string{"realm": "a"}}, + {Scheme: "bearer", Params: map[string]string{ + "realm": "b", + "error": "insufficient_scope", + }}, + } + 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")