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.
OAuth 2 generally works like this:
- You use some initial authentication method (e.g. web login or username & password) to obtain a refresh token and an access token.
- You add the access token to the
Authorizationheader of every request that needs authentication. The value of this header field looks roughly like this:
- 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-Authenticatewill be set to something like
Bearer realm:additional_info. The
Bearerpart is basically the identifier for OAuth 21.
- The client then needs to use the refresh token from step 1 to obtain a new access token from the authorization server.
- It then retries the original request with the new access token.
More generally, most authentication flows work as describe in this graphic:
- The client makes a request to the server.
- The request fails because the client did not provide authentication details or the ones provided were invalid.
- 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.
- The authentication provider provides new details. Be that the response from a server, or the input from a dialog shown to the user.
- The client uses these new authentication details, adapts the original request to include these details and retries the request.
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:
The delegate method is not actually called whenever the
WWW-Authenticateis set. It’s only called if the
WWW-Authenticatespecifies HTTP-Basic2 Authorization, so only when its value is something like
Basic Realm=.... This means it will not be called for OAuth (2).
The completion block also only supports HTTP Basic authorization. The only thing you can hand to the completion block is a
URLCredentialobject. But a
URLCredentialalways consists of a
password, there is no
tokenproperty 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
passwordgets 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. 🤦♂️
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:
URLSession:task:didReceiveChallenge:completionHandler:on every response that contains a
WWW-Authenticateheader, not just for HTTP Basic Auhorization.
- Make the
completionBlockaccept a protocol instead of the
URLCredentialdirectly, like so:
URLCredential could just implement that protocol with a default HTTP Basic handling
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:
and then use that to implement an OAuth callback like so:
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.
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. ↩
It might also support HTTP Digest Authorization but I didn’t try it and could not find any documentation. ↩
If you know of one or find one, please tell me! As far as I can tell, the request on the
URLSessionTaskis immutable, the task doesn’t have any way to retry it and the
completionBlockof the delegate call doesn’t have a way to specify a new request. ↩
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
AuthorizationValueProvidingthe other using the old
URLCredentialand mapping it accordingly. ↩