URLSession Token Authentication: What's the 'correct' way to do it?

I'm working on an API client for a REST service that uses a custom token-based authentiation scheme. The app hits a specificed authentication endpoint with a username and password, said endpoint returns a token that's good for X amount of time, and the app passes that token along with every subsequent request. When that token expires, we start over.


Most literature out there tells me to manually set the Authorization header on my request, but official Apple documentation discourages this, as that header is meant to be 'owned' by the built-in HTTP loading system. That said, official documentation on the 'correct' way to do this is shockingly lacking, and the standard didReceiveChallenge callbacks seem better suited for non-custom Basic/Digest/etc authentication schemes.


One thought I had was registering my own URLProtocol subclass to handle our custom flow. However, while I haven't had a chance to sit down and take a crack at that yet, my understanding from skimming these forums is that it's suffering from some bit-rot right now, so it 'might' (?) not be the best choice. That, and it's also not clear to me whether the rules around the Authorization header change when a custom URLProtocol is in play.


So, community (paging eskimo in particular!), what's the correct way for me to go about this?

Solved Answer

Most literature out there tells me to manually set the

Authorization
header on my request, but official Apple documentation discourages this, as that header is meant to be 'owned' by the built-in HTTP loading system.

There is, indeed, a serious Catch-22 here. If you set the

Authorization
header manually, you’re stomping on fields that are owned by NSURLSession’s built-in authentication infrastructure. However, that infrastructure does not support custom authentication challenges (r. 26855589). So, what’s a developer to do?

If you have control over the server then there’s a good way out of this: change the server to pick up the authentication token from a custom header.

If you don’t have control over the server, there’s no good solution. If I were in your shoes I’d manually set the

Authorization
header. That’s the best of a bad set of alternatives. Critically, lots of folks do this, so whatever mechanism that we eventually introduce to get around this issue will have to be compatible with this approach.

One thought I had was registering my own URLProtocol subclass to handle our custom flow.

Yeah, don’t do this. A custom

NSURLProtocol
subclass is going to give you much more long-term grief than setting the
Authorization
header directly.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

3 Replies

Most literature out there tells me to manually set the

Authorization
header on my request, but official Apple documentation discourages this, as that header is meant to be 'owned' by the built-in HTTP loading system.

There is, indeed, a serious Catch-22 here. If you set the

Authorization
header manually, you’re stomping on fields that are owned by NSURLSession’s built-in authentication infrastructure. However, that infrastructure does not support custom authentication challenges (r. 26855589). So, what’s a developer to do?

If you have control over the server then there’s a good way out of this: change the server to pick up the authentication token from a custom header.

If you don’t have control over the server, there’s no good solution. If I were in your shoes I’d manually set the

Authorization
header. That’s the best of a bad set of alternatives. Critically, lots of folks do this, so whatever mechanism that we eventually introduce to get around this issue will have to be compatible with this approach.

One thought I had was registering my own URLProtocol subclass to handle our custom flow.

Yeah, don’t do this. A custom

NSURLProtocol
subclass is going to give you much more long-term grief than setting the
Authorization
header directly.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks eskimo, this is really helpful!


In regards to my thought on URLProtocol, I had some time last night to take a crack at it (for the sake of science, of course). By the time I was done, I had two thoughts:


1. Holy moly, this API needs some love.

2. Holy moly, this is pretty slick.


It was honestly very cool in the way I was able to hook into the didReceiveChallenge machinery and make it look like a native challenge by URLSession. All the crazy token stuff was handled in the background by the URLProtocol subclass and completely hidden to the user. By the end of my little experiment, the entirety of my front-facing client looked like:


let myBaseURL = URL(string: "my-custom-protocol://gateway.myserver.com")!
lazy var session: URLSession = {
    let configuration = URLSessionConfiguration.default
    configuration.protocolClasses = [MyURLProtocol.self]
    return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()

func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    guard challenge.previousFailureCount < 5 else {
        completionHandler(.cancelAuthenticationChallenge, nil)
        return
    }

    if let proposedCredential = challenge.proposedCredential, proposedCredential.hasPassword {
        completionHandler(.useCredential, proposedCredential)
        return
    }

    switch challenge.protectionSpace.authenticationMethod {
    case NSURLAuthenticationMethodDefault where challenge.protectionSpace.protocol == "my-custom-protocol":
        completionHandler(.useCredential, URLCredential(user: "<username>", password: "<password>", persistence: .permanent))
    default:
        completionHandler(.cancelAuthenticationChallenge, nil)
    }
}

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    let url = myBaseURL.appendingPathComponent("/some/data/endpoint/")
    let task = session.dataTask(with: url) { (data, response, error) in
        if let result = try? JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.allowFragments) as? NSDictionary {
            print(result)
        }
    }
    task.resume()
    return true
}


At the same time, the bit-rot is real. URLProtocol was clearly a product of its time and not designed for anything like URLSession in mind. Despite this, I really think there's potential here, so I gotta ask: is there hope for URLProtocol modernization in the near future (perhaps even it getting superceded by a new URLSessionProtocol class), or is the writing on the wall for this API?

NSURLProtocol
has, as you’ve noted, suffered from bit rot however there are also deeper, architectural issues:
  • A lot of folks use it recursively (à la the CustomHTTPProtocol sample code) and it was simply not designed for that.

  • The protocol is only available within your process, and these days more and more subsystems are moving their networking out of process (most notably WKWebView and the media subsystem).

The second point is the real kicker. To fix this Apple would have to do something super clever, like host each protocol in its own app extension. That’s not impossible, but it would require a major engineering effort.

Back in the day (and I’m talking way back in the day, when the Foundation URL loading system was first introduced as part of Safari), you could put an

NSURLProtocol
subclass into a bundle and the system would make it available to all apps. That neat-o-rama but needless to say those days are gone.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"