Facebook iOS SDK: authorize with permissions requires 2 attempts - ios

I have an app that still uses the deprecated Facebook class to connect with Facebook. If I authorize with no extended permissions, everything works fine. But if I do include permissions, the first round trip to authorize always fails (even though it gets a valid token!). Am I missing a step?
Here's the code to initiate Facebook authorization for the app
- (IBAction) doConnect:(id)sender
{
NSArray* permissions = [NSArray arrayWithObjects:
#"email",#"publish_actions",nil];
[self.facebook authorize:permissions];
}
Here's the code that gets invoked after the user has granted permissions and control returns to my app. The url always includes a nice looking token, even the first time through.
// handle the incoming url from app switching
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
return [self.facebook handleOpenURL:url];
}
And here's the FBSessionDelegate method that gets invoked after a successful connect. Even though the url above contained a token, it's gone the first time we get here. But if I invoke the doConnect method above, the token will be present when we get here.
// FBSessionDelegate
- (void)fbDidLogin
{
if( [self.facebook accessToken] == nil )
{
NSLog(#"Had an access token above, but not now!");
// If I reinvade the doConnect: method again, it will work!!!
}
// ...
}
Looking deep in the sdk code in FBSession.m, it seems that the requested permissions haven't been associated with the new token first time through, causing the session to ignore the new token. First time through, cachedPermissions is always an empty list
// get the cached permissions, and do a subset check
NSArray *cachedPermissions = [tokenInfo objectForKey:FBTokenInformationPermissionsKey];
BOOL isSubset = [FBSession areRequiredPermissions:permissions
aSubsetOfPermissions:cachedPermissions];

You are asking for two types of permissions, a read type (email) and a write type (publish_actions).
You should be using the latest Facebook SDK, v3.1.1 and you'll have to split up read and writes separately - especially if you wish to support iOS6. You can only ask for reads initially. See the note in https://developers.facebook.com/docs/tutorial/iossdk/upgrading-from-3.0-to-3.1/ and the section on asking for read and writes separately.

For Facebook SDK 3.1, use [FBSession activeSession]'s
reauthorizeWithPublishPermissions: defaultAudience:completionHandler:
For Facebook SDK 3.2, use [FBSession activeSession]'s
requestNewPublishPermissions: defaultAudience:completionHandler:

we can authorise user in single request. It does not need two times attempt. To achieve this what we need do is "We need to ask for publish permission first". Call the below method
let fbLoginMngr = FBSDKLoginManager();
fbLoginMngr.logOut()
fbLoginMngr.logInWithPublishPermissions
it will ask first profile details then asks for requested publish permission. Once we get a call back we query to Graph api to extract the profile data like below.
let fbRequest = FBSDKGraphRequest(graphPath:"me", parameters:self.FB_REQ_PARAMS);
fbRequest.startWithCompletionHandler { (connection : FBSDKGraphRequestConnection!, result : AnyObject!, error : NSError!) -> Void in
if error == nil {
debugPrint(result)
} else {
handleError(error)
}
}

Related

iOS ADAL-Make silent call using refresh token

I am using iOS ADAL library version 2.2.6 and receiving refresh token upon successful login. Now I want to make a silent call by using this refresh token. I tried with following method but it fails to return the access token.
ADAuthenticationContext *authContext;
[authContext acquireTokenSilentWithResource:resourceId
clientId:clientId
redirectUri:redirectUri
userId:strUserID //loggedIn userID
completionBlock:^(ADAuthenticationResult *result){
// It alway throws an error //Please call the non-silent acquireTokenWithResource methods.
if(result.error){
ADAuthenticationError *error = nil;
authContext = [ADAuthenticationContext authenticationContextWithAuthority:inputData.authority error:&error];
[authContext acquireTokenWithResource:inputData.ResourceID
clientId:inputData.ClientId // Comes from App Portal
redirectUri:inputData.RedirectUri // Comes from App Portal
completionBlock:^(ADAuthenticationResult *result)
{
if (AD_SUCCEEDED != result.status){
// Show alert with error description
}
else{
//Handle Success token
}
}];
}else{
//Handle Success token
}
}];
But it always throws an error saying "The user credentials are needed to obtain access token. Please call the non-silent acquireTokenWithResource methods."
Is there any way to make a silent call using refresh token? please help me on it. Thanks in advance.
When you use Microsoft's authentication libraries, you should always first check to see if there is a user in the cache that can be used for your resource before prompting the user to sign in. This allows us to check if the user had previously signed in to your app or if there are other apps that share state with your app that may have already asked the user to sign in elsewhere.
If the user is found, we will try to acquire a token without interrupting the user at all. Sometimes a user will have changed their password or done some other action that will require them to sign in again even if they have signed in to your app previously. This is what you are seeing. The library is telling you that for the user you are trying to acquire a token for, they need to sign in again to make something right.
In order to handle all these cases elegantly, we recommend that you use the pseudocode pattern of:
acquireTokenSilent()
(if error InteractiveAuthenticationRequired) {
acquireTokenInteractively() }
The pattern first checks if a user you specify is available in the token cache. If it is, we then call the Azure Active Directory service to see if the Refresh token for that user is valid. If both of these are true, then the user is signed in silently. If the user isn't found or the server rejects the Refresh Token, then an error is sent from the library that indicates the user needs to sign in interactively.
In the above, you are doing this first part, but you aren't handling the case where the user needs to sign in if there is a problem.
The best way is to catch the error with a ADErrorCode of AD_ERROR_USER_INPUT_NEEDED
Here is a code sample on how to do this pattern.
// Here we try to get a token from the stored user information we would have from a successful authentication
[authContext acquireTokenSilentWithResource:data.resourceId
clientId:data.clientId
redirectUri:redirectUri
userId:data.userItem.userInformation.userId
completionBlock:^(ADAuthenticationResult *result) {
if (!result.error)
{
completionBlock(result.tokenCacheStoreItem.userInformation, nil);
} else {
if ([result.error.domain isEqual:ADAuthenticationErrorDomain] && result.error.code == AD_ERROR_USER_INPUT_NEEDED) {
// Here we know that input is required because we couldn't get a token from the cache
[authContext acquireTokenWithResource:data.resourceId
clientId:data.clientId
redirectUri:redirectUri
userId:data.userItem.userInformation.userId
completionBlock:^(ADAuthenticationResult *result) {
if (result.status != AD_SUCCEEDED)
{
completionBlock(nil, result.error);
}
else
{
data.userItem = result.tokenCacheStoreItem;
completionBlock(result.tokenCacheStoreItem.userInformation, nil);
}
}];
} else {
completionBlock(nil, result.error);
}
}
}];
Keep in mind this code is very verbose. You will most likely want to have acquireTokenWithResource: a separate method that you could call with [self acquireTokenWithResource]

FBSession.openActiveSessionWithReadPermissions not calling completionHandler

I'm implementing OAuth login using Facebook SDK, but the FBSession.openActiveSessionWithReadPermissions:allowLoginUI:completionHandler method is not calling the completionHandler nor is it returning "true", which would indicate a cached token. Everything else seems to work fine, except this. What's wrong with this code?
var completionHandler: FBSessionStateHandler = {
session, status, error in
NSLog("token not cached");
if error {
// Login failed for some reason, so we notify the delegate.
self.delegate.didFailOpenningSession?(self, withError: ErrorUtility.error(
code: .FailedOpenningSession,
withFacebookError: error
));
// Don't proceed on errors
return;
}
// Session is now open, we should notify the delegate
self.delegate.didOpenSession?(self);
};
NSLog("here");
// Open the session, prompting the user if necessary. This might send the application to the background
if FBSession.openActiveSessionWithReadPermissions(["public_profile"], allowLoginUI: true, completionHandler: completionHandler) {
NSLog("Token cached");
// If we end up here, the session token must exist, so we now have an open session
self.delegate.didOpenSession?(self);
}
Edit: I forgot to mention that the NSLog logs "here", but not "Token cached" nor "token not cached"
Edit: Now things got a bit stranger. When my app launches I show a screen with a Facebook login button. When the user presses the button the code above gets triggered. After I authorise the app, the app gets back to the same screen (it shouldn't, but I can't do it any other way until the completion handler gets called). Then I tried pressing the button again and now I do get the logs.
This is what I get:
here
token not cached
here
The first line is from the first button press, the other two appear when I press it the second time. Now what's so odd about it? Shouldn't "token not cached" and "here" be reversed??? Is it calling the competionHandler from the last call or something?
Nevermind, that much is fixed.
Edit: one other thing. If I press the button multiple times, I always get "token not cached". I think it should be caching
It's solved. I was missing the method AppDelegate.application:handleOpenURL -> Bool
First ensure that you are using correct closure syntax. You are not providing a matching closure callback for the block callback.
notice the block callback for the FB session is:
^(FBSession *session,
FBSessionState state, NSError *error)
You should have
/*function parameters*/){ session, state, error in
//closure declaration goes at the top after the {
//Insert logic here
NSLog("Token cached");
}
This will equate to
FBSession.openActiveSessionWithReadPermissions(permissions, allowLoginUI:true){ session, state, error in
//callback code body here
}
This usually because of missing handle seesion in app’s application:openURL function.
It is in AppDelegate.m. Description from facebook samples says below:
// During the Facebook login flow, your app passes control to the Facebook iOS app or Facebook in a mobile browser.
// After authentication, your app will be called back with the session information.
// Override application:openURL:sourceApplication:annotation to call the FBsession object that handles the incoming URL
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
return [FBSession.activeSession handleOpenURL:url];
}

