1
votes

I have an application with fairly simple authorization/authentication and need a bit of help on how to handle the authorization life cycle. Here's the jist of it:

  1. The user enters their credentials to log in and the server responds with a token and stores in the keychain
  2. The token is used in the "Authorization" header for every subsequent URL request (REST calls)
  3. The token eventually expires and the server will respond with 401 status code
  4. At this point the user needs to re-authenticate via the login screen so we can get a new token and retry the request

Here's some code that represents the best I could come up with right now.

static NSString * _authorizationToken;
@implementation MyRestfulModel
+ (id) sharedRestfulModel
{
    // singleton model shared across view controllers
    static MyRestfulModel * _sharedModel = nil;
    @synchronized(self) {
        if (_sharedModel == nil) 
            _sharedModel = [[self init] alloc];
    }
    return _sharedModel;
}

+ (NSString *) authorizationToken
{
    if (!_authorizationToken)
        _authorizationToken = @"";
    return _authorizationToken;
}

+ (void) setAuthorizationToken: (NSString *token)
{
    _authorizationToken = token;
}

-(void) doSomeRestfulCall: (NSString *) restURL
        completionHandler: (void (^)(NSData * data)) callback
{
    // construct url path and request
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:restURL];
    [request setValue:MyRestfulModel.authorizationToken forHTTPHeaderField:@"Authorization"];
    [[[NSURLSession sharedSession] dataTaskWithRequest: request
                                     completionHandler:^(NSData *data, NSURLResponse * response, NSError * error) {

                    NSHTTPResponse * httpResponse = (NSHTTPResponse *) response;
                    if([httpResponse statusCode] == 401) {  // WHAT TO DO HERE ?

                        dispatch_sync(dispatch_get_main_queue(), ^{
                            MyAppDelegate * delegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
                            [delegate showLoginViewController callback:^(NSString * username, NSString * newToken) {
                                // recreate the NSURLSession and NSURLConfiguration with the new token and retry
                                MyRestfulModel.authenticationToken = token;
                                [self doSomeRestfulCall:callback];
                            }];
                        }
                        return;
                    } else {
                        callback(data);
                    }
    }] resume];
}

I hope to do it like this so that the view controllers never need to worry about retrying a call due to a token expiration, all of the scary session handling and authentication can be done by one object.

I was considering trying to use NSURLSessionDelegate but I couldn't figure out the didReceiveChallenge part w.r.t. popping up the login view controller. The other option I was considering is adding a "retryHandler" similar to the completion handler which would be called after the user re-authenticates.

1

1 Answers

0
votes

I think I found a nice solution. Take the "doSomeRestfulCall" method for example.

-(void) doSomeRestfullCall: (NSString *) restURL
         completionHandler: (void (^)(NSData * data)) callback
{
    // construct url path and request
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:restURL];
    [request setValue:MyRestfulModel.authorizationToken forHTTPHeaderField:@"Authorization"];
    [[[NSURLSession sharedSession] dataTaskWithRequest: request
                                     completionHandler:^(NSData *data, NSURLResponse * response, NSError * error) {
                   NSHTTPResponse * httpResponse = (NSHTTPResponse *) response;
                   if([httpResponse statusCode] == 401) {
                        [LoginViewController showWithCompletion:^(NSString *username, NSString *token) {
                            NSLog(@"Retrying request after user reauthenticated");
                            MyRestfulModel.authorizationToken = token;
                            [self doSomeRestfulCall:restURL completionHandler:callback];
                        }];
                        return;
                   } else {
                       callback(data);
                   }
               }] resume];
}

Then the login view controller is where a lot of the magic happens. (You can also use an alert view if you don't want a custom login controller, check this post out: http://nscookbook.com/2013/04/ios-programming-recipe-22-simplify-uialertview-with-blocks/)

@interface LoginViewController
@property (copy) void(^completionBlock)(NSString * username, NSString * tokenCredentials);
@end

@implementation LoginViewController

+ (void) showWithCompletion: (void (^)(NSString * username, NSString * tokenCredentials))completion
{
    AppDelegate * appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
    UIViewController * rootController = appDelegate.window.rootViewController;
    UIStoryboard * storyboard = rootController.storyboard;

    LoginViewController * controller = [storyboard instantiateViewControllerWithIdentifier:@"Login"];
    controller.completionBlock = completion;

    controller.delegate = wrapper;
    controller.modalPresentationStyle = UIModalPresentationFormSheet;
    [rootController presentViewController:controller animated:YES completion:nil];
}


//
// <code for contacting your authentication service>
//


-(void) loginSuccessful: (NSString *) username withTokenCredentials:(NSString *)token
{
    // <code to store username and token in the keychain>

    if (self.completionBlock)
        self.completionBlock(username, token);
}