3
votes

I'm sending http requests with session-cookie stored on the device side. The problem is that I want to persist it across app termination. But it seems like all the cookies of my app get deleted. I tried on both the simulator and device and they got the same behavior.

Is there any iOS way to prevent this cookie deletion? If not, how can I save it to disk and recover it back?

I'm planning to save this cookie in iOS keychain for security. And I think all NSHTTPCookie properties can be safely converted to NSString. So my current idea is to convert from NSHTTPCookie -> NSDictionary -> String -> Save in Keychain, and go backward to use get the original cookie. The problem is I don't want to go through the hassles of converting NSDictionary -> String and parsing String -> NSDictionary.

3
You could save and send the cookies yourself.Tom Irving
Yes, but does iOS provide this mechanism?Hlung
Kinda. If you've got a NSHTTPURLResponse, you can get an NSArray of NSHTTPCookies with NSHTTPCookie's +cookiesWithResponseHeaderFields:forURL: method. Then to send them, you just gotta create the Cookie: header yourself. I'll post more details in an answer.Tom Irving

3 Answers

2
votes

Found a nice clean way of doing this. The trick is using JSONKit (which came with RestKit) to convert between NSString and NSDictionary using JSON format. And here I use SFHFKeychainUtils to help me with the keychain stuff. This way you don't have to worry about each property in the cookie and all the conversion work. :)

#import "RestKit/JSONKit.h"

#define WSC_serviceName @"WSC_serviceName"
#define WSC_username    @"WSC_username"
#define WSC_cookieName  @"_your_cookie_name" // name of the session cookie

- (void)saveSessionCookieToKeyChain {
    for (NSHTTPCookie *c in [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies) {
        if ([c.name isEqualToString:WSC_cookieName]) {
            NSString *cookieString = [c.properties JSONString];
            NSLog(@"cookieString: %@",cookieString);
            NSError *saveError = nil;
            [SFHFKeychainUtils storeUsername:WSC_username
                                 andPassword:cookieString 
                              forServiceName:WSC_serviceName 
                              updateExisting:YES error:&saveError];
        }
    }
}

- (BOOL)readsSessionCookieFromKeyChain {
    NSError *readError = nil;
    NSString *jsonString = [SFHFKeychainUtils getPasswordForUsername:WSC_username
                                                      andServiceName:WSC_serviceName 
                                                               error:&readError];
    if (!jsonString) return NO;
    NSData* jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; // be sure that data is in UTF8 or it won't work
    JSONDecoder* decoder = [[JSONDecoder alloc] initWithParseOptions:JKParseOptionNone];
    NSDictionary* jsonDict = (NSDictionary*)[decoder objectWithData:jsonData];
    NSLog(@"jsonDict: %@",jsonDict);

    NSHTTPCookie *cookie = [[NSHTTPCookie alloc] initWithProperties:jsonDict];
    [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];

    return cookie!=nil;
}

- (void)clearSessionCookieAndRemoveFromKeyChain {
    for (NSHTTPCookie *c in [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies) {
        [[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:c];
    }
    NSError *deleteError = nil;
    [SFHFKeychainUtils deleteItemForUsername:WSC_username 
                              andServiceName:WSC_serviceName 
                                       error:&deleteError];
}
1
votes

Assuming you have an NSHTTPURLResponse, you can get an array of cookies like so:

NSDictionary * headers = [(NSHTTPURLResponse *)response allHeaderFields];
NSArray * cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers forURL:response.URL];

Where response is the NSHTTPURLResponse.

You're going to get NSURLResponses in these 2 methods of the NSURLConnectionDelegate

- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;

NSHTTPCookie has a properties property which returns an NSDictionary, so you can save them. They can then be created with the same dictionary using -initWithProperties:

To send them, you'll need to create your own string for the Cookie header of a NSURLRequest. Something like:

NSMutableString * cookieString = [[NSMutableString alloc] init];
for (NSHTTPCookie * cookie in myLoadedCookies){
    [cookieString appendFormat:@"%@=%@; ", cookie.name, cookie.value];
}
[request setValue:cookieString forHTTPHeaderField:@"Cookie"];
[cookieString release];

Where request is an NSMutableURLRequest.

You should also make sure to stop iOS managing cookies itself:

[request setHTTPShouldHandleCookies:NO];
1
votes

===== SUMMARY =====

Well, I'm going to create another answer just to summarize possible ways to persist a NSDictionary (that doesn't contain objects of course!).

The easiest way to do this is to use NSDictionary's writeToFile: method. This will generate a plist file. (Also available in NSArray) If you're worried about security, encrypt the array to an NSData object and write that.

Another way, not the fastest one though, is to use a tool to convert NSDictionary to a string format such as JSON or XML. You can save this string in iOS keychain and retrieve it later easily. (SFHFKeychainUtils is a great for helping with keychain stuff). A good thing about using keychain is it is automatically encrypted.