Skip to content

Handle authentication challenges with DefaultHTTPClient #409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 4, 2024
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file. Take a look

### Added

#### Shared

* You can now use `DefaultHTTPClientDelegate.httpClient(_:request:didReceive:completion:)` to handle authentication challenges (e.g. Basic) with `DefaultHTTPClient`.

#### Navigator

* The `AudioNavigator` API has been promoted to stable and ships with a new Preferences API.
Expand Down
71 changes: 70 additions & 1 deletion Sources/Shared/Toolkit/HTTP/DefaultHTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
import Foundation
import UIKit

public enum URLAuthenticationChallengeResponse {
/// Use the specified credential.
case useCredential(URLCredential)
/// Use the default handling for the challenge as though this delegate method were not implemented.
case performDefaultHandling
/// Cancel the entire request.
case cancelAuthenticationChallenge
/// Reject this challenge, and call the authentication delegate method again with the next
/// authentication protection space.
case rejectProtectionSpace
}

/// Delegate protocol for `DefaultHTTPClient`.
public protocol DefaultHTTPClientDelegate: AnyObject {
/// Tells the delegate that the HTTP client will start a new `request`.
Expand Down Expand Up @@ -42,6 +54,14 @@ public protocol DefaultHTTPClientDelegate: AnyObject {
/// This will be called only if `httpClient(_:recoverRequest:fromError:completion:)` is not implemented, or returns
/// an error.
func httpClient(_ httpClient: DefaultHTTPClient, request: HTTPRequest, didFailWithError error: HTTPError)

/// Requests credentials from the delegate in response to an authentication request from the remote server.
func httpClient(
_ httpClient: DefaultHTTPClient,
request: HTTPRequest,
didReceive challenge: URLAuthenticationChallenge,
completion: @escaping (URLAuthenticationChallengeResponse) -> Void
)
}

public extension DefaultHTTPClientDelegate {
Expand All @@ -55,6 +75,15 @@ public extension DefaultHTTPClientDelegate {

func httpClient(_ httpClient: DefaultHTTPClient, request: HTTPRequest, didReceiveResponse response: HTTPResponse) {}
func httpClient(_ httpClient: DefaultHTTPClient, request: HTTPRequest, didFailWithError error: HTTPError) {}

func httpClient(
_ httpClient: DefaultHTTPClient,
request: HTTPRequest,
didReceive challenge: URLAuthenticationChallenge,
completion: @escaping (URLAuthenticationChallengeResponse) -> Void
) {
completion(.performDefaultHandling)
}
}

/// An implementation of `HTTPClient` using native APIs.
Expand Down Expand Up @@ -200,6 +229,13 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
}
receiveResponse?(response)
},
receiveChallenge: { [weak self] challenge, completion in
if let self = self, let delegate = self.delegate {
delegate.httpClient(self, request: request, didReceive: challenge, completion: completion)
} else {
completion(.performDefaultHandling)
}
},
consume: consume,
completion: { [weak self] result in
if let self = self, case let .failure(error) = result {
Expand Down Expand Up @@ -283,6 +319,15 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
findTask(for: task)?.urlSession(session, didCompleteWithError: error)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let task = findTask(for: task) else {
completionHandler(.performDefaultHandling, nil)
return
}

task.urlSession(session, didReceive: challenge, completion: completionHandler)
}
}

/// Represents an on-going HTTP task.
Expand All @@ -294,6 +339,7 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
private let request: HTTPRequest
fileprivate let task: URLSessionTask
private let receiveResponse: (HTTPResponse) -> Void
private let receiveChallenge: (URLAuthenticationChallenge, @escaping (URLAuthenticationChallengeResponse) -> Void) -> Void
private let consume: (Data, Double?) -> Void
private let completion: (HTTPResult<HTTPResponse>) -> Void

Expand All @@ -312,11 +358,19 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
case finished
}

init(request: HTTPRequest, task: URLSessionDataTask, receiveResponse: @escaping ((HTTPResponse) -> Void), consume: @escaping (Data, Double?) -> Void, completion: @escaping (HTTPResult<HTTPResponse>) -> Void) {
init(
request: HTTPRequest,
task: URLSessionDataTask,
receiveResponse: @escaping (HTTPResponse) -> Void,
receiveChallenge: @escaping (URLAuthenticationChallenge, @escaping (URLAuthenticationChallengeResponse) -> Void) -> Void,
consume: @escaping (Data, Double?) -> Void,
completion: @escaping (HTTPResult<HTTPResponse>) -> Void
) {
self.request = request
self.task = task
self.completion = completion
self.receiveResponse = receiveResponse
self.receiveChallenge = receiveChallenge
self.consume = consume
}

Expand Down Expand Up @@ -427,6 +481,21 @@ public final class DefaultHTTPClient: HTTPClient, Loggable {
}
finish()
}

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completion: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
receiveChallenge(challenge) { response in
switch response {
case let .useCredential(credential):
completion(.useCredential, credential)
case .performDefaultHandling:
completion(.performDefaultHandling, nil)
case .cancelAuthenticationChallenge:
completion(.cancelAuthenticationChallenge, nil)
case .rejectProtectionSpace:
completion(.rejectProtectionSpace, nil)
}
}
}
}
}

Expand Down
Loading