/AuthenticationController

The code for my recipe in the upcoming iPhone Recipes book

Primary LanguageObjective-C

Dennis Blöte
http://dennisbloete.de
mail@dennisbloete.de

Draft for the iPhone Recipes Book, version 0.1 (08/28/2009)


Authentication handling
=======================

Problem
-------
Most applications that deal with users registered on a backend (like webapps) are likely to verify
the user credentials. This recipe explains how to extract the authentication functionality that
handles the workflow of prompting the user for credentials and verifying them at the backend. 



Solution
--------
A separate (Modal View) Controller that acts as the apps AuthenticationController and gets called by
other controllers. This controller encapsulates the authentication logic so that it is cleanly separated
from the rest of the app. The provided code is compatible for all iPhone OS from 2.0 on.

The basic procedure will work like this: The app looks for previously stored credentials and uses them to
authenticate the user. If there are no stored credentials, the user gets prompted for his username and
password. After that the backend gets called and we try to authenticate: In case of success the credentials
get saved, otherwise the user gets alerted and is prompted for the username and password again.

To start, create a new UIViewController subclass called AuthenticationController and let Xcode generate an
accompanying XIB file. The XIB will contain the login form with UITextFields for the username and password,
as well as a submit and cancel button.

The interface for the AuthenticationController will look like this:

//----- AuthenticationController.h -----

#define kUsernameDefaultsKey @"username"
#define kPasswordDefaultsKey @"password"

@interface AuthenticationController : UIViewController {
  @private
	  id target;
	  SEL selector;
	  UIViewController *viewController;
	  UIActionSheet *authSheet;
	  NSString *username;
	  NSString *password;
	  IBOutlet UITextField *usernameField;
	  IBOutlet UITextField *passwordField;
	  IBOutlet UIButton *submitButton;
	  IBOutlet UIButton *cancelButton;
}
 
@property (nonatomic, retain) NSString *username;
@property (nonatomic, retain) NSString *password;
 
- (id)initWithTarget:(id)theTarget andSelector:(SEL)theSelector andViewController:(UIViewController *)viewController;
- (void)authenticate;
- (IBAction)submit:(id)sender;
- (IBAction)cancel:(id)sender;
 
@end

//----- AuthenticationController.h -----

The header file contains two macros which define the keys for saving and restoring the username and password.
It defines the outlets for the text fields and buttons that need to be connected in Interface Builder. You also
need to wire up the buttons with the appropriate actions called submit and cancel.

The AuthenticationController gets initalized with a target, selector and view controller. The target is the view
controller or app delegate that holds a reference to the AuthenticationController and receives the selector after
the authentication process is finished. The passed view controller is the one that brings up the login view as a
moal view controller.

Before we will have a look at the implementation, lets examine how this will be used by the target - in this case
an instance of an UIApplicationDelegate:

//----- AuthAppDelegate.m -----

- (void)authenticate {
	authController = [[AuthenticationController alloc] initWithTarget:self 
	                                                      andSelector:@selector(authenticationSucceeded:)
	                                                andViewController:navigationController];
	[authController authenticate];
}

- (void)authenticationSucceeded:(BOOL)success {
	NSLog(@"Authentication succeeded: %@!", success ? @"YES" : @"NO");
	[authController release];
}

//----- AuthAppDelegate.m -----

The AuthAppDelegate should hold a reference to an instance of the AuthenticationController and implement two
methods that deal with the authentication: The authenticate method initializes an AuthenticationController with
the AuthAppDelegate as target and the authenticationSucceeded method as selector. This selector will receive a
boolean argument which tells us whether the authentication succeeded. The view controller is the root view controller
of the app.

After initializing the AuthenticationController we ask the instance to start an authentication process. If the 
request cannot be verified directly, the login screen will appear. In any case the authentication will result in
a call to the authenticationSucceeded: method which was passed as the selector to the AuthenticationController.

Let's have a look at the authentication workflow: First of all there are some private methods that will be used
internally.

//----- AuthenticationController.m -----

@interface AuthenticationController ()
- (void)failWithMessage:(NSString *)theMessage;
- (void)startAuthentication;
- (void)finishAuthenticationWithSuccess:(BOOL)success;
- (void)presentLogin;
- (void)dismissLogin;
- (void)showAuthenticationSheet;
- (void)dismissAuthenticationSheet;
@end


@implementation AuthenticationController

@synthesize username, password;

