HTTP authentication challenges with Swift concurrency

This thread has been locked by a moderator.

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.

Up vote post of eskimo
247 views