Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Sources/ContainerizationOCI/Client/RegistryClient+Token.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion Sources/ContainerizationOCI/Client/RegistryClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions Tests/ContainerizationOCITests/AuthChallengeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}