Switch from unauth to developer authenticated cognito user - AWS iOS SDK - ios

Overall Problem:
I have a problem using a developer authenticated identity with my front end (iOS). I know my backend produces the correct token and identityID but my refresh method never gets called. I've also looked at the sample but I get slightly confused with everything going on.
Flow Explanation:
Currently I have a login screen that has a login button. The user presses the login button, then my api class takes the credentials, encrypts the password and stores it in keychain (commented out for now since it doesn't work on simulator). My DeveloperAuthenticatedIdentityProvider is called my app BusytimeAuthenticated. I have completed all the methods (I'm using AWS lambda and DynamoDB to authenticate users so) I start with unauthenticated access which allows me to access only two methods, login and signup. Then I want to assume my authenticated user which allows me to call my other methods.
my API Code:
[AWSLogger defaultLogger].logLevel = AWSLogLevelVerbose;
id<AWSCognitoIdentityProvider> identityProvider = [[BusytimeAuthenticated alloc] initWithRegionType:AWSRegionUSEast1
identityId:nil
identityPoolId:#"SOMEIDENTITYPOOLID"
logins:#{#"SOMEPROVIDERNAME": #"SOMEUSERNAME"}
providerName:#"SOMEPROVIDERNAME"
];
credentialsProvider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:AWSRegionUSEast1
identityProvider:identityProvider
unauthRoleArn:nil
authRoleArn:nil];
configuration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSEast1
credentialsProvider:self.credentialsProvider];
AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = configuration;
[[credentialsProvider refresh] continueWithBlock:^id(BFTask *task){
[self testAuth];
return nil;
}];
my DeveloperAuthenticatedIdentityProvider code (BusytimeAuthenticated) :
#import "BusytimeAuthenticated.h"
#interface BusytimeAuthenticated()
#property (strong, atomic) NSString *providerName;
#property (strong, atomic) NSString *token;
#end
#implementation BusytimeAuthenticated
#synthesize providerName=_providerName;
#synthesize token=_token;
- (instancetype)initWithRegionType:(AWSRegionType)regionType
identityId:(NSString *)identityId
identityPoolId:(NSString *)identityPoolId
logins:(NSDictionary *)logins
providerName:(NSString *)providerName{
if (self = [super initWithRegionType:regionType identityId:identityId accountId:nil identityPoolId:identityPoolId logins:logins]) {
self.providerName = providerName;
}
return self;
}
// Return the developer provider name which you choose while setting up the
// identity pool in the Amazon Cognito Console
- (BOOL)authenticatedWithProvider {
return [self.logins objectForKey:self.providerName] != nil;
}
// If the app has a valid identityId return it, otherwise get a valid
// identityId from your backend.
- (BFTask *)getIdentityId {
// already cached the identity id, return it
if (self.identityId) {
return [BFTask taskWithResult:nil];
}
// not authenticated with our developer provider
else if (![self authenticatedWithProvider]) {
return [super getIdentityId];
}
// authenticated with our developer provider, use refresh logic to get id/token pair
else {
return [[BFTask taskWithResult:nil] continueWithBlock:^id(BFTask *task) {
if (!self.identityId) {
return [self refresh];
}
return [BFTask taskWithResult:self.identityId];
}];
}
}
// Use the refresh method to communicate with your backend to get an
// identityId and token.
- (BFTask *)refresh {
if (![self authenticatedWithProvider]) {
return [super getIdentityId];
}else{
// KeychainWrapper *keychain = [[KeychainWrapper alloc]init];
AWSLambdaInvoker *lambdaInvoker = [AWSLambdaInvoker defaultLambdaInvoker];
NSDictionary *parameters = #{#"username" : #"SOMEUSERNAME",
#"password":#"SOMEENCRYPTEDPASS",
#"isError" : #NO};
NSLog(#"Here");
[[lambdaInvoker invokeFunction:#"login" JSONObject:parameters] continueWithBlock:^id(BFTask* task) {
if (task.error) {
NSLog(#"Error: %#", task.error);
}
if (task.exception) {
NSLog(#"Exception: %#", task.exception);
}
if (task.result) {
self.identityId = [task.result objectForKey:#"IdentityId" ];
self.token = [task.result objectForKey:#"Token" ];
// [keychain mySetObject:[task.result objectForKey:#"Token" ] forKey:#"Token"];
// [keychain mySetObject:[task.result objectForKey:#"IdentityId" ] forKey:#"IdentityId"];
NSLog(#"Result: %#", task.result);
}
return [BFTask taskWithResult:self.identityId];
}];
}
return NULL;
}
#end
Summary Problem:
Unfortunately when I test my new priveleges, I see from the error: "Unauth_Role/CognitoIdentityCredentials is not authorized to perform: lambda:InvokeFunction". Clearly I'm not switching properly. I've placed a breakpoint in my refresh method to see if it's getting called. It's not. I'm not quite understanding how I switch properly. Any help with getting this to work is much appreciated.
Note: One big change I did make though is I took out the "DeveloperAuthenticationClient" class because I assumed I could do it without it.

The fundamental problem is that you are trying to call a Lambda function (which requires credentials) to get credentials. Because you are using the "default" client configuration, when your developer authenticated client comes back with a response it is going to override the credentials used to access your Lambda function. Additionally, once that id has been transitioned to authenticated, you won't be able to use it to get credentials in an unauth flow and would need to generate a new unauthenticated id just to authenticate again and then get back to your authenticated id.
I would strongly encourage you to setup API Gateway in front of your Lambda function to remove this circular dependency.

Update based on new information in the question...
A few things here:
1. Avoid code like while(!finished) to wait on an async task to complete. In the best case, this style of busy waiting will consume a CPU/core at 100% while doing nothing useful and adversely affect battery life and will only hurt performance of your app. Instead, use a notification with a block. Since you have already have a AWSTask in this instance, instead of returning nil at the end of the [credentialsProvider refresh] continueWithBlock... just call your [self testAuth] right there and do away with the finished/while code.
2. In your getIdentityId implementation the first if condition checks if there is an identityId and if there is it returns nil. I'm guessing you goal here is to cache the identityId after a successful authentication and return that so that you don't have to call your backend every time getIdentityId is called. If that is the case, pretty sure you want to return identityId instead of nil
3. I don't think this is the cause of your issue but will simplify things: As long as you've configured your identity pool with Auth/UnAuth roles in the console, you don't have to explicitly use them when initializing the AWSCognitoCredentialsProvider.
Once these are resolved if you continue to have problems, please debug the code in more detail and tell us things like the following:
Does the refresh method get called? If so, which parts of your if statement does it enter and what is the result? Does it ever enter the else block and call your backend identity provider? Does it successfully retrieve an identity id and return it?
If you get further but start experiencing a slightly different issue then please mark this question answered and post a separate question instead of continuing to edit this question. This will help keep things clear (this question/answer is getting pretty long and has changed).
Original answer to initial posted question/code... The getIdentity method of the AWSCognitoCredentialsProvider returns an AWSTask (i.e. a BFTask). So you'll need to call something like continueWithBlock in order to actually execute the method. In the first block of code above it looks like you're not doing that.

Related

API Gateway generated SDK for iOS (Objective-C) with Cognito User Pool for authorized users

I have deployed an API using AWS API Gateway and I am trying to access it through an iOS device. Some endpoints support unauthorized users and I have no trouble accessing them but others don't and I can't manage to query them. I use the new Cognito User Pool feature for authentication which works fine when using a web application (or postman).
First, even though some of the endpoints are protected (like in the following picture of the console), when I deploy the API and generate the SDK for iOS (Objective-C), I can read in the README file: "All endpoints do not require authorization."
Then, when I run the following authentication code from AWS documentation, everything seems to work just fine:
AWSServiceConfiguration *serviceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionEUWest1 credentialsProvider:nil];
AWSCognitoIdentityUserPoolConfiguration *userPoolConfiguration = [[AWSCognitoIdentityUserPoolConfiguration alloc] initWithClientId:ClientId clientSecret:nil poolId:UserPoolId];
[AWSCognitoIdentityUserPool registerCognitoIdentityUserPoolWithConfiguration:serviceConfiguration userPoolConfiguration:userPoolConfiguration forKey:#"UserPool"];
AWSCognitoIdentityUserPool *pool = [AWSCognitoIdentityUserPool CognitoIdentityUserPoolForKey:#"UserPool"];
AWSCognitoIdentityUser *user = [pool getUser];
[[user getSession:username password:password validationData:nil] continueWithBlock:^id(AWSTask<AWSCognitoIdentityUserSession *> *task) {
if (task.error) {
NSLog(#"Could not get user session. Error: %#", task.error);
} else if (task.exception) {
NSLog(#"Could not get user session. Exception: %#", task.exception);
} else {
NSLog(#"Successfully retrieved user session data");
AWSCognitoIdentityUserSession *session = (AWSCognitoIdentityUserSession *) task.result;
NSMutableString *poolId = [[NSMutableString alloc] initWithString:#"cognito-idp.eu-west-1.amazonaws.com/"];
[poolId appendString:UserPoolId];
NSString *tokenStr = [session.idToken tokenString];
NSDictionary *tokens = [[NSDictionary alloc] initWithObjectsAndKeys:tokenStr, poolId, nil];
CognitoPoolIdentityProvider *idProvider = [[CognitoPoolIdentityProvider alloc] init];
[idProvider addTokens:tokens];
AWSCognitoCredentialsProvider *credentialsProvider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:AWSRegionEUWest1 identityPoolId:IdentityPoolId identityProviderManager:idProvider];
[credentialsProvider clearKeychain];
[credentialsProvider clearCredentials];
AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionEUWest1
credentialsProvider:credentialsProvider];
[[credentialsProvider getIdentityId] continueWithBlock:^id _Nullable(AWSTask<NSString *> * _Nonnull task) {
if (task.error) {
NSLog(#"Could not get identity id: %#", task.error);
} else if (task.exception) {
NSLog(#"Could not get identity id: %#", task.exception);
} else {
NSLog(#"Identity id: %#", task.result);
}
return nil;
}];
}
return nil;
}];
I implemented CognitoPoolIdentityProvider as specified in this post.
#interface CognitoPoolIdentityProvider ()
#property (strong, nonatomic) NSDictionary *tokens;
#end
#implementation CognitoPoolIdentityProvider
- (AWSTask<NSDictionary *> *)logins {
return [AWSTask taskWithResult:self.tokens];
}
- (void)addTokens:(NSDictionary *)tokens {
self.tokens = tokens;
}
#end
I manage to get a proper token (tested using postman) and a user id:
2016-12-27 12:43:35.760 AskHub[26625:10037234] AWSiOSSDK v2.4.11 [Debug] AWSURLResponseSerialization.m line:63 | -[AWSJSONResponseSerializer responseObjectForResponse:originalRequest:currentRequest:data:error:] | Response body:
{"IdentityId":"eu-west-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}
2016-12-27 12:43:35.766 AskHub[26625:10037234] Identity id: eu-west-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
However, when I run the following code to hit a protected endpoint, CloudFront considers that I am not authenticated.
APIHealthClient *apiClient = [APIHealthClient defaultClient];
APIAskHubRequest *initReq = [[APIAskHubRequest alloc] init];
initReq.message = #"FIN";
NSLog(#"Sending initial request");
[[apiClient webhooksAskhubPost:initReq] continueWithBlock:^id(AWSTask *task) {
if (task.error) {
NSLog(#"Could not send initial request: %#", task.error);
} else if (task.exception) {
NSLog(#"Could not send initial request: %#", task.exception);
} else {
NSLog(#"Successfully sent initial request");
}
return nil;
}];
Reponse:
2016-12-27 12:56:25.247 AskHub[26784:10046562] Could not send initial request: Error Domain=com.amazonaws.AWSAPIGatewayErrorDomain Code=1 "(null)" UserInfo={HTTPBody={
message = Unauthorized;
}, HTTPHeaderFields={type = immutable dict, count = 9,
entries =>
3 : Via = {contents = "X.X XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.cloudfront.net (CloudFront)"}
4 : x-amzn-ErrorType = {contents = "UnauthorizedException"}
5 : Content-Type = {contents = "application/json"}
6 : Content-Length = {contents = "27"}
7 : Connection = {contents = "keep-alive"}
8 : x-amzn-RequestId = {contents = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"}
9 : Date = {contents = "Tue, 27 Dec 2016 11:56:25 GMT"}
10 : X-Cache = {contents = "Error from cloudfront"}
11 : X-Amz-Cf-Id = {contents = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}
}
}
Am I missing something here? Does the automatically generated SDK support User Pool authentication?
Based upon the code I see, you are clearing credentials but never getting credentials. On quick review it looks like you are setting up the service configuration and authentication correctly, but you never call credentials.
If that was the case, even though your application would authenticate, the credentials provider would never go get the logins dictionary.
(You can verify this in the AWS Console, if you look at the identityId you are trying to use to access the resource, you can look at the "logins" count. If it is 0, you are not logged in.)
In order to be authenticated you need to have the credentials provider call your identityProviderManager "logins" method.
That call happens when you do "getCredentialsForIdentity" which is "credentials" in the SDK.
Typically the sequence is GetID followed by GetCredentialsForId. In the IOS SDK, the GetId happens with "getSession" (so you should have that), and the GetCredentials happens with "credentials"
(Note that there are different SDKs with different naming in each (Mobile Hub vs IOS SDK for example) but all that matters is that your credentials provider gets a login dictionary. With verbose logging you should actually see the "logins" dictionary logged, which will prove to you that a logins dictionary is being provided to the credentials provider).
I figured out that there is a confusion here between two types of authentication modes: using a credentials provider or just adding the JWT token to the "Authorization" header parameter (or your custom parameter name if you specified one when creating your authorizer).
The first method did not work for me, even though it is the only one that I have found in the documentation when it comes to using the iOS SDK. The second method works pretty fine, but to be able to use it without hacking into the SDK, don't forget to add the "Authorization" header when you specify your API in API Gateway.

Onedrive SDK - Authentication Token is getting nil value

During OneDrive Authentication using OneDrive SDK for iOS, after the authentication screen, At times, the highlighted values of “response” objects are NIL . I’m not sure why this is happening.
Out of 10 trials, 2-3 times the values inside the Repsonse object is NIL. So this is landing the App in crash.Any guess why this is happening? May be because of poor network? Or any other thing I might have missed? In Onedrive SDK - LiveAuthRequest.m file has the following lines of code.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
id response = [LiveAuthHelper readAuthResponse:self.tokenResponseData];
if ([response isKindOfClass:[LiveConnectSession class]])
{
_client.session = response; => Crash is Happening Here*
self.session = response;
[self updateStatus:AuthTokenRetrieved];
}
else
{
self.error = response;
[self updateStatus:AuthFailed];
}
self.tokenResponseData = nil;
self.tokenConnection = nil;
}
The Response from the Onedrive Server side. 1. authentication_Token, 2.refreshToken, 3.scope 4.expires_in 5.tokenType",
iOS version:5.1.1
OneDrive SDK Version:5.0

Quickblox user search from multiple sources

I have a requirement to search QuickBlox users whose IDs match their
Facebook IDs OR
Twitter IDs OR
Login IDs
So I am firing multiple QBUser queries at once, using following code:
[QBUsers usersWithLogins:[NSArray arrayWithObject:m_searchString] delegate:self];
[QBUsers usersWithFacebookIDs:[NSArray arrayWithObject:m_searchString] delegate:self];
[QBUsers usersWithTwitterIDs:[NSArray arrayWithObject:m_searchString] delegate:self];
So for example, if I give search string as "Testuser" - it should search all users having login = Testuser, FB login = Testuser, and Twitter login = Testuser.
Based on source of request (FB/Twitter/My own app), I need to put results in different UI parts.
The problem is, I can't differentiate which result comes back for which request.
-(void)completedWithResult:(Result*)result
{
[self showActivityIndicator:NO];
// QuickBlox User creation result
if([result isKindOfClass:[QBUUserPagedResult class]])
{
// Success result
if(result.success)
{
}
}
}
I can see that above code is hit 3 times. But I don't see anything in QBUUserPagedResult class that tells me from which request this result has come.
Something like a tag for request should suffice, but I am not sure what thing it is, looking at the documentation.
Is there anything I can use?
Alternately, is there another approach to what I am trying to achieve (instead of multiple requests)?
I figured it out that context option which is an NSString works like a tag for each Quickblox request:
[QBUsers usersWithLogins:[NSArray arrayWithObject:m_searchString] delegate:self context:MY_STRING_CONSTANT];
Then in delegate function:
- (void)completedWithResult:(Result *)result context:(void *)contextInfo
{
if(result.success && [result isKindOfClass:QBUUserPagedResult.class])
{
NSString * _context = (__bridge NSString *) contextInfo;
if([_context isEqualToString:MY_STRING_CONSTANT])
{
}
}
}
As simple as that, but the documentation doesn't say much about it, or it's not visible as it should be. I had to dig into their forums to figure it out.

Google OAuth Login Error: Invalid credentials

I have an iPad application which allows users to login to their Gmail account(s) using OAuth2. Thus far, the login process and email fetching is successful. However, when the app is closed and then re-opened after a (long) period of time, an error is produced "invalid credentials,' even though previous logins with the same credentials were successful.
Login Flow:
1) User logs in to gmail using OAuth 2.
2) User email address and oAuthToken provided by the GTMOAuth2Authentication object are saved to core data for future logins.
3) IMAP Session is created using saved email address and OAuthToken.
Here is the relevant code
Google Login
- (void)gmailOAuthLogin
{
NSDictionary *googleSettings = [[EmailServicesInfo emailServicesInfoDict] objectForKey:Gmail];
GTMOAuth2ViewControllerTouch *googleSignInController =
[[GTMOAuth2ViewControllerTouch alloc] initWithScope:GmailScope clientID:GmailAppClientID clientSecret:GmailClientSecret keychainItemName:KeychainItemName completionHandler:^(GTMOAuth2ViewControllerTouch *googleSignInController, GTMOAuth2Authentication *auth, NSError *error){
if (error != nil) {
//handle error
} else {
[[ModelManager sharedInstance] authenticateWithEmailAddress:[auth userEmail]
oAuthToken:[auth accessToken] imapHostname:[googleSettings objectForKey:IMAPHostName] imapPort:[[googleSettings objectForKey:IMAPPort]integerValue] smtpHostname:[googleSettings objectForKey:SMTPHostName] smtpPort:[[googleSettings objectForKey:SMTPPort]integerValue] type:EmailProtocolTypeImapAndSmtpGMail success:^(Account *account) {
//create IMAP session using above arguments
} failure:^(NSError *error) {
//handle error
}];
}
}];
[self presentGoogleSignInController:googleSignInController];
}
Create IMAP Session Using MailCore2
- (void)authenticateWithEmailAddress:(NSString *)emailAddress password:(NSString *)password oAuthToken:(NSString *)oAuthToken imapHostname:(NSString *)imapHostname imapPort:(NSInteger)imapPort smtpHostname:(NSString *)smtpHostname smtpPort:(NSInteger)smtpPort success:(void (^)())success failure:(void (^)(NSError *))failure
{
self.imapSession = [[MCOIMAPSession alloc] init];
self.imapSession.hostname = imapHostname;
self.imapSession.port = imapPort;
self.imapSession.username = emailAddress;
self.imapSession.connectionType = MCOConnectionTypeTLS;
self.imapSession.password = nil;
self.imapSession.OAuth2Token = oAuthToken;
self.imapSession.authType = nil != oAuthToken ? MCOAuthTypeXOAuth2 :
self.imapSession.authType;
[self.imapSession setConnectionLogger:^(void * connectionID, MCOConnectionLogType type,
NSData * data){
NSLog(#"MCOIMAPSession: [%i] %#", type, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
self.smtpSession = [[MCOSMTPSession alloc] init];
self.smtpSession.hostname = smtpHostname;
self.smtpSession.port = smtpPort;
self.smtpSession.username = emailAddress;
self.smtpSession.connectionType = MCOConnectionTypeTLS;
self.smtpSession.password = nil;
self.smtpSession.OAuth2Token = oAuthToken;
self.smtpSession.authType = nil != oAuthToken ? MCOAuthTypeXOAuth2 :
self.smtpSession.authType;
[self.smtpSession setConnectionLogger:^(void * connectionID, MCOConnectionLogType type, NSData * data){
NSLog(#"MCOSMTPSession: [%i] %#", type, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
[[self.imapSession checkAccountOperation] start:^(NSError *error) {
if (nil == error) {
success();
} else {
failure(error); //FAILS WITH INVALID CREDENTIALS ERROR
}
}];
}
Once again, the above code works fine, unless the application has not been used in some time. I was not sure if I needed to refresh the OAuthToken or not, so I tried doing the following on launch of the application:
GTMOAuth2Authentication *auth = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:KeychainItemName clientID:GmailAppClientID clientSecret:GmailClientSecret];
BOOL canAuthorize = [auth canAuthorize]; //returns YES
NSDictionary *googleSettings = [[EmailServicesInfo emailServicesInfoDict] objectForKey:Gmail];
[[ModelManager sharedDefaultInstance] authenticateWithEmailAddress:[auth userEmail] oAuthToken:[auth refreshToken] imapHostname:[googleSettings objectForKey:IMAPHostName] imapPort:[[googleSettings objectForKey:IMAPPort]integerValue] smtpHostname:[googleSettings objectForKey:SMTPHostName] smtpPort:[[googleSettings objectForKey:SMTPPort]integerValue] type:EmailProtocolTypeImapAndSmtpGMail success:^(Account *account) {
//create IMAP session
} failure:^(NSError *error) {
NSLog(#"failure %#", error);
}];
But I still get the same error. I have no idea why the OAuth token stops working or how to resolve this. Since the user is able to save multiple accounts, I am wondering if I need to save the refresh token for each account in core data and use that if the access token stops working?
(Disclaimer - I don't know iOS or the gtm-oauth2 libraries, but I do know Google's OAuth implementation.)
Conceptually you do need to persist the refresh token for the user. The refresh token is a long-lived credential which is used (along with your client secret) to get a short-lived access token that is used for actual API calls.
If you anticipate making multiple calls in a short period of time then your app will normally actually persist both the refresh token and access token (currently access tokens will last 1 hour).
That all said, it looks like the gtm-oauth2 library should be taking care of persisting these already (looks like authForGoogleFromKeychainForName does this).
What I think you need help with is getting an up-to-date access token that you can use to initiate your IMAP session.
The gtm-oauth2 library does contain an authorizeRequest method. It takes information about an HTTP request you intend to make and adds the appropriate authorization headers. It looks like this code will examine the state of the access token, and refresh it if necessary.
While I know you aren't able to make an HTTP request (you need to speak IMAP), my suggestion is to use this method with a dummy NSMutableURLRequest - and then, once it's finished, don't actually send the HTTP request, instead examine the headers it added and pull the access token from there.
See:
https://code.google.com/p/gtm-oauth2/wiki/Introduction#Using_the_Authentication_Tokens
Hope that helps - I don't have an iOS environment to test it on.

Is it possible to get oauth token when requesting access to Facebook via Accounts framework?

I need to do make some API calls in the background on the server that's why I need the oauth token.
Using ACAccountCredential's oauthToken method, it returns null. My question is, it it possible to somehow get the oauth token?
For example Instagram does this. They authenticate via Accounts framework and send the token to their servers where they do all the graph API calls.
Thanks
(Edited to address #0xSina's comment)
Yes. I understand that you've tried accessing the oauthToken property of the ACAccountCredential, of the Facebook ACAccount object and that it is returning nil. That said, this IS the way to do it and it does not return nil when everything is set up correctly.
I created a test project to prove this and the relevant code is below. A few things to check:
1) on your Facebook app information page, ensure that you have your bundle ID set in the Native iOS App section.
2) (pretty obviously) make sure you have your Facebook account setup in iOS settings.
3) if you can't get your main project to work, try an empty test project, as I did, and see if that works. You'll have to remember to set your test project's bundleID to match what you entered on the Facebook settings page!
Here's the code with some asserts to notify you if something's failing:
- (void) viewDidAppear:(BOOL)animated
{
ACAccountStore* as = [ACAccountStore new];
ACAccountType* fbat = [as accountTypeWithAccountTypeIdentifier: ACAccountTypeIdentifierFacebook];
NSAssert( fbat != nil, #"oops, can't find account-type for facebook!");
[as requestAccessToAccountsWithType: fbat
options: #{ ACFacebookAppIdKey : #"99999999999", // your FB appid here!
ACFacebookPermissionsKey : #[ #"email" ] }
completion: ^(BOOL granted, NSError *error) {
NSAssert( granted && error==nil, #"oops, access not granted!");
if ( granted )
{
NSArray* accounts = [as accountsWithAccountType: fbat];
NSAssert( accounts.count > 0, #"oops, no accounts??" );
ACAccount* fbaccount = [accounts lastObject];
ACAccountCredential* ac = [fbaccount credential];
NSAssert( ac != nil, #"oops, no credential??" );
NSString* token = [ac oauthToken];
NSAssert( token.length > 0, #"oops, no credential??" );
NSLog( #"token=%#", token );
}
}];
}
One common issue, I think, is to mis-format the ACFacebookPermissionsKey as a string instead of an array of strings. Or to forego it altogether because the docs say it is optional (it isn't.)
And, here's the output from my test (slightly altered so I'm not publishing a real token here)
2013-07-26 16:54:23.699 testfb[26870:1303] token=CAAAAEZBYxezsBAK6OK9YPQIfA0GjiG7rn2XjdZBC6lZCpfAAAocr2cyEnmne9eMh59ZA4UabQyZAsdQMu0gMruxEJXJI1tFfBVTmLqNpRK31GPQvr3rXRAr50HUXI37iS46gPwXwZAroUHT7WqZCza6HcV6L35gVThBcOttWNxozj1df70XluEreM14nucSmvimjVGu5i4ZBZAUrSYLsvO
It seems that sometimes, either due to one's development process or timed expiration, the Facebook credentials for an app can become invalid. At this point, calls to account.credential will return invalid or nil. In this case you must renew the credential. To do this, simply call -renewCredentialsForAccount:completion: on the account in question. For Facebook accounts this will renew the oauth token automatically.
For other service types the user may be prompted for their password, check the docs in this case.
I was having the exact same issue. The problem for me was that I was setting my Facebook account in the completion handler of: requestAccessToAccountsWithType and then attempting to access the ACAccountCredential in a different function.
Getting the oauth token in the completion handler and saving it for later solved the problem for me:
[account requestAccessToAccountsWithType:accountType options:options completion:
^(BOOL granted, NSError *error) {
...
dispatch_async(dispatch_get_main_queue(), ^{
facebookAccount = [arrayOfAccounts lastObject];
oAuthToken = facebookAccount.credential.oauthToken;
});
...
}
];

Resources