by Chadwick Wood
November 16th, 2010
ASIHTTPRequest is a wonderful Objective-C library for simplifying web request programming in your iOS (and OS X) apps. It's my go-to library for web server communication in iPhone apps. But, it's taken me awhile to get my own system for using it within apps that communicate with a server that needs authentication. In this article, I'll show you how to create your own subclass of ASIHTTPRequest to further simplify constructing requests to your API, including logging in and out.
The example I'm going to use here is the subclass I created for using in the new version of Sphericle, so I've called it SphHTTPRequest. Here's the header for our subclass:
#import
#import "ASIFormDataRequest.h"
@interface SphHTTPRequest : ASIFormDataRequest {
}
- (id)initWithPath:(NSString *)path;
+ (NSURL *)apiURLWithPath:(NSString *)path;
+ (NSString *)username;
+ (BOOL)loggedIn;
+ (void)storeUsername:(NSString *)u password:(NSString *)p;
+ (void)logout;
@end
I'll cover what the different methods do next, but here I just want to point out that I'm actually subclassing ASIFormDataRequest. I do this because it makes it easier to make my POST requests with data using the setPostValue:forKey: method. Note: Because of this choice, if you want your request method to be something other than POST (e.g. GET, PUT, DELETE), you'll need to set the requestMethod property on the request after you make any calls to setPostValue (because the implementation of setPostValue actually changes the request method to POST when called). This is especially important when you're making a PUT request (e.g. to make a record update to a RESTful Rails server).
In Sphericle, every request I'm making is to the same server. And what's more, when I'm testing, that server is different from the production server. So, it makes sense to centralize how requests are constructed, to make it easy to switch between servers, and to make our code less verbose. For this purpose, we have the following two methods:
+ (NSURL *)apiURLWithPath:(NSString *)path {
return [NSURL URLWithString:[NSString stringWithFormat:@"%@%@",@"http://yourserver.com/",path]];
}
- (id)initWithPath:(NSString *)path {
if (self = [super initWithURL:[SphHTTPRequest apiURLWithPath:path]]) {
self.shouldPresentAuthenticationDialog = YES;
self.useKeychainPersistance = YES;
}
return self;
}
The apiURLWithPath: method gives us an easy way to construct a URL by just passing a path string (e.g. @"users" or @"users/3"). If you need to switch the server your app is talking to, just replace @"http://yourserver.com/" with the new server URL. It makes sense to actually move that string out to a #define, or even a property list, but I'll leave that to you.
The initWithPath: method uses the apiURLWithPath: to give us a quick way to construct a request to our server by just furnishing the path. In it, I also set a couple of properties that I want for all of my requests as well (use the default authentication dialog, and store username/password info in the keychain).
One confusing point for me with ASIHTTPRequest was how to use it to figure out whether the user is currently logged in or not, and if so, what the current user's username is. This information could be stored separately in your app, but I wanted to do as little of that as possible, so I came up with the following:
+ (NSString *)username {
NSURL *url = [self apiURLWithPath:@""];
NSURLCredential *authenticationCredentials = [self savedCredentialsForHost:[url host]
port:[[url port] intValue]
protocol:[url scheme]
realm:SPH_LOGIN_REALM];
if (authenticationCredentials)
return [authenticationCredentials user];
else
return nil;
}
+ (BOOL)loggedIn {
return ([self username] != nil);
}
The username method will return the username that is stored in the keychain by ASIHTTPRequest for the current user. The loggedIn method will return true or false based on whether a stored username exists.
Please note that this method returning true doesn't exactly mean that your user is currently logged in; what it means is that there is a username for your server stored in the keychain. This username might not be paired with a valid password. But, in my app I write separate code to handle a login failure, which will remove any invalid stored username/password pairs, so this approach works for me. A little more on that when I get to the logout method...
You're probably wondering what SPH_LOGIN_REALM is. That's a string that matches the authentication realm that your server specifies when initiating basic HTTP authentication. Honestly, I get a little confused on this part, but basically this string will be the one that you see in the browser-based authentication dialog that you'll see if you try logging in to your server via a browser.
Never store your users' passwords as plain text. That's an amateur move. The better approach is to let the keychain store that info for you. To that end, I created a method (storeUsername:password:) that will take a username and password and store it to the keychain. This method is called when a user first logs in, or signs up. In your controller code, you grab the username/password values and call this method with them, and they will be kept safe. Here's the code:
+ (void)storeUsername:(NSString *)u password:(NSString *)p {
NSURL *url = [self apiURLWithPath:@""];
NSURLCredential *credentials = [NSURLCredential credentialWithUser:u password:p persistence:NSURLCredentialPersistencePermanent];
[self saveCredentials:credentials forHost:[url host] port:[[url port] intValue] protocol:[url scheme] realm:SPH_LOGIN_REALM];
}
Again, you'll note the usage of that same SPH_LOGIN_REALM for designating how the username/password pair should be stored. Once you've stored the username and password this way, any future requests that require authentication will be automatically furnished with login info. Magic.
The last bit of authentication logic needed is a way to log out. Within the context of your app, logging out will probably have requirements in many places, but this logout method handles the task of removing the stored username/password pair from the keychain, so that when your user logs out, they can be assured that their authentication information is no longer stored:
+ (void)logout {
// clear saved credentials
[self clearSession];
// remove all credentials
NSURL *url = [self apiURLWithPath:@""];
NSURLProtectionSpace *protectionSpace = [[[NSURLProtectionSpace alloc] initWithHost:[url host]
port:[[url port] intValue]
protocol:[url scheme]
realm:SPH_LOGIN_REALM
authenticationMethod:NSURLAuthenticationMethodDefault] autorelease];
NSURLCredential *credential;
while (credential = [[NSURLCredentialStorage sharedCredentialStorage] defaultCredentialForProtectionSpace:protectionSpace]) {
[[NSURLCredentialStorage sharedCredentialStorage] removeCredential:credential forProtectionSpace:protectionSpace];
}
}
Basically, this method clears the current session and then looks for all authentication info stored in the keychain that matches our API server, and deletes it. The while loop is needed because multiple username/password combos can be stored. My intention with this logout method is to erase all of them (your needs may differ).
This demonstration is just how I use ASIHTTPRequest, and it may not be for everyone. There are certainly other approaches possible, and improvements that could be made on this approach. It's still a learning process for me.
If you have any questions about any of this, or suggestions on how it could be improved (or how you disagree with it entirely), I'd love to hear them in the comments.