iPhone: NSHTTPCookie is not saved across app restarts
Asked Answered
I

7

32

In my iPhone app, I want to be able to reuse the same server-side session when my app restarts. A session on the server is identified by a cookie, which is sent on each request. When I restart the app, that cookie is gone and I can't use the same session anymore.

What I noticed when I used the NSHTTPCookieStorage to look up the cookie I got from the server, is that [cookie isSessionOnly] returns YES. I get the impression that this is why cookies are not saved across restarts of my app. What would I have to do to make my cookie NOT session only? What HTTP headers do I have to send from the server?

Intravenous answered 18/4, 2010 at 14:27 Comment(0)
E
45

You can save the cookie by saving its properties dictionary and then restoring as a new cookiebefore you go to re-connect.

Save:

NSArray* allCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:[NSURL URLWithString:URL]];
for (NSHTTPCookie *cookie in allCookies) {
    if ([cookie.name isEqualToString:MY_COOKIE]) { 
        NSMutableDictionary* cookieDictionary = [NSMutableDictionary dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults] dictionaryForKey:PREF_KEY]];
        [cookieDictionary setValue:cookie.properties forKey:URL];
        [[NSUserDefaults standardUserDefaults] setObject:cookieDictionary forKey:PREF_KEY];
    }
 }

Load:

NSDictionary* cookieDictionary = [[NSUserDefaults standardUserDefaults] dictionaryForKey:PREF_KEY];
NSDictionary* cookieProperties = [cookieDictionary valueForKey:URL];
if (cookieProperties != nil) {
    NSHTTPCookie* cookie = [NSHTTPCookie cookieWithProperties:cookieProperties];
    NSArray* cookieArray = [NSArray arrayWithObject:cookie];
    [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookieArray forURL:[NSURL URLWithString:URL] mainDocumentURL:nil];
}
Enzootic answered 9/5, 2011 at 15:17 Comment(2)
do you need to synchronize the nsuserdefaults?Gama
You only need to synchronize if you need to save them right then. Otherwise they will be saved at some interderminate time later. Here's the doc page: developer.apple.com/library/mac/#documentation/Cocoa/Reference/…Enzootic
A
28

I have upvoted @TomIrving's answer and am elaborating here because many users will not see the very important comment in which he says:

"You need to set an expiration date, otherwise the cookie is assumed to be session only."

Basically, the cookie will be deleted when you close your app UNLESS the cookie has an expiration date in the future.

You don't need to store and restore the cookies in and from NSUserDefaults if you have control over the server and can ask it to set the "Expires" header to something in the future. If you don't have control over the server or do not wish to override your server's behavior, you can 'trick' your app by changing the expiresDate from within it:

When you reopen your app, you'll notice that the cookie has not been deleted.

Azedarach answered 14/4, 2014 at 12:25 Comment(1)
From a server-side security approach, this should be the accepted answer.Pettit
L
5

Session-only cookies will expire by their nature. You can store them manually in Keychain if you really want it. I prefer Keychain to saving in UserDefaults or archiving because cookies are better be secured, just like user's password.

Unfortunately saving session-only cookies is not very helpful, the code below is just an illustration how to store cookies, but can't force the server to accept such cookies in any way (unless you can control the server).

Swift 2.2

// Saving into Keychain
if let cookies = NSHTTPCookieStorage.sharedHTTPCookieStorage().cookies {
    let cookiesData: NSData = NSKeyedArchiver.archivedDataWithRootObject(cookies)
    let userAccount = "some unique string to identify the item in Keychain, in my case I use username"
    let domain = "some other string you can use in combination with userAccount to identify the item"           
    let keychainQuery: [NSString: NSObject] = [
                        kSecClass: kSecClassGenericPassword,
                        kSecAttrAccount: userAccount + "cookies", 
                        kSecAttrService: domain,
                        kSecValueData: cookiesData]
    SecItemDelete(keychainQuery as CFDictionaryRef) //Trying to delete the item from Keychaing just in case it already exists there
    let status: OSStatus = SecItemAdd(keychainQuery as CFDictionaryRef, nil)
    if (status == errSecSuccess) {
        print("Cookies succesfully saved into Keychain")
    }
}

// Getting from Keychain
let userAccount = "some unique string to identify the item in Keychain, in my case I use username"
let domain = "some other string you can use in combination with userAccount to identify the item"
let keychainQueryForCookies: [NSString: NSObject] = [
                             kSecClass: kSecClassGenericPassword,
                             kSecAttrService: domain, // we use JIRA URL as service string for Keychain
                             kSecAttrAccount: userAccount + "cookies",
                             kSecReturnData: kCFBooleanTrue,
                             kSecMatchLimit: kSecMatchLimitOne]
