Recently, I worked on implementing OAuth 2 authentication using just plain NSURLSession APIs. While I came to really like Apple’s authentication API for HTTP Basic authentication in the past, I was really disappointed with it when trying to use it for OAuth 2.

The Problem

OAuth 2 generally works like this:

  1. You use some initial authentication method (e.g. web login or username & password) to obtain a refresh token and an access token.
  2. You add the access token to the Authorization header of every request that needs authentication. The value of this header field looks roughly like this: Bearer my_access_token.
  3. The access token has a very short life-time, usually between a few minutes and a few hours. When it expires, the request fails with a HTTP 401 status code and the header field WWW-Authenticate will be set to something like Bearer realm:additional_info. The Bearer part is basically the identifier for OAuth 21.
  4. The client then needs to use the refresh token from step 1 to obtain a new access token from the authorization server.
  5. It then retries the original request with the new access token.

More generally, most authentication flows work as describe in this graphic:

A diagram showing a generalized authentication flow. 1. Client talks to server. 2. Server responds with authentication failure. 3. Client talks to authentication details provider. 4. Authentication details provider respondes. 5. Client retries original request to server.

  1. The client makes a request to the server.
  2. The request fails because the client did not provide authentication details or the ones provided were invalid.
  3. The client talks to some kind of authentication details provider to get new authentication details. In the case of OAuth 2 this is the authentication server that provides new access tokens. But it could just as well simply be the user that the application asks for their username and password in the case of HTTP Basic authentication or a lookup in the local keychain.
  4. The authentication provider provides new details. Be that the response from a server, or the input from a dialog shown to the user.
  5. The client uses these new authentication details, adapts the original request to include these details and retries the request.

How To

To implement authentication flows like this, the NSURLSessionDelegate has a really useful method called: URLSession:task:didReceiveChallenge:completionHandler: where the completionHandler has the following type: void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential).

This is basically exactly what you want. According to the documentation:

The URL loading system classes do not call their delegates to handle request challenges unless the server response contains a WWW-Authenticate header.

Calling the delegate method when the WWW-Authenticate header is set is exactly what we want.

You can then fetch the required credential and call the completion block when you are done to hand the new credentials to the URL loading system and it will retry the request automatically, without you having to do anything. This is especially valuable, as there is no other way to retry an NSURLSessionTask, all you can do if you are not relying on this method is create a new task once the original one failed.

Except there are two things missing:

  1. The delegate method is not actually called whenever the WWW-Authenticate is set. It’s only called if the WWW-Authenticate specifies HTTP-Basic2 Authorization, so only when its value is something like Basic Realm=.... This means it will not be called for OAuth (2).

  2. The completion block also only supports HTTP Basic authorization. The only thing you can hand to the completion block is a URLCredential object. But a URLCredential always consists of a username and password, there is no token property or similar. And the username and password will always be encoded as HTTP Basic Authorization header, there is no way to influence how whatever you put in username and password gets encoded into the header of the retried request. In fact, there is no way to modify the request to retry at all. There is just no way to access it3.

In summary, almost all the pieces are there to have a nice and simple way to implement OAuth following all the patterns in the URL Loading system, except that the conditions when the delegate method is called and the completionBlock parameters are too limited. 🤦‍♂️

My Wish

All we would need is a slightly more flexible way to configure the retried response. It would even be enough to only be able to provide a value for the authorization header. Similar to the problems above we only need two things:

  1. Call URLSession:task:didReceiveChallenge:completionHandler: on every response that contains a WWW-Authenticate header, not just for HTTP Basic Auhorization.
  2. Make the completionBlock accept a protocol instead of the URLCredential directly, like so:
protocol AuthorizationValueProviding {
    func authorizationHeaderValue() -> String
}

protocol URLSessionTaskDelegate {
    // adapted version of existing method, note the changed completionBlock
    optional func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, AuthorizationValueProviding?) -> Void);
}

And URLCredential could just implement that protocol with a default HTTP Basic handling

extension URLCredential : AuthorizationValueProviding {
    func authorizationHeaderValue() -> String {
        // HTTP Basic encoding of username and password. See https://tools.ietf.org/html/rfc7617#section-2
        let authorizationData = (user + ":" + password).data(using: .utf8)?.base64EncodedString()
        return "Basic \(authorizationData)" 
    }
}

This way you could still pass a simple URLCredential object to the same completionBlock and keep backwards compatibility4.

And you could even make String implement it by default if you liked:

extension String : AuthorizationValueProviding {
    func authorizationHeaderValue() -> String {
        return  self
    }
}

and then use that to implement an OAuth callback like so:

optional func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, AuthorizationValueProviding?) -> Void) {
        refreshAccessToken() { newAccessToken:String in
            completionHandler(.useCredential, "Bearer \(newAccessToken)")
        }
    }
}

Can I please have that, Apple? It’s Christmas soon, right?

I filed a bugreport with Apple, if you agree, please duplicate it. If there are other ideas how to do this, please let me know.

  1. Of course, it’s more complicated than that. There are different token types and different authentication schemes within OAuth. But it’s a good enough approximation, and at least in the case I tried I couldn’t find a better way to identify whether a server is actually asking for OAuth. In reality, in most cases you don’t need to guess from the response, you already know that server requires OAuth when it needs authentication and you would only sanity-check the response to confirm it contains what you expect before you start the OAuth flow.

  2. It might also support HTTP Digest Authorization but I didn’t try it and could not find any documentation.

  3. If you know of one or find one, please tell me! As far as I can tell, the request on the URLSessionTask is immutable, the task doesn’t have any way to retry it and the completionBlock of the delegate call doesn’t have a way to specify a new request.

  4. If we need to support both Basic and Digest authorization the credential probably would have to be wrapped in another object that specifies whether it should be encoded as Basic or Digest. Alternatively, at least in Swift, the delegate method could be overloaded with two different completion blocks, one using the new AuthorizationValueProviding the other using the old URLCredential and mapping it accordingly.