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
}
}
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];