Trim whitespace around WWW-Authenticate directives (#1377)

RFC 7235 allows optional whitespace around the commas that separate
auth-params, e.g. `Bearer realm="...", service="...", scope="..."`.
The parser kept that whitespace in the keys, so "service" and "scope"
were stored as " service" and " scope" and were never sent to the
token endpoint.
This commit is contained in:
Nicoló Angileri
2026-10-16 19:09:54 -07:00
committed by GitHub
parent ea453cb111
commit 8955b9f946
3 changed files with 27 additions and 5 deletions
+5 -2
View File
@@ -27,8 +27,11 @@ class WWWAuthenticate {
+ "key=value or key=\"value\"")
}
let key = String(parts[0])
var value = String(parts[1])
// Directives can be surrounded by optional whitespace[1]
//
// [1]: https://www.rfc-editor.org/rfc/rfc7235#section-4.1
let key = parts[0].trimmingCharacters(in: .whitespaces)
var value = parts[1].trimmingCharacters(in: .whitespaces)
value = value.trimmingCharacters(in: CharacterSet(charactersIn: "\""))
kvs[key] = value
+8 -3
View File
@@ -20,6 +20,10 @@ final class RegistryRealmTests: XCTestCase {
try await checkAuthentication(mode: "encoded")
}
func testChallengeWithSpacesAfterCommas() async throws {
try await checkAuthentication(mode: "spaced")
}
private func checkAuthentication(mode: String) async throws {
let server = try RealmTokenServer(mode: mode)
defer { server.stop() }
@@ -83,7 +87,7 @@ private final class RealmTokenServer {
if !request.lowercased().contains("authorization: bearer test-token\r\n") {
let query: String
switch self.mode {
case "plain": query = ""
case "plain", "spaced": query = ""
case "encoded": query = "?tenant=alpha%2Bbeta&escape=%7e%2f&mode=one&mode=two&empty="
default: query = "?tenant=alpha%26beta&mode=one&mode=two&empty="
}
@@ -91,7 +95,8 @@ private final class RealmTokenServer {
(self.mode == "encoded" ? "&sc%6Fpe=old&service=old" : "")
let realm = "http://127.0.0.1:\(self.listener.port!.rawValue)/token\(query)\(stale)"
status = "401 Unauthorized"
extraHeaders = "WWW-Authenticate: Bearer realm=\"\(realm)\",service=\"registry\",scope=\"repository:org/repo:pull\"\r\n"
let separator = self.mode == "spaced" ? ", " : ","
extraHeaders = "WWW-Authenticate: Bearer realm=\"\(realm)\"\(separator)service=\"registry\"\(separator)scope=\"repository:org/repo:pull\"\r\n"
}
} else if url.path == "/token" {
let receivedURL = URLComponents(url: url, resolvingAgainstBaseURL: false)!
@@ -102,7 +107,7 @@ private final class RealmTokenServer {
values("tenant") == ["alpha+beta"] && values("escape") == ["~/"]
let normalValid = values("tenant") == ["alpha&beta"]
let valid = values("service") == ["registry"] && values("scope") == ["repository:org/repo:pull"] &&
(self.mode == "plain" || ((self.mode == "encoded" ? encodedValid : normalValid) &&
(self.mode == "plain" || self.mode == "spaced" || ((self.mode == "encoded" ? encodedValid : normalValid) &&
values("mode") == ["one", "two"] && values("empty") == [""]))
status = valid ? "200 OK" : "400 Bad Request"
body = valid ? "{\"token\":\"test-token\"}" : "{\"error\":\"missing or changed realm parameters\"}"
@@ -23,6 +23,20 @@ final class WWWAuthenticateTests: XCTestCase {
XCTAssertEqual(["a": "b", "c": "d"], wwwAuthenticate.kvs)
}
func testWhitespaceAroundDirectives() throws {
// RFC 7235 allows optional whitespace around the commas separating directives[1]
//
// [1]: https://www.rfc-editor.org/rfc/rfc7235#section-4.1
let wwwAuthenticate = try WWWAuthenticate(rawHeaderValue: "Bearer realm=\"https://auth.example.com/token\", service=\"registry.example.com\" , scope=\"repository:org/repo:pull,push\"")
XCTAssertEqual("Bearer", wwwAuthenticate.scheme)
XCTAssertEqual([
"realm": "https://auth.example.com/token",
"service": "registry.example.com",
"scope": "repository:org/repo:pull,push",
], wwwAuthenticate.kvs)
}
func testIncompleteHeader() throws {
XCTAssertThrowsError(try WWWAuthenticate(rawHeaderValue: "Whatever")) {
XCTAssertTrue($0 is RegistryError)