OAuth2 for iPhone and iPad applications
If you're wondering why its been a bit quiet around here of late, its because I've been hard at work on my first iPad application, a client for 37 Signals Campfire. I'll be talking about that more in another blog post but today I want to focus on something else: OAuth2.
I was invited by 37 Signals' Jeremy Kemper to try out their in-progress Launchpad + OAuth2 integration platform. I was happy to: the resulting integration would mean a more secure and streamlined experience for my app's users. Instead of having to add all of their individual Campfire accounts one at a time, they could simply sign in with their 37 Signals identity once and have all of the accounts associated with their ID imported into the app in one go.
Further more, the use of OAuth2 means that my app never needs to store a copy of the user's username and password. Once they have logged into their 37 Signals identity using a web view, all my app needs to do is store an access token and refresh it as needed. Users can revoke permission for an app to access their account at any time.
Introducing LROAuth2Client
I was quite keen from the outset to open source my OAuth2 implementation and that is what I have done. Documentation is sparse and having been asked on Twitter recently how to use it, I thought I'd write this post to give a basic outline of how it works.
Before continuing, it would be helpful to familiarise yourself with OAuth2 if you haven't already. The basic gist is:
- A client requests access to a provider's service using its own unique client ID and secret token.
- The user logs into the service directly (using a web page on the provider's server) and grants the client permission to access.
- The provider redirects the user to a URL unique to the client passing along a verification code in the query string.
- The client verifies the authorization request and uses the verification code from step 3 to obtain an access token.
- The client may periodically refresh the access token when it expires.
The above sequence describes the "web server" flow as outlined in the draft OAuth2 spec; there are other flows but this is the only one supported by LROAuth2Client right now and is the step I will outline below.
Getting started
To get started with LROAuth2Client, you will need the following:
- Your client's unique ID, redirect URI and secret token. These are typically provided after registering your client with the provider.
- The end-user endpoint URI. This is where you will redirect users so they can sign in to the provider's service.
- The token URI. This is used in conjunction with the verification code to obtain an access token.
If possible, you should try and obtain the end-user and token URIs at runtime rather than hardcoding them into your application. This can be typically done by making an unauthorised request to a secure URI and obtaining them from the WWW-Authenticate header in the returned 401 response. 1
Creating an instance of the client
Having established that you need to ask the user to authorise your app for access, you need to create an instance of LROAuth2Client
.
You'll notice that the redirect URL can be any valid URL - I recommend using a custom scheme for your app. LROAuth2Client
will intercept calls to this URL for you. If the provider only supports HTTP or HTTPS URLs, it will intercept those as well.
At this point, you will need to configure the client with the end-user URI and the token URI. As I mentioned earlier, the best way of doing this is to send an unauthorised request and obtain these from the returned header but for the purposes of this tutorial, we will hardcode them:
Authenticating the user
To actually ask the user to authenticate, you will need to construct a UIWebView
, which will load the request for the end-user URI. In these examples, we'll assume we have an instance of UIWebView
already loaded from a NIB file and stored in an outlet called myWebView
. You will then need to ask the client to authenticate the user using that webview:
LROAuth2Client
will assign itself as the web view's delegate and make the request to the end-user URI. Once the user authenticates and authorises access the provider will redirect to your client's redirect URL.
LROAuth2Client
will intercept a request to this URL, cancel the request and extract the verification code from the request query string. It will then make a POST request to the token URL using the verification code and assuming everything works as it should, will receive an access token that can now be used to authorise any requests your app makes.
When the whole process has finished, LROAuth2Client
will keep a pointer to the access token (an instance of LROAuth2AccessToken
) and notify its delegate. You can use this delegate method to store this access token somewhere that can be accessed by the rest of your app. Because LROAuth2AccessToken
implements the NSCoding
protocol, you could simply save the token to disk, as follows:
Refreshing the token
Access tokens will typically expire after a certain amount of time. LROAuth2AccessToken
has an expiresAt
property (an instance of NSDate
) and a refreshToken
property. If the token has expired, you should use the client to refresh that token and save the updated token to disk. An example implementation might look something like this:
The bigger picture
In case you were wondering how you would put this all together in your application, I've created a sample XCode project that you can download from Github. It uses the new Facebook graph API to retrieve a list of your Facebook friends and uses OAuth2 for authorization. To get started, just follow the instructions in the provided README.
At present, LROAuth2Client
only does what I needed it to do. It has the following limitations:
- Only supports the web-server flow.
- Only supports services that return the access token in JSON or form encoded string format.
- Has a dependency on ASIHTTPRequest and TouchJSON.
I'm very open to pull requests so if you feel like stripping away the ASIHTTPRequest
dependency and using NSURLConnection instead, please, go ahead. If you want to add XML support and support for the other flows, that would be great too. If you have any feedback, send a tweet to @lukeredpath.