Transparent OAuth Token Refresh Using ReactiveCocoa

OAuth 2.0 refresh tokens meet your master

Almost all the apps that we develop integrate with some sort of backend web service and a lot of them also require users to authenticate in order to access certain resources.

Using some sort of OAuth 2.0 for authentication is very common today. For example we had to integrate with the Salesforce REST API in one of our recent apps. When developing this app we came to a pretty good solution for automatically and transparently refreshing the user’s access token when needed. In this post we would like to share that solution.

OAuth 2.0 basically works by issuing an access token and a refresh token to an authenticated user. The access token is needed in all requests to the API to ensure the user is authorized to access the requested resources. The access token is typically short-lived and the developer should assume it can expire at any time. The refresh token can be used to request a new access token when the old one has expired without needing the user to re-authenticate.

The problem

In order to correctly handle situations where an access token has expired we need to catch all errors and check the reason. In case the error is caused by an expired access token we should try to get a new one using the refresh token and if successful we should replay the original request with the new access token. In case the request failed for any other reason we should just let the regular error handling mechanisms handle the situation.

What we did

At Shape we love ReactiveCocoa and try to use it as much as possible to make our apps more reliable and maintainable as well as more enjoyable to develop. Since we already build most of our API clients using ReactiveCocoa we had a feeling that the token refresh problem could be solved beautifully using ReactiveCocoa.

One of the best things about ReactiveCocoa is the amazing efforts put into the project by the developers (primarily Github’s Justin Spahr-Summers and Josh Abernathy). They are always ready to assist with their experience and endless knowledge when you find yourself stuck. In this case we had some great input from Josh Abernathy that helped us arrive at the final solution.

We represent API requests using RACSignals that will next and complete or error when the requests finish. An easy way to do this is to use the AFNetworking-RACExtensions pod that we described in our last post: Wrapping AFNetworking with ReactiveCocoa.

An API request in our solution could look like this:

1
2
3
4
5
6
7
8
- (RACSignal *)getResourceWithId:(NSString *)resourceId {
    NSString *path = [NSString stringWithFormat:@"resource/%@", resourceId];
    RACSignal *requestSignal = [[self.requestOperationManager rac_GET:path parameters:nil] map:^id(RACTuple *tuple) {
        RACTupleUnpack(AFHTTPRequestOperation *operation, NSDictionary *response) = tuple;
        return response[@"results"];
    }];
    return [self doRequestAndRefreshTokenIfNecessary:requestSignal];
}

In the above example self.requestOperationManager is an instance of AFHTTPRequestOperationManager that has a custom requestSerializer set to take care of adding the access token to every request. Notice how we wrap our API request signal with the doRequestAndRefreshTokenIfNecessary: method to get the desired transparent token refresh behavior. The method looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
- (RACSignal *)doRequestAndRefreshTokenIfNecessary:(RACSignal *)requestSignal {
    return [requestSignal catch:^(NSError *error) {
        // Catch the error, refresh the token, and then do the request again.
        BOOL hasRefreshToken = [UserManager sharedInstance].refreshToken != nil;
        BOOL httpCode401AccessDenied = error.code == -1011;
        if (httpCode401AccessDenied && hasRefreshToken) {
            NSLog(@"Will attempt to refresh access token.");
            return [[[self refreshToken] ignoreValues] concat:requestSignal];
        }
        return requestSignal;
    }];
}

This method catches errors on API request signals before they are propagated to its subscribers. This is done with the catch: method that lets us create a new signal when an error is sent on a signal. Inside the catch block we check if the error is caused by an expired token or any other error. If it is any other error we return the original signal so the error can just propagate to the subscribers without any modified behavior.

The magic happens when we receive an expired token error. In this case we use the concat: method to create a new signal composed of the refresh token signal and the original signal. The request signal will be re-subscribed to when the refresh signal completes and thus repeat (using the newly acquired access token). Any error will immediately propagate to the subscriber and terminate the signal.

What we achieved

By wrapping our API requests in the doRequestAndRefreshTokenIfNecessary: method we now get automatic and transparent handling of expired access tokens. Consumers of the API clients should not need to worry about tokens that may expire and how to handle that.

With the solution presented above we solved a common problem using very little code and without using any ivars or properties to store state. As mentioned above the main trick used was given to us on the ReactiveCocoa github issues page, but we thought it would be worth explaining here.

If you have any suggestions for improvements then please let us know in the comments.

Comments