var rawResultForCookies: AnyObject?
let status: OSStatus = SecItemCopyMatching(keychainQueryForCookies, &rawResultForCookies)
if (status == errSecSuccess) {
    let retrievedData = rawResultForCookies as? NSData
    if let unwrappedData = retrievedData {
        if let cookies = NSKeyedUnarchiver.unarchiveObjectWithData(unwrappedData) as? [NSHTTPCookie] {
            for aCookie in cookies {
                NSHTTPCookieStorage.sharedHTTPCookieStorage().setCookie(aCookie)
            }
        }
    }
}
Leonie answered 23/7, 2016 at 17:21 Comment(0)
M
3

I believe it's up to the server to decide whether or not the cookie is session-only, you can't do anything about it.

Mesothelium answered 18/4, 2010 at 16:36 Comment(7)
No, it's really the client (iPhone app) that throws the session cookie away. The session still exists on the server.Intravenous
Right, but the server will tell the iPhone app that the cookie is session only.Mesothelium
Yes, and that's through HTTP. So I'm looking for the correct headers to send from the server, to tell the iPhone app that it can keep the cookie around longer.Intravenous
Oh, I see what you're looking for now, I apologise. How are you setting the cookie at the moment?Mesothelium
No problem :) I have a Java application running in Tomcat, which generates the following HTTP header to set the cookie on the client -> Set-Cookie: JSESSIONID=3E576D79DED4DAEDCB5E24A674AA19C7; Path=/Intravenous
You need to set an expiration date, otherwise the cookie is assumed to be session only.Mesothelium
As explained by many folks, the reason is not setting expiration date.Disentomb
A
2

Swift way

Store:

static func storeCookies() {
    let cookiesStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage()
    let userDefaults = NSUserDefaults.standardUserDefaults()

    let serverBaseUrl = "http://yourserverurl.com"
    var cookieDict = [String : AnyObject]()

    for cookie in cookiesStorage.cookiesForURL(NSURL(string: serverBaseUrl)!)! {
        cookieDict[cookie.name] = cookie.properties
    }

    userDefaults.setObject(cookieDict, forKey: cookiesKey)
}

Restore:

static func restoreCookies() {
    let cookiesStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage()
    let userDefaults = NSUserDefaults.standardUserDefaults()

    if let cookieDictionary = userDefaults.dictionaryForKey(cookiesKey) {

        for (cookieName, cookieProperties) in cookieDictionary {
            if let cookie = NSHTTPCookie(properties: cookieProperties as! [String : AnyObject] ) {
                cookiesStorage.setCookie(cookie)
            }
        }
    }
}
Allynallys answered 27/1, 2016 at 9:5 Comment(1)
This answer (storing your cooking in plain text) is insecure. Please use the keychain and not NSUserDefaults like this answer.Chengtu
I
1

Swift 5 version

func storeCookies() {

    guard let serverBaseUrl = URL(string: Constants.baseURL) else {
        return
    }

    let cookiesStorage: HTTPCookieStorage = .shared

    var cookieDict: [String: Any] = [:]

    cookiesStorage.cookies(for: serverBaseUrl)?.forEach({ cookieDict[$0.name] = $0.properties })

    let userDefaults = UserDefaults.standard
    userDefaults.set(cookieDict, forKey: Constants.cookiesKey)
}

func restoreCookies() {

    let cookiesStorage: HTTPCookieStorage = .shared

    let userDefaults = UserDefaults.standard

    guard let cookieDictionary = userDefaults.dictionary(forKey: Constants.cookiesKey) else {
        return
    }

    let cookies = cookieDictionary
        .compactMap({ $0.value as? [HTTPCookiePropertyKey: Any] })
        .compactMap({ HTTPCookie(properties: $0) })

    cookiesStorage.setCookies(cookies, for: URL(string: Constants.baseURL), mainDocumentURL: nil)
}
Idiomorphic answered 30/8, 2019 at 13:7 Comment(0)
T
0

SWIFT 3

SAVE:

if let httpResponse = response as? HTTPURLResponse, let fields = httpResponse.allHeaderFields as? [String : String] {
            let cookies = HTTPCookie.cookies(withResponseHeaderFields: fields, for: response.url!)
            HTTPCookieStorage.shared.setCookies(cookies, for: response.url!, mainDocumentURL: nil)
            for cookie in cookies {
                if cookie.name == cookieName{
                    if cookieName == Constants.WS.COOKIES.COOKIE_SMS {
                        UserDefaults.standard.set(NSKeyedArchiver.archivedData(withRootObject: cookie), forKey: Constants.SHARED_DEFAULT.COOKIE_SMS)
                        UserDefaults.standard.synchronize()
                    }

                    return cookie.value
                }
            }
        }

GET:

let cookie: HTTPCookie = NSKeyedUnarchiver.unarchiveObject(with: UserDefaults.standard.object(forKey: Constants.SHARED_DEFAULT.COOKIE_SMS) as! Data) as! HTTPCookie
        HTTPCookieStorage.shared.setCookie(cookie)
Transcurrent answered 10/10, 2016 at 14:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.