iPhone: Subclass ASIHTTPRequest for Your App's Login System

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.

Create Your Subclass

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 <Foundation/Foundation.h>
#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).

Simplifying Request Construction

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).

Am I Logged In?

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.

Storing Login Credentials

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.

Logging Out

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).

Your Mileage May Vary

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.

Comments

Anonymous's picture

Can you please give a link or working example for digest http authentication.

I'm trying to connecty to a soap web service but diauthenticationChallenge is not getting called at all.

i need to pass three parameters to do authentication.parameters are username,password and authentication type

jemeador's picture

I found this tutorial very helpful. You've saved me a lot of trouble by posting this! I would like to see how you handle invalid logins if you use your own login form and just some other general information on how you tie all this together in your app.

Chadwick Wood's picture

Anonymous, are you making sure to set the delegate property of your ASIHTTPRequest object? The best example I can think of for doing basic HTTP authentication is in the ASIHTTPRequest docs here.

jemeador, I might be able to write a follow-up post later about the points your bring up, but that stuff gets into application-specific code and is a little harder to write succinctly about. In short, one thing I'm doing to handle invalid logins is just to check the response code after trying to authenticate, then showing any appropriate error message using UIAlertView.

Anonymous's picture

Example or it didn't happen.

Anonymous's picture

Stupid and rude. If you can't figure it out, ask nicely...

john 's picture

please send me the working code for username and password

Ron's picture

Really nice work. I was hoping to see it working. atm im doing a login controller using asihttprequest if you can send me a demo will be appreciate, thanks for the article.