diff --git a/Sources/tart/OCI/WWWAuthenticate.swift b/Sources/tart/OCI/WWWAuthenticate.swift index ac5d5c2..a0eed77 100644 --- a/Sources/tart/OCI/WWWAuthenticate.swift +++ b/Sources/tart/OCI/WWWAuthenticate.swift @@ -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 diff --git a/Tests/TartTests/RegistryRealmTests.swift b/Tests/TartTests/RegistryRealmTests.swift index d28529b..ee3a6ec 100644 --- a/Tests/TartTests/RegistryRealmTests.swift +++ b/Tests/TartTests/RegistryRealmTests.swift @@ -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\"}" diff --git a/Tests/TartTests/WWWAuthenticateTests.swift b/Tests/TartTests/WWWAuthenticateTests.swift index 0857cda..d34dc79 100644 --- a/Tests/TartTests/WWWAuthenticateTests.swift +++ b/Tests/TartTests/WWWAuthenticateTests.swift @@ -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)