Episode #41

Authentication with AFNetworking

18 minutes
Published on November 8, 2012

This video is only available to subscribers. Get access to this video and 584 others.

Many APIs require some sort of authentication. In this episode, we explore the use of an API that authenticates with a username and password, and returns an authenticated token that has an expiration date. You'll see use of AFNetworking to deal with the request, attaching the authenticated token as an HTTP Header to outgoing requests, as well as the use of SSKeychain to abstract away the lower level Keychain API.

Episode Links

Creating a class to securely store the authentication token

<strong>CredentialStore.h</strong>
@interface CredentialStore : NSObject

- (BOOL)isLoggedIn;
- (void)clearSavedCredentials;
- (NSString *)authToken;
- (void)setAuthToken:(NSString *)authToken;

@end
<strong>CredentialStore.m</strong>
#import "CredentialStore.h"
#import "SSKeychain.h"

#define SERVICE_NAME @"NSScreencast-AuthClient"
#define AUTH_TOKEN_KEY @"auth_token"

@implementation CredentialStore

- (BOOL)isLoggedIn {
    return [self authToken] != nil;
}

- (void)clearSavedCredentials {
    [self setAuthToken:nil];
}

- (NSString *)authToken {
    return [self secureValueForKey:AUTH_TOKEN_KEY];
}

- (void)setAuthToken:(NSString *)authToken {
    [self setSecureValue:authToken forKey:AUTH_TOKEN_KEY];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"token-changed" object:self];
}

- (void)setSecureValue:(NSString *)value forKey:(NSString *)key {
    if (value) {
        [SSKeychain setPassword:value
                     forService:SERVICE_NAME
                        account:key];
    } else {
        [SSKeychain deletePasswordForService:SERVICE_NAME account:key];
    }
}

- (NSString *)secureValueForKey:(NSString *)key {
    return [SSKeychain passwordForService:SERVICE_NAME account:key];
}

Logging in using AFNetworking

- (void)login:(id)sender {
    [SVProgressHUD show];

    id params = @{
        @"username": self.usernameField.text,
        @"password": self.passwordField.text
    };

    [[AuthAPIClient sharedClient] postPath:@"/auth/login.json"
                                parameters:params
                                   success:^(AFHTTPRequestOperation *operation, id responseObject) {

                                       NSString *authToken = [responseObject objectForKey:@"auth_token"];
                                       [self.credentialStore setAuthToken:authToken];

                                       [SVProgressHUD dismiss];

                                       [self dismissViewControllerAnimated:YES completion:nil];

                                   } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                       if (operation.response.statusCode == 500) {
                                           [SVProgressHUD showErrorWithStatus:@"Something went wrong!"];
                                       } else {
                                           NSData *jsonData = [operation.responseString dataUsingEncoding:NSUTF8StringEncoding];
                                           NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData
                                                                                                options:0
                                                                                                  error:nil];
                                           NSString *errorMessage = [json objectForKey:@"error"];
                                           [SVProgressHUD showErrorWithStatus:errorMessage];
                                       }
                                   }];   
}

The API Client

The API client simply has the base URL to our API and the ability to attach the auth_token header, if one is present. In order to allow the auth_token to be updatable, we listen for a notification posted by CredentialStore.

#import "AuthAPIClient.h"
#import "CredentialStore.h"

#define BASE_URL @"http://nsscreencast-auth-server.herokuapp.com"

@implementation AuthAPIClient

+ (id)sharedClient {
    static AuthAPIClient *__instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURL *baseUrl = [NSURL URLWithString:BASE_URL];
        __instance = [[AuthAPIClient alloc] initWithBaseURL:baseUrl];
    });
    return __instance;
}

- (id)initWithBaseURL:(NSURL *)url {
    self = [super initWithBaseURL:url];
    if (self) {
        [self registerHTTPOperationClass:[AFJSONRequestOperation class]];
        [self setAuthTokenHeader];

        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(tokenChanged:)
                                                     name:@"token-changed"
                                                   object:nil];
    }
    return self;
}

- (void)setAuthTokenHeader {
    CredentialStore *store = [[CredentialStore alloc] init];
    NSString *authToken = [store authToken];
    [self setDefaultHeader:@"auth_token" value:authToken];
}

- (void)tokenChanged:(NSNotification *)notification {
    [self setAuthTokenHeader];
}

Fetching a regular authenticated endpoint

Now that we've logged in, it is trivial to request a protected endpoint, because the AuthAPIClient will automatically include the auth_token header if necessary.

  [[AuthAPIClient sharedClient] getPath:@"/home/index.json"
                               parameters:nil
                                  success:^(AFHTTPRequestOperation *operation, id responseObject) {
                                      [SVProgressHUD dismiss];
                                      self.messageTextView.text = operation.responseString;
                                  } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                      if (operation.response.statusCode == 500) {
                                          [SVProgressHUD showErrorWithStatus:@"Something went wrong!"];
                                      } else {
                                          NSData *jsonData = [operation.responseString dataUsingEncoding:NSUTF8StringEncoding];
                                          NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData
                                                                                               options:0
                                                                                                 error:nil];
                                          NSString *errorMessage = [json objectForKey:@"error"];
                                          [SVProgressHUD showErrorWithStatus:errorMessage];
                                      }

                                      if (operation.response.statusCode == 401) {
                                          // the auth token we have is no longer valid, clear it
                                          [self.credentialStore setAuthToken:nil];
                                      }
                                  }];

Notes about the example server

You can use the example Rails app provided in this episode on your own (running it locally or deploying to somewhere like Heroku). Or you can use the server I already deployed.

The URL is http://nsscreencast-auth-server.herokuapp.com.

The endpoints:

  • /auth/login.json (Parameters: username, password) Note: this endpoint accepts a ?ttl=<seconds> parameter that will set the expiration date of the token (only if the current token is already expired). This can be useful for testing the expiration.
  • /home/index.json (Requires auth_token header)

I created 10 admin accounts for you to use: (admin, admin2, admin3, admin4, etc). All of them have a password of secret.