From e9876f5c4bf609c003c522fbd040551ed721c7cf Mon Sep 17 00:00:00 2001 From: Akshita <110122283+akshita317@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:51:01 +0530 Subject: [PATCH] oauthex: accept token68 challenges in WWW-Authenticate A challenge carries either auth-params or a token68 (RFC 9110, section 11.6.1), as in WWW-Authenticate: Negotiate oYH1MIHyoAMKAQ, Bearer resource_metadata="..." parseSingleChallenge only understood auth-params. A token68 without padding failed with "expected key=value", which made ParseWWWAuthenticate fail the whole header, so the auth handlers never saw the Bearer challenge next to it. A token68 with padding, such as dGVzdA==, was misread as a parameter named "dgvzda". Recognize a token68 and return the challenge without parameters. The "Bearer realm=" test case is removed: "realm=" is a valid token68, and a parameter with no value is still covered by the case "malformed param - no value before comma". --- oauthex/resource_meta.go | 25 +++++++++++++++++++++++++ oauthex/resource_meta_test.go | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/oauthex/resource_meta.go b/oauthex/resource_meta.go index 880182bc2..6ba7e7426 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 } @@ -237,6 +238,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) @@ -321,3 +327,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 c262bffc7..332a5b9ba 100644 --- a/oauthex/resource_meta_test.go +++ b/oauthex/resource_meta_test.go @@ -149,6 +149,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") @@ -251,8 +272,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, }, {