I'm using AFNetworking as a network layer for my iPhone app which connects to a Rails server that uses Devise for authentication. If I sign in (with a POST call) providing username/password then after that any GET I perform is ok.
If I close the app (not just background) then all my GET requests fail because I guess they're not authenticated.
So I presume cookies are stored somewhere; is there a way to save them in NSUserDefaults or somewhere like that in order to avoid logging in all the time?
You do not need to bother with NSUserDefaults nor any keychain wrapper if you use NSURLCredential.
Indeed NSURLCredential is much simpler to use, as it allows you to store both username and password in the keychain in two lines of code.
Your code would be something like that once the user is logged in:
NSURLCredential *credential;
credential = [NSURLCredential credentialWithUser:username password:password persistence:NSURLCredentialPersistencePermanent];
[[NSURLCredentialStorage sharedCredentialStorage] setCredential:credential forProtectionSpace:self.loginProtectionSpace];
Then, each time the app is launched, you can check whether your user was already logged in by searching for any credential in order to automatically log back your user (if need be):
NSURLCredential *credential;
NSDictionary *credentials;
credentials = [[NSURLCredentialStorage sharedCredentialStorage] credentialsForProtectionSpace:self.loginProtectionSpace];
credential = [credentials.objectEnumerator nextObject];
NSLog(#"User %# already connected with password %#", credential.user, credential.password);
You also need to clean the credential when the user wants to log out:
NSURLCredential *credential;
NSDictionary *credentials;
credentials = [[NSURLCredentialStorage sharedCredentialStorage] credentialsForProtectionSpace:self.loginProtectionSpace];
credential = [credentials.objectEnumerator nextObject];
[[NSURLCredentialStorage sharedCredentialStorage] removeCredential:credential forProtectionSpace:self.loginProtectionSpace];
loginProtectionSpace is created once for all. Please, note this sample code assumes there is only one credential in this space, which is usually the case unless you manage several accounts.
Here is an example of how you would create a NSURLProtectionSpace:
NSURL *url = [NSURL URLWithString:#"http://www.example.com"];
self.loginProtectionSpace = [[NSURLProtectionSpace alloc] initWithHost:url.host
port:[url.port integerValue]
protocol:url.scheme
realm:nil
authenticationMethod:NSURLAuthenticationMethodHTTPDigest];
Cookies are indeed automatically stored for the lifetime of your application for any subsequent requests on a particular server. A good strategy would be to store the username and password in the keychain or in NSUserDefaults like this:
// Setting
[[NSUserDefaults standardDefaults] setObject:username forKey:#"username"];
[[NSUserDefaults standardDefaults] synchronize];
// Getting
NSString *username = [[NSUserDefaults standardDefaults] objectForKey:#"username"];
You may want to use this in combination with AFHTTPClient to send your credentials along with every request in an Authorization HTTP header.
Related
I'm using the ios keychain (keychainItemWrapper / SSKeychain) to store my app's login token and maintain logged in state. Currently I store a simple NSDictionary in the keychain containing my token, a token expiry and a refresh token. I serialize it to NSData and storing using kSecValueData. I also set the kSecAttrAccount and kSecAttrService, but don't use those for auth.
This works great, about 95% of the time. The problem is that that randomly, unpredictably and sporadically, the keychain does not return data when I request it to retrieve the token. It is usually after a moderate time away from the app, when reopening it. It doesn't have to be from in background, or after any specific delay though.
It fails specifically when asking for my NSData below and returns <> instead of <ABCD EFGH IJKL ....>. I think it is nil. Thus the code thinks the user isn't logged in and drops them immediately on my App's Signup/Login landing page, without logout error, token expiry error, etc. If I minimize the app, then reopen, it almost always gets the correct keychain info and the user is logged in again.
This creates a confusing experience when encountered. It also means the user can't maintain this true 100% logged in state, with occasionally being randomly logged out. I've been unable to predict it or debug it and changing keychain libraries, as shown below, hasn't fixed it for me. It happens for me, and several TestFlight users, and in our production app currently.
Any suggestions how to maintain keychain integrity and loading 100% of time? We're about ready to implement an NSUserDefaults backup storage on the token to use in these cases, something I really don't want to do to store an auth token.
Storing:
// load keychain
KeychainItemWrapper *keychainItem = [KeychainItemWrapper keyChainWrapperForKeyID:kcIdentifier];
NSString *firstLaunch = [keychainItem objectForKey: (__bridge id)(kSecAttrAccount)];
if (firstLaunch == nil){
// initialize if needed
[keychainItem setObject:email forKey: (__bridge id)(kSecAttrAccount)];
[keychainItem setObject:kcIdentifier forKey: (__bridge id)kSecAttrService];
[keychainItem setObject:(id)kSecAttrAccessibleAfterFirstUnlock forKey:(id)kSecAttrAccessible];
}
// serialize "auth" NSDictionary into NSData and store
NSString *error;
NSData *dictionaryData = [NSPropertyListSerialization dataFromPropertyList:auth format:NSPropertyListXMLFormat_v1_0 errorDescription:&error];
[keychainItem setObject:dictionaryData forKey:(id)kSecValueData];
Loading:
// after similar KeychainItemWrapper initialization as above
NSData *dictionaryData = [keychainItem objectForKey:(id)kSecValueData];
NSString *error;
NSDictionary *auth = [NSPropertyListSerialization propertyListFromData:dictionaryData mutabilityOption:NSPropertyListImmutable format:nil errorDescription:&error];
NSString *token = auth[#"access_token"];
I have also tried using the SSKeychain library CocoaPod that is widely available, and a wrapper around the keychain logic. It is a cleaner access but fails with the same issue. Here I'm just storing NSString values since there was no direct way to store NSData in the lib.
// store in keychain
[SSKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlock];
[SSKeychain setPassword:auth[#"access_token"] forService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_TOKEN];
[SSKeychain setPassword:auth[#"expires_at"] forService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_EXPIRES_AT];
[SSKeychain setPassword:auth[#"refresh_token"] forService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_REFRESH_TOKEN];
// load from keychain
[SSKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlock];
NSString *token = [SSKeychain passwordForService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_TOKEN];
NSString *expires_at = [SSKeychain passwordForService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_EXPIRES_AT];
NSString *refresh_token = [SSKeychain passwordForService:SSKEYCHAIN_SERVICE account:SSKEYCHAIN_REFRESH_TOKEN];
Keychain does have issues at the moment, and for quite a while really. It sounds like you're getting off lightly as usually when it breaks a force-quit of the app is required to bring it back to life.
One thing that helps is to access the keychain just once on the first request and then cache the result in memory, if it's already in memory then just return it from there.
If you can observe a specific error when this happens then trap it and retry or, as is the current case for some unfortunate apps, kill the app. Killing the app is actually the current guidance from Apple if you raise a tech ticket to discuss the issue with them.
The only other real solution is to encrypt the data and store it in a file, but then you have issues with encryption keys so this is little better than obfuscation against a keen attacker.
I'm new to Apigee and can't seem to wrap my head around it. I am familiar with implementing an iOS app that talks to a database via a webservice call. The call would involve passing back and forth JSON or variables though POST, GET, etc.
The user flow I envision is a lot like Facebook long term token storage. Here are the steps:
Type username and password to login.
The app will remember the access_token in the keychain.
The access_token will be used with any future requests such as updating profile. This way the user doesn't have re-login every time he/she is using the app.
Log out will clear all the token.
If the token is invalid or expired, the app will take the user back to login.
I've taken multiple routes and ended up getting stuck on all of them when it comes to Apigee.
ROUTE 1
I made a call to logInUser and receive access_token in return.
[self.apigeeDataClient logInUser:username password:password];
All this is good until I want to update user's email address using the code below.
NSMutableDictionary *requestDict = [NSMutableDictionary dictionary];
[requestDict setObject:email forKey:kDataEmail];
NSString *url = [NSString stringWithFormat:#"%#/%#/%#/users/%#?access_token=%#", BASE_URL, UG_ORG_NAME, APP_NAME, [userData objectForKey:kDataUUID], self.accessToken];
NSString *op = #"PUT";
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:[NSDictionary dictionaryWithDictionary:requestDict]
options:0
error:&error];
[self.apigeeDataClient apiRequest:url operation:op data:[NSString stringWithUTF8String:[jsonData bytes]]];
It seems that every other time it's giving me "No content to map to Object due to end of input" error. I checked out this thread but have no luck. I made sure the JSON object is not null. I tried changing the operation from PUT to POST and GET. None of which update the email address in the database.
ROUTE 2
Instead of using apiRequest, I switched to updateEntity.
NSMutableDictionary *requestDict = [NSMutableDictionary dictionary];
[requestDict setObject:email forKey:kDataEmail];
[requestDict setObject:kDataUsers forKey:kDataType];
[requestDict setObject:self.accessToken forKey:kDataAccessToken];
NSString *entityID = [userData objectForKey:kDataUUID];
[self.apigeeDataClient updateEntity:entityID entity:requestDict];
It looks promising except I started getting "Subject does not have permission" like the issue described in this thread. I tried calling assignPermissions like mentioned in Apigee document but that didn't solve the problem. I even provide access_token with the call, even though I shouldn't have to.
In the attempt to avoid calling login also tried calling storeOAuth2TokensInKeychain and retrieveStoredOAuth2TokensFromKeychain mentioned here. That didn't work either.
The only thing way to resolve this error is by calling logInUser before making a call to updateEntity. This means the user will have to login every time he/she wants to use the app. I know I can store username/password in the keychain. But before I do that I'm wondering if there's better solution out there.
I know it's a long post. So thank you for reading this far. Any pointers are greatly appreciated.
I am working on an iPhone/iOS app. It has a login page with two text fields "email" & "Password".Now i want to save email.
i.e When I logged out it should remember the email address so it doesn't have to be input again.
Please tell me how can I save email.
If you don't consider email sensitive. Use NSUserDefaults...
Saving
[[NSUserDefaults standardUserDefaults]setObject:#"example#email.com" forKey:#"email"];
[[NSUserDefaults standardUserDefaults]synchronize];
Retrieving
// getting an NSString
NSString *emailString = [[NSUserDefaults standardUserDefaults] stringForKey:#"email"];\
IMPROVE ANSWER
If you consider email sensitive. Use keychain instead.
To store
KeychainItemWrapper *keychain =
[[KeychainItemWrapper alloc] initWithIdentifier:#"MyAppLoginData" accessGroup:nil];
[keychain setObject:loginStr forKey:(id)kSecAttrAccount];
[keychain setObject:pwdStr forKey:(id)kSecValueData];
To query
NSString *login = [keychain objectForKey:(id)kSecAttrAccount];
NSString *pwd = [keychain objectForKey:(id)kSecValueData];
To Delete:
[keychain resetKeychainItem];
To do this you will first need to add KeychainItemWrapper in you project.
Another important aspects of using keychain to store data is
The data is persistent even after app uninstall-install
Data can be shared across apps too. Read more here
In this case email is a part of user credentials, needed to login. So i would say it is sensitive information. Please do not ever use NSUserDefaults for storing sensitive data, that belongs to user. These defaults are stored as plist in binary format, they are not encrypted. Use Keychain Services API
provided by iOS instead.
I'm authenticating to Firebase with FirebaseSimpleLogin and Email/Password authentication in iOS. It seems that making the [authClient loginWithEmail:username andPassword:password withCompletionBlock:^(NSError *error, FAUser *user) { ... }]; takes roughly 5-8 seconds to complete.
Is there a way to speed up the login, like caching the authToken from FAUser, and using starting to use that directly in the first Firebase call?
Update:
It seems that storing the authToken after a successful login to NSUserDefaults:
[[NSUserDefaults standardUserDefaults] setValue:user.authToken forKey:USERDEFAULTS_LOGIN_TOKEN];
[[NSUserDefaults standardUserDefaults] synchronize];
... and then doing an authWithCredential: call with the stored authToken on next login attempt:
NSString *authToken = [[NSUserDefaults standardUserDefaults] stringForKey:USERDEFAULTS_LOGIN_TOKEN];
if (authToken) {
NSLog(#"Firebase logging in with token...");
[[Mesh root] authWithCredential:authToken withCompletionBlock:^(NSError *error, id data) { ...
... isn't any faster. Is there another way to speed up the login?
With the release of the Firebase iOS / OS-X Client v1.2.0, Firebase caches the local client authentication state and greatly optimizes the speed of re-authentication. Previous client versions required multiple server roundtrips before the client would enter an "authenticated" state, but this is now immediate if a valid, persisted session has been located on-disk.
Also note that Firebase Simple Login has been deprecated and replaced with a reimplementation of Firebase authentication that is enabled in the core Firebase client libraries. Check out https://www.firebase.com/docs/ios/guide/user-auth.html for the guides on how to get started with it on iOS.
When I used the core API I simply used the code
[dbsession updateAccessToken:#"..." accessTokenSecret:#"..." forUserId:#"..."];
to access my dropbox account from any copy of the app. But now I found out of this new Sync API that is easier and more flexible, but I didn't find any equivalent for the code displayed above. It now is:
DBAccountManager* accountMgr = [[DBAccountManager alloc] initWithAppKey:#"..." secret:#"..."];
[DBAccountManager setSharedManager:accountMgr];
??[DBAccountManager updateAccessToken:#"..." accessTokenSecret:#"..." forUserId:#"..."];??
How can I access my account? Where can I insert the AccessToken?
From your question, it seems that this method on DBAccountManager is the one for using your appKey and secret:
- (id)initWithAppKey:(NSString *)key secret:(NSString *)secret
From the documentation description, it says this method "...create[s] a new account manager with your app’s app key and secret. You can register your app or find your key at the apps page."
After you create an instance of DBAccountManager and set it to be the shared manager using [DBAccountManager setSharedManager:], you can login the specific user by calling this method:
[[DBAccountManager sharedManager] linkFromController:YOUR_ROOT_CONTROLLER];
Here's a description from the dropbox iOS tutorial:
"To start interacting with the Sync API, you'll need to create a DBAccountManager object. This object lets you link to a Dropbox user's account which is the first step to working with data on their behalf"
"...the linking process will switch to the Dropbox mobile app if it's installed. Once the user completes the authorization step, Dropbox will redirect them back to your app using the URL scheme you registered when setting up the SDK. Your app needs to handle those requests to complete the auth flow."
The final step as mentioned in the tutorial is to handle the redirect. Here's some code to do this:
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url sourceApplication:(NSString *)source annotation:(id)annotation {
DBAccount *account = [[DBAccountManager sharedManager] handleOpenURL:url];
if (account) {
NSLog(#"App linked successfully!");
return YES;
}
}
The user's account information can now be obtained through [DBAccountManager sharedManager].linkedAccount which is a DBAccount with properties like userId and accountInfo.
Here's a link to the docs for reference. Hope this helps!
Update
It seems I may have misunderstood your question. I am giving you instructions on how to use the Sync API and didn't quite clarify that there is actually no place for a user's accessToken in the API. This has been replaced with the web flow that I describe above.
You can achieve what you want by generating a callback url that dropbox uses in the sync API. First you need to set the dropbox.sync.nonce user setting to match whatever you pass in as the state parameter in the NSURL. Then set the oauth_token, oauth_token_secret, and uid params with what you used to pass into [DBAccountManager updateAccessToken:#"..." accessTokenSecret:#"..." forUserId:#"..."];. See below:
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:#"9b0aa24b0bd50ce3a1a904db9d309c50"
forKey:#"dropbox.sync.nonce"];
[userDefaults synchronize];
NSURL *url =
[NSURL URLWithString:#"db-APP_KEY://1/connect?
oauth_token=updateAccessToken&
oauth_token_secret=accessTokenSecret&
uid=forUserId&
state=9b0aa24b0bd50ce3a1a904db9d309c50"];
[[UIApplication sharedApplication] openURL:url];
Notice how the state parameter is the same as the value stored in the user defaults. Keep in mind this is undocumented and may change in a later API version.