My app stores the user's credentials when he signs in or up. When the app gets started I check in didFinishLaunchingWithOptions if we have stored credentials. This works fine when starting the app by tapping on the App Icon or launching it from Xcode.

However, when the app gets killed in the background and relaunched by the system due to a location change update, the credential returned by defaultCredentialForProtectionSpace is nil. When I restart the app again normally, the credential is back again.

So when [launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey] is true, the NSURLCredential returned by the NSURLCredentialStorage is nil; when it's false, we get the expected credential.

Here's some code:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Other init things happen here, including setting up the NSURLProtectionSpace

    NSURLCredentialStorage *credentialStorage = [NSURLCredentialStorage sharedCredentialStorage];
    NSURLCredential *credential = [credentialStorage defaultCredentialForProtectionSpace:self.protectionSpace];
    if (credential) {
        // do something - XXX this does not happen when app is launched in background
    }
} 
1

There are 1 answers

0
qingu On BEST ANSWER

It turns out the NSURLCredentialStorage sets kSecAttrAccessibleWhenUnlocked in the Keychain to store the credentials. That means the credentials can not be accessed when the user sets a password to unlock the phone and the phone is locked.

---EDIT---

The above explains why it's not working and here is the solution I ended up implementing: change the kSecAttrAccessible in the Keychain to kSecAttrAccessibleAfterFirstUnlock and deal with not being able to access the credentails in the rare cases when the phone got rebooted but has not been unlocked yet.

When the App is in the foreground, the Keychain is unlocked in the mode used by NSURLCredentialStorage: kSecAttrAccessibleWhenUnlocked. So in applicationDidBecomeActive: we can access the Keychain entries from NSURLCredentialStorage and make our changes:

- (void)applicationDidBecomeActive:(UIApplication *)application
{
    KeychainItemWrapper *wrapper = [[KeychainItemWrapper alloc] initWithIdentifier:@"server.com" accessGroup:nil];
    [wrapper setObject:(__bridge id)kSecAttrAccessibleAfterFirstUnlock forKey:(__bridge id)kSecAttrAccessible];

    // ...
}

The above code uses an adapted version of the KeychainItemWrapper. The original version is from Apple, but I based my solution on the ARCified version here: https://gist.github.com/dhoerl/1170641. You still have to change that version to work with kSecClassInternetPassword instead of kSecClassGenericPassword, which mainly means you can't use kSecAttrGeneric to query the Keychain items, but instead kSecAttrServer.

---EDIT---

The above method worked fine with iOS 7.x but does not work anymore on iOS 8.x. The code runs fine, but the Keychain still uses kSecAttrAccessibleWhenUnlocked.

The only solution now is to use NSURLCredentialStorage only when you know you will only use this in the foreground. Otherwise you need to store credentials yourself in the Keychain using the KeychainItemWrapper from https://gist.github.com/dhoerl/1170641. Then you can set kSecAttrAccessibleAfterFirstUnlock yourself when storing the credentials.

Example code:

KeychainItemWrapper *keychain = [[KeychainItemWrapper alloc] initWithIdentifier:@"YOUR_IDENTIFIER accessGroup:nil];
[keychain setObject:(__bridge id)kSecAttrAccessibleWhenUnlocked forKey:(__bridge id)kSecAttrAccessible];

[keychain setObject:userName forKey:(__bridge id)kSecAttrAccount];
[keychain setObject:[password dataUsingEncoding:NSUTF8StringEncoding] forKey:(__bridge id)kSecValueData];