Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best solution to refresh token automatically with AFNetworking?

Once your user is logged in, you get a token (digest or oauth) which you set into your HTTP Authorization header and which gives you the authorization to access your web service. If you store your user's name, password and this token somewhere on the phone (in user defaults, or preferably in the keychain), then your the user is automatically logged in each time the application restarts.

But what if your token expires? Then you "simply" need to ask for a new token and if the user did not change his password, then he should be logged in once again automatically.

One way to implement this token refreshing operation is to subclass AFHTTPRequestOperation and take care of 401 Unauthorized HTTP Status code in order to ask a new token. When the new token is issued, you can call once again the failed operation which should now succeeds.

Then you must register this class so that each AFNetworking request (getPath, postPath, ...) now uses this class.

[httpClient registerHTTPOperationClass:[RetryRequestOperation class]]

Here is an exemple of such a class:

static NSInteger const kHTTPStatusCodeUnauthorized = 401;

@interface RetryRequestOperation ()
@property (nonatomic, assign) BOOL isRetrying;
@end

@implementation RetryRequestOperation
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *, id))success
                              failure:(void (^)(AFHTTPRequestOperation *, NSError *))failure
{
    __unsafe_unretained RetryRequestOperation *weakSelf = self;

    [super setCompletionBlockWithSuccess:success failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        // In case of a 401 error, an authentification with email/password is tried just once to renew the token.
        // If it succeeds, then the opration is sent again.
        // If it fails, then the failure operation block is called.
        if(([operation.response statusCode] == kHTTPStatusCodeUnauthorized)
           && ![weakSelf isAuthenticateURL:operation.request.URL]
           && !weakSelf.isRetrying)
        {
            NSString *email;
            NSString *password;

            email = [SessionManager currentUserEmail];
            password = [SessionManager currentUserPassword];
            // Trying to authenticate again before relaunching unauthorized request.
            [ServiceManager authenticateWithEmail:email password:password completion:^(NSError *logError) {
                if (logError == nil) {
                    RetryRequestOperation *retryOperation;

                    // We are now authenticated again, the same request can be launched again.
                    retryOperation = [operation copy];
                    // Tell this is a retry. This ensures not to retry indefinitely if there is still an unauthorized error.
                    retryOperation.isRetrying = YES;
                    [retryOperation setCompletionBlockWithSuccess:success failure:failure];
                    // Enqueue the operation.
                    [ServiceManager enqueueObjectRequestOperation:retryOperation];
                }
                else
                {
                    failure(operation, logError);
                    if([self httpCodeFromError:logError] == kHTTPStatusCodeUnauthorized)
                    {
                        // The authentication returns also an unauthorized error, user really seems not to be authorized anymore.
                        // Maybe his password has changed?
                        // Then user is definitely logged out to be redirected to the login view.
                        [SessionManager logout];
                    }
                }
            }];
        }
        else
        {
            failure(operation, error);
        }
    }];
}

- (BOOL)isAuthenticateURL:(NSURL *)url
{
    // The path depends on your implementation, can be "auth", "oauth/token", ...
    return [url.path hasSuffix:kAuthenticatePath];
}

- (NSInteger)httpCodeFromError:(NSError *)error
{
    // How you get the HTTP status code depends on your implementation.
    return error.userInfo[kHTTPStatusCodeKey];
}

Please, be aware that this code does not work as is, as it relies on external code that depends on your web API, the kind of authorization (digest, oath, ...) and also which kind of framework you use over AFNetworking (RestKit for example).

This is quite efficient and has proved to work well with both digest and oauth authorization using RestKit tied to CoreData (in that case RetryRequestOperation is a subclass of RKManagedObjectRequestOperation).

My question now is: is this the best way to refresh a token? I am actually wondering if NSURLAuthenticationChallenge could be used to solve this situation in a more elegant manner.

like image 504
Phil Avatar asked Jun 27 '14 08:06

Phil


1 Answers

Your current solution works and you have the code for it, there might be a reasonable amount of code to achieve it but the approach has merits.

Using an NSURLAuthenticationChallenge based approach means subclassing at a different level and augmenting each created operation with setWillSendRequestForAuthenticationChallengeBlock:. In general this would be a better approach as a single operation would be used to perform the whole operation rather than having to copy it and update details, and the operation auth support would be doing the auth task instead of the operation completion handler. This should be less code to maintain, but that code will likely be understood by less people (or take longer to understand by most) so the maintenance side of things probably balances out over all.

If you're looking for elegance, then consider changing, but given that you already have a working solution there is little gain otherwise.

like image 112
Wain Avatar answered Sep 28 '22 10:09

Wain