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 Episode source codeincludes server and client apps HTTP Client app - This is the app I used to test out the API 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.