diff --git a/oauthex/resource_meta.go b/oauthex/resource_meta.go index 2869c82f..b71371f4 100644 --- a/oauthex/resource_meta.go +++ b/oauthex/resource_meta.go @@ -110,6 +110,7 @@ type Challenge struct { Scheme string // Params is a map of authentication parameters. // Keys are case-insensitive. Parsed keys are always lower-case. + // A challenge that carries a token68 instead of parameters has no Params. Params map[string]string } @@ -255,6 +256,11 @@ func parseSingleChallenge(s string) (Challenge, error) { if !found { return c, nil } + if isToken68(strings.TrimSpace(paramsStr)) { + // The challenge carries a token68 (for example, Negotiate with a + // SPNEGO token) rather than auth-params. It has no parameters. + return c, nil + } params := make(map[string]string) paramsStr = trimEmptyListElements(paramsStr) @@ -339,3 +345,22 @@ func trimEmptyListElements(s string) string { return r == ',' || unicode.IsSpace(r) }) } + +// isToken68 reports whether s is a token68 (RFC 9110, section 11.2): +// +// token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=" +func isToken68(s string) bool { + body := strings.TrimRight(s, "=") + if body == "" { + return false + } + for _, r := range body { + switch { + case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z', '0' <= r && r <= '9': + case strings.ContainsRune("-._~+/", r): + default: + return false + } + } + return true +} diff --git a/oauthex/resource_meta_test.go b/oauthex/resource_meta_test.go index c6678006..d50626fa 100644 --- a/oauthex/resource_meta_test.go +++ b/oauthex/resource_meta_test.go @@ -198,6 +198,27 @@ func TestParseWWWAuthenticateEmptyListElements(t *testing.T) { } } +// A challenge may carry a token68 instead of auth-params (RFC 9110, +// section 11.6.1). It must not stop the Bearer challenge next to it from +// being parsed. +func TestParseWWWAuthenticateToken68(t *testing.T) { + got, err := ParseWWWAuthenticate([]string{ + `Negotiate oYH1MIHyoAMKAQ, Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"`, + }) + if err != nil { + t.Fatal(err) + } + want := []Challenge{ + {Scheme: "negotiate"}, + {Scheme: "bearer", Params: map[string]string{ + "resource_metadata": "https://example.com/.well-known/oauth-protected-resource", + }}, + } + 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") @@ -300,8 +321,18 @@ func TestParseSingleChallenge(t *testing.T) { wantErr: false, }, { - name: "malformed param - no value", - input: "Bearer realm=", + name: "token68", + input: "Negotiate oYH1MIHyoAMKAQ", + want: Challenge{Scheme: "negotiate"}, + }, + { + name: "token68 with padding", + input: "Basic dGVzdA==", + want: Challenge{Scheme: "basic"}, + }, + { + name: "malformed token68", + input: "Negotiate abc def", wantErr: true, }, {