From 5835a86b3bc096f874ec75989ab4917770086cc5 Mon Sep 17 00:00:00 2001 From: kiwigitops Date: Mon, 15 Jun 2026 04:32:54 -0400 Subject: [PATCH] Fix basic auth registry failure reason --- .../Client/RegistryClient+Token.swift | 19 +++++++++++++++++++ .../Client/RegistryClient.swift | 6 +++++- .../AuthChallengeTests.swift | 16 ++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Sources/ContainerizationOCI/Client/RegistryClient+Token.swift b/Sources/ContainerizationOCI/Client/RegistryClient+Token.swift index 5dd93502..cf4e839b 100644 --- a/Sources/ContainerizationOCI/Client/RegistryClient+Token.swift +++ b/Sources/ContainerizationOCI/Client/RegistryClient+Token.swift @@ -159,6 +159,10 @@ extension RegistryClient { internal func createTokenRequest(parsing authenticateHeaders: [String]) throws -> TokenRequest { let parsedHeaders = Self.parseWWWAuthenticateHeaders(headers: authenticateHeaders) + return try createTokenRequest(from: parsedHeaders) + } + + internal func createTokenRequest(from parsedHeaders: [AuthenticateChallenge]) throws -> TokenRequest { let bearerChallenge = parsedHeaders.first { $0.type == "Bearer" } guard let bearerChallenge else { throw ContainerizationError(.invalidArgument, message: "missing Bearer challenge in \(TokenRequest.authenticateHeaderName) header") @@ -174,6 +178,21 @@ extension RegistryClient { return tokenRequest } + internal static func authenticationFailureReason(authentication: Authentication?, challenges: [AuthenticateChallenge]) -> String? { + guard authentication != nil else { + return nil + } + let hasBearerChallenge = challenges.contains { $0.type.caseInsensitiveCompare("Bearer") == .orderedSame } + guard !hasBearerChallenge else { + return nil + } + let hasBasicChallenge = challenges.contains { $0.type.caseInsensitiveCompare("Basic") == .orderedSame } + guard hasBasicChallenge else { + return nil + } + return "access denied or wrong credentials" + } + internal static func parseWWWAuthenticateHeaders(headers: [String]) -> [AuthenticateChallenge] { var parsed: [String: [String: String]] = [:] for challenge in headers { diff --git a/Sources/ContainerizationOCI/Client/RegistryClient.swift b/Sources/ContainerizationOCI/Client/RegistryClient.swift index 14f3142c..17316b7b 100644 --- a/Sources/ContainerizationOCI/Client/RegistryClient.swift +++ b/Sources/ContainerizationOCI/Client/RegistryClient.swift @@ -178,9 +178,13 @@ public final class RegistryClient: ContentClient { response = _response if _response.status == .unauthorized || _response.status == .forbidden { let authHeader = _response.headers[TokenRequest.authenticateHeaderName] + let authChallenges = Self.parseWWWAuthenticateHeaders(headers: authHeader) + if let reason = Self.authenticationFailureReason(authentication: self.authentication, challenges: authChallenges) { + throw RegistryClient.Error.invalidStatus(url: path, _response.status, reason: reason) + } let tokenRequest: TokenRequest do { - tokenRequest = try self.createTokenRequest(parsing: authHeader) + tokenRequest = try self.createTokenRequest(from: authChallenges) } catch { // The server did not tell us how to authenticate our requests, // Or we do not support scheme the server is requesting for. diff --git a/Tests/ContainerizationOCITests/AuthChallengeTests.swift b/Tests/ContainerizationOCITests/AuthChallengeTests.swift index f8621019..1fb262bc 100644 --- a/Tests/ContainerizationOCITests/AuthChallengeTests.swift +++ b/Tests/ContainerizationOCITests/AuthChallengeTests.swift @@ -55,4 +55,20 @@ struct AuthChallengeTests { #expect(challenges.count == 1) #expect(challenges[0] == testCase.expected) } + + @Test func basicChallengeWithCredentialsReportsAuthenticationFailure() throws { + let challenges = RegistryClient.parseWWWAuthenticateHeaders(headers: ["Basic realm=\"IssueRegistry\""]) + let reason = RegistryClient.authenticationFailureReason( + authentication: BasicAuthentication(username: "issue-user", password: "wrong-password"), + challenges: challenges) + + #expect(reason == "access denied or wrong credentials") + } + + @Test func basicChallengeWithoutCredentialsDoesNotReportAuthenticationFailure() throws { + let challenges = RegistryClient.parseWWWAuthenticateHeaders(headers: ["Basic realm=\"IssueRegistry\""]) + let reason = RegistryClient.authenticationFailureReason(authentication: nil, challenges: challenges) + + #expect(reason == nil) + } }