- (id)initWithTarget:(id)theTarget andSelector:(SEL)theSelector andViewController:(UIViewController *)theViewController {
	[super initWithNibName:@"Authentication" bundle:nil];
	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
	self.username = [defaults stringForKey:kUsernameDefaultsKey];
	self.password = [defaults stringForKey:kPasswordDefaultsKey];
	target = theTarget;
	selector = theSelector;
	viewController = theViewController;
	return self;
}

//----- AuthenticationController.m -----

The initializer loads the XIB file that was created along with the controller and tries to fetch the username and
password from the NSUserDefaults. The references are set without retaining the instances.

//----- AuthenticationController.m -----

- (void)viewDidLoad {
  [super viewDidLoad];
	usernameField.text = username;
	passwordField.text = password;
}

- (void)dealloc {
	[usernameField release];
	[passwordField release];
	[submitButton release];
	[cancelButton release];
	[username release];
	[password release];
  [super dealloc];
}

- (IBAction)submit:(id)sender {
	self.username = usernameField.text;
	self.password = passwordField.text;
	[self authenticate];
}

- (IBAction)cancel:(id)sender {
	[self finishAuthenticationWithSuccess:NO];
}

- (void)authenticate {
	if (username.length > 0 && password.length > 0) {
		[self startAuthentication];
	} else {
		[self failWithMessage:@"Please enter your username and password"];
	}
}

- (void)startAuthentication {
	[self showAuthenticationSheet];
	NSURL *url = [NSURL URLWithString:@"http://twitter.com/account/verify_credentials.xml"];
	NSURLRequest *request = [NSURLRequest requestWithURL:url];
	[NSURLConnection connectionWithRequest:request delegate:self];
}

//----- AuthenticationController.m -----

The authenticate method verifies that the username and password fields are not left blank and starts the authentication
request. To keep it simple we will use a NSURLConnection that will perform a HTTP Basic Auth. The following methods are
helper methods and their names describe most of their functionality:

//----- AuthenticationController.m -----

- (void)failWithMessage:(NSString *)theMessage {
	[self dismissAuthenticationSheet];
	[self presentLogin];
	[usernameField becomeFirstResponder];
	UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"" message:theMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
	[alert show];
	[alert release];
}

- (void)showAuthenticationSheet {
	authSheet = [[UIActionSheet alloc] initWithTitle:@"Authenticating, please wait..." delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
	UIView *currentView = viewController.modalViewController ? viewController.modalViewController.view : viewController.view;
	[authSheet showInView:currentView];
	[authSheet release];
}

- (void)dismissAuthenticationSheet {
	[authSheet dismissWithClickedButtonIndex:0 animated:YES];
	authSheet = nil;
}

- (void)presentLogin {
	if (viewController.modalViewController == self) return;
	[viewController presentModalViewController:self animated:YES];
}

- (void)dismissLogin {
	if (viewController.modalViewController != self) return;
	[viewController dismissModalViewControllerAnimated:YES];
}

- (void)finishAuthenticationWithSuccess:(BOOL)success {
	if (success) { // save credentials
		NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
		[defaults setValue:username forKey:kUsernameDefaultsKey];
		[defaults setValue:password forKey:kPasswordDefaultsKey];
		[defaults synchronize];
	}
	[self dismissAuthenticationSheet];
	[self dismissLogin];
	[target performSelector:selector withObject:success];
}

//----- AuthenticationController.m -----

In case of success the finishAuthenticationWithSuccess: method saves the users credentials. After that the selector gets
called with the boolean value telling the target whether the authentication was successful or not. The following part are
NSURLConnection delegation methods that perform the verification. Depending on the scenario in your application this
part is likely to be replaced by what you are actually using to verifythe users credentials.

//----- AuthenticationController.m -----

- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
	if ([challenge previousFailureCount] == 0) {
		NSURLCredential *credential = [[NSURLCredential alloc] initWithUser:username password:password persistence:NSURLCredentialPersistenceForSession];
		[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
		[credential release];
	} else {
		[challenge.sender cancelAuthenticationChallenge:challenge];
	}
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
	[self finishAuthenticationWithSuccess:YES];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
	[self failWithMessage:@"Please ensure that you are connected to the internet and that your username and password are correct"];
}

@end

//----- AuthenticationController.m -----


Discussion
----------

This example stores the credentials in the NSUserDefaults and the password gets saved in clear text. You should think
about extending the code to use the keychain, so that the password is stored securely.