Dropbox Sync API AccessToken

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.

Facebook request does not return email

No email is outputted from the Facebook request. How do I get this? I need it for my login/signup process
- (void)request:(FBRequest *)request didLoad:(id)result{
NSLog(#"FACEBOOK RESULT %# ", result);
}
This is the initial request:
[facebook requestWithGraphPath:#"me" andDelegate:self];
The email property cannot be obtained without requesting additional permission to obtain it, see https://developers.facebook.com/docs/reference/login/email-permissions/.
My application uses the following code to first check whether the Facebook session is valid (self.facebook is a Facebook object initialized with initWithAppId):
if (![self.facebook isSessionValid]) {
NSArray* permissions = [NSArray arrayWithObjects: #"email", nil];
[self.facebook authorize:permissions];
} else {
// Since we have a valid session we can get the userinfo
[self getFacebookUserInfo];
}
Your application prompt will indicate "Using this app requires: * Your basic info * Your e-mail address" If the user authorizes you to obtain this information your access token returned will have bits in set to allow you to obtain the e-mail address, and the following will work:
-(void)getFacebookUserInfo {
[self.facebook requestWithGraphPath:#"me" andDelegate:self];
}
assuming your -(void)request:(FBRequest *)request didLoad:(id)result method is available (which it appears that it is).
Note that the full flow of SSO (Single Sign On) is not given here in this post, you'll need to go through https://developers.facebook.com/docs/tutorials/ios-sdk-tutorial/#implementsso in detail.
Note also that you may be using a deprecated API for iOS 6, you should look at https://developers.facebook.com/docs/tutorial/iossdk/upgrading-from-2.0-to-3.1/ before going further.
With 2.4 api, parameters are used in order to specify which user data items you are interested in receiving.
E.g.:
FBSDKGraphRequestConnection *connection2 = [[FBSDKGraphRequestConnection alloc] init];
FBSDKGraphRequest *selfRequest = [[FBSDKGraphRequest alloc] initWithGraphPath:#"me?"
parameters:#{#"fields":#"id,name,first_name,last_name,verified,email"}];
[connection2 addRequest:selfRequest
completionHandler:^(FBSDKGraphRequestConnection *innerConnection, NSDictionary *result, NSError *error) {
if (error) {
NSLog(#"%#", error);
return;
}
if (result) {
// get user data
NSMutableDictionary *facebookUserData = [NSMutableDictionary dictionaryWithDictionary:result];
NSLog(#"result=%#", result);
HOWEVER, they apparently messed up big time, as email is NOT CURRENTLY being returned for DEVELOPERS, and currently in development Apps.
Incidentally, it is IMPOSSIBLE to submit bugs on Apps in development, as they require approved apps in the feedback system...
I wonder how do they think we are going to make sure that something works BEFORE submitting an app for approval, or what is the logic of asking for an App Store id of an App which can't be approved BEFORE Facebook integration can be tested to the fullest.
Besides, who is the genius at Facebook who decides to break working code one day from another?

Can't get user info from Facebook with OAuth 2.0

As soon as the user is logged in, I retrieve the user info using this code:
[_facebook requestWithGraphPath:#"me" andDelegate:self];
It worked for the first few times, but it eventually returned an error. I thought it was a session thing since I have "offline_access" on my permissions. I logged out from Facebook and compiled again from a clean build and it still returns this error:
Error Domain=facebookErrDomain Code=10000 UserInfo=0x54a2930 "Operation could not be completed. (facebookErrDomain error 10000.)"
I'm assuming my access token is valid since I can do posting. Am I right to assume that?
I also noticed that whenever I log in, Facebook doesn't redirect me to ask permission if I allow my app to access my user info.
Can anyone tell me what's wrong?
Answer on authorize command seems to be different. Now, there's no expiration date as it was before. I tried with forcing isSessionValid to return TRUE, and it works.
isSessionValid checks accessToken AND expirationDate. As there's no expiration date, the function return FALSE.
Don't know if it's a temporary modification from facebook, or definitive one. In this case, SDK must be fixed, I presume. And a new call must retrieve expirationDate.
Here's a temporary fix I've used. You can find this method line 46 in FBLoginDialog.m
- (void) dialogDidSucceed:(NSURL*)url {
NSString *q = [url absoluteString];
NSString *token = [self getStringFromUrl:q needle:#"access_token="];
NSDate *expirationDate = [NSDate distantFuture];
if ((token == (NSString *) [NSNull null]) || (token.length ==0)) {
[self dialogDidCancel:url];
[self dismissWithSuccess:NO animated:YES];
} else {
if ([_loginDelegate respondsToSelector:#selector(fbDialogLogin:expirationDate:)]) {
[_loginDelegate fbDialogLogin:token expirationDate:expirationDate];
}
[self dismissWithSuccess:YES animated:YES];
}
}
Does everyone with this issue request the offline_access permission? It's the only reason I can see an expiration date not being sent. Maybe try not requestion the offline_access permission.
I've had same problem. I checked my privacy settings,permissions,other method etc..., but I couldn't solve it.
Many sites said "check permission for accessing to offline_access/read_stream/publish_stream",I think these are important too,but I couldn't fix it.
Then I found following solution finally, it works fine.
You should change "safariAuth:YES" to "safariAuth:NO" on "Facebook.m" file.
- (void)authorize:(NSArray *)permissions
delegate:(id<FBSessionDelegate>)delegate {
[_permissions release];
_permissions = [permissions retain];
_sessionDelegate = delegate;
//[self authorizeWithFBAppAuth:YES safariAuth:YES];
//You can get auth_token in iOS App!
[self authorizeWithFBAppAuth:YES safariAuth:NO];
}
Then you can use following method to get your own info if you are logged in.
[facebook requestWithGraphPath:#"me" andDelegate:self];
if you are not logged in, You should login by following method first.
permissions = [[NSArray arrayWithObjects:
#"email", #"read_stream", #"user_birthday",
#"user_about_me", #"publish_stream", #"offline_access", nil] retain];
if (![facebook isSessionValid]) {
[facebook authorize:permissions delegate:self];
}
Following post saved me.Thank you so much!
How to retrieve Facebook response using Facebook iOS SDK
Good Luck & Happy-coding!
Thank you.
I've had this problem, mainly because my Facebook account has is maxed on privacy.
It's most likely the permissions that you passed in during login. BY default it will only give access to public info. Remember there is less and less info that is public in Facebook.
Try changing your permission to something like "read_stream",nil
Then instead of [_facebook requestWithGraphPath:#"me" andDelegate:self];
Try using [_facebook requestWithGraphPath:#"me/home" andDelegate:self];
This should return your news feed.
Worked for me 5 mins ago.
Not much to add here, doesn't work here too. I use the iOS SDK to post to the stream and it worked until yesterday. Today my client called me and complained. Ugh.
I have requested the offline access permission and of course the "publish_stream" permission...

Resources