I recently handled a couple of questions about this for DTS, so I thought I’d write it up for the benefit of all.
If you have any questions or comments, please put them in a new thread here on DevForums. Tag it with Foundation and CFNetwork so that I see it.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
HTTP authentication challenges with Swift concurrency
URLSession
has always made it easy to access HTTP resources, but things get even better when you combine that with Swift concurrency. For example:
import Foundation
func main() async throws {
// Set up the request.
let url = URL(string: "https://example.com")!
let request = URLRequest(url: url)
// Run it; this will throw if there’s a transport error.
let (body, response) = try await URLSession.shared.data(for: request)
// Check for a server-side error.
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
… handle the error …
}
// Success.
… HTTP response headers are in `response.allHeaderFields` …
… HTTP response body is in `body` …
}
try await main()
But what happens if you need to handle an HTTP authentication challenge? The current documentation for authentication challenges is very good, but it assumes that you have a session delegate. That doesn’t really mesh well with Swift concurrency.
Fortunately there’s an easier way: Pass in a per-task delegate. Consider this slightly updated snippet:
let url = URL(string: "https://postman-echo.com/basic-auth")!
let request = URLRequest(url: url)
let (body, response) = try await URLSession.shared.data(for: request)
This fetches a resource, https://postman-echo.com/basic-auth
, that requires HTTP Basic authentication. This request fails with a 401 status code because nothing handles that authentication. To fix that, replace the last line with this:
let delegate = BasicAuthDelegate(user: "postman", password: "password")
let (body, response) = try await URLSession.shared.data(for: request, delegate: delegate)
This creates an instance of the BasicAuthDelegate
, passing in the user name and password required to handle the challenge. That type implements the URLSessionTaskDelegate
protocol like so:
final class BasicAuthDelegate: NSObject, URLSessionTaskDelegate {
init(user: String, password: String) {
self.user = user
self.password = password
}
let user: String
let password: String
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
// We only care about Basic authentication challenges.
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic else {
return (.performDefaultHandling, nil)
}
// Limit the authentication attempts.
guard challenge.previousFailureCount < 2 else {
return (.cancelAuthenticationChallenge, nil)
}
// If all is well, return a credential.
let credential = URLCredential(user: user, password: password, persistence: .forSession)
return (.useCredential, credential)
}
}
This delegate handles the NSURLAuthenticationMethodHTTPBasic
authentication challenge, allowing the overall request to succeed.
Now you have the best of both worlds:
-
An easy to use HTTP API
-
With the flexibility to handle authentication challenges
This approach isn’t right for all programs, but if you’re just coming up to speed on URLSession
it’s a great place to start.