My dream for NSURLSession Authentication
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:
- 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
Authorization
header of every request that needs authentication. The value of this header field looks roughly like this:Bearer my_access_token
. - 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 likeBearer realm:additional_info
. TheBearer
part 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.
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:
-
The delegate method is not actually called whenever the
WWW-Authenticate
is set. It’s only called if theWWW-Authenticate
specifies HTTP-Basic2 Authorization, so only when its value is something likeBasic 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
URLCredential
object. But aURLCredential
always consists of ausername
andpassword
, there is notoken
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 inusername
andpassword
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:
- Call
URLSession:task:didReceiveChallenge:completionHandler:
on every response that contains aWWW-Authenticate
header, not just for HTTP Basic Auhorization. - Make the
completionBlock
accept a protocol instead of theURLCredential
directly, like so:
And 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
URLSessionTask
is immutable, the task doesn’t have any way to retry it and thecompletionBlock
of 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
AuthorizationValueProviding
the other using the oldURLCredential
and mapping it accordingly. ↩