Facebook Login for iOS
The Facebook SDK for iOS provides various login experiences that your app can use to authenticate someone. This document includes all the information you need to know in order to implement Facebook login in your iOS app. If you want to read more about Facebook login in general and why you may want to use it, you can do so in our login portal.
There are two ways to implement Facebook login in your iOS app: using the Facebook login button or implementing your custom login UI using API calls. We will cover both of these with step-by-step tutorials, including sample code and sample apps that you can download from Github. These tutorials will help you with the practical side of login implementation. If you want to gain a deeper understanding of how Facebook sessions work, we will cover that in a separate section.
One of the benefits of having people sign in to your app using Facebook is that they can grant permissions to your app so you can retrieve information or perform actions on Facebook on their behalf. This document also offers information on how to manage permissions tied to a Facebook session.
Facebook sessions and permissions are stored in access tokens, which are cached automatically by the Facebook SDK so they are available when a logged in user returns to your app. However you can manage your own token cache if you wish to do so and we explain how to do it here.
Use the Login Button
In this section:
- Overview
- Prerequisites
- Adding the login UI control
- Handling the Response from the Facebook app
- Asking for permissions
- Handling authentication callbacks
- Testing your login flow
- Additional resources
Overview
The Facebook SDK for iOS provides prebuilt UI components that you can use to log people in and out of your app. In order to authenticate the user, these controls make a call to the user's Facebook app or will fall back to using a web dialog if the user does not have a Facebook app installed.
FBLoginView
is a control in the Facebook SDK that handles a lot of the complexity of helping users log in and log out. It displays a UI button that is aware of the person's login state and will automatically display the appropriate message, "Log in" or "Log out", depending on whether the person using it is authenticated or not, like this:
When people tap the logout button, they are shown an action sheet that lets them confirm that they want to log out:
This tutorial explains how to implement login with Facebook using the FBLoginView UI control. The sample code shown here belongs to the FBLoginUIControlSample app. If you want to see the complete code of this sample, you can find it on github.com/fbsamples/ios-howtos. However, this tutorial is self-contained and you don't need to check out the app code to be able to follow it.
Note: If you're implementing Facebook login in your app, you may want to check if there's a valid access token left over from an earlier session and authenticate the user automatically without them having to touch the login button. To learn more about how logging your users in using cached tokens works you can visit our understanding sessions guide. To find an example implementation, with code, you can refer to our custom UI login tutorial.
Prerequisites
Before you start this tutorial you will need:
- Your environment set up
- A Facebook app properly configured and linked to your iOS app, with Single Sign On enabled
- The Facebook SDK added to your project
- Your Facebook app ID and display name added your app's .plist file
If you haven't done this and need help doing so, you can follow our getting started guide.
Adding the login UI control
You can add the login button programmatically or graphically.
Programmatically
First, add the Facebook for iOS SDK header file to your controller code:
#import <FacebookSDK/FacebookSDK.h>
Then you can create a login button inside any view controller with the following code:
FBLoginView *loginView = [[FBLoginView alloc] init];
[self.view addSubview:loginView];
You can position and size the button as you like. For example, the following code aligns the button in the center horizontally:
FBLoginView *loginView = [[FBLoginView alloc] init];
// Align the button in the center horizontally
loginView.frame = CGRectOffset(loginView.frame, (self.view.center.x - (loginView.frame.size.width / 2)), 5);
[self.view addSubview:loginView];
Graphically
You can add the login UI control graphically using the iOS Interface Builder. To add the UI control, do the following:
- Add a
View
object to your layout. You may size it as you wish. - In the Identity inspector, change the Class property to
FBLoginView
After your changes, your layout should look similar to this:
Note: In Facebook SDK 3.7.x and earlier versions, the login UI control does not support using Auto Layout constraints. You will need to define its layout the old way, by setting the springs and struts in the Size inspector’s autoresizing mask.
After setting up your layout, make the following change to the code that's invoked when your app launches. The changes make sure that the FBLoginView
class is loaded before the view is shown:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
[FBLoginView class];
...
return YES;
}
Note: If you've added the -ObjC
flag to your linker options, then you don't have to add this code. Adding that flag causes the linker to load all the object files in the Facebook SDK, including theFBLoginView
class. If you want to know more about what the -ObjC
flag does, you can check out ourtroubleshooting guide.
Handling the Response from the Facebook app
To process the response you get from interacting with the Facebook login process, you need to override the application:openURL:sourceApplication:annotation:
method in your app delegate by adding the following code:
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
// Call FBAppCall's handleOpenURL:sourceApplication to handle Facebook app responses
BOOL wasHandled = [FBAppCall handleOpenURL:url sourceApplication:sourceApplication];
// You can add your app-specific url handling code here if needed
return wasHandled;
}
Asking for permissions
When someone connects with an app using Facebook login, the app can access optionally ask to read a subset of data that person might have added to Facebook. For example, the information contained in their public profile is available behind the public_profile
permission. Similarly, all other pieces of information that someone adds to their Facebook profile are secured behind other read permissions. For example, a person's primary email address is accessed via the email
permission.
In addition to reading data, apps also need separate publish permissions in order to post content on the person's behalf. These are separate from the read permissions that let you read a person's information. People are often very sensitive about granting publish permissions, so you should only ask for publish permissions once a person is ready to post something from your app and not during the initial login process. The login control makes this easy by separating read permissions and publish permissions. If you want to know more about permissions, you can check out our permissions section.
Here's an example that asks for email
, public_profile
, and user_friends
read permissions. This is useful if you're creating the control programmatically:
FBLoginView *loginView =
[[FBLoginView alloc] initWithReadPermissions:
@[@"public_profile", @"email", @"user_friends"]];
Alternately, if you're using the FBLoginView
through Interface Builder you can set the read permissions through the readPermissions
property of an FBLoginView
instance:
// In your view header file:
@property (weak, nonatomic) IBOutlet FBLoginView *loginView;
// In your viewDidLoad method:
self.loginView.readPermissions = @[@"public_profile", @"email", @"user_friends"];
Permissions review
If your app asks for more than public_profile
, email
and user_friends
, then it will have to be reviewed by Facebook before it can be made available to the general public. Please see ourdocumentation on reviews for more information.
Handling authentication callbacks
FBLoginView
maintains authenticated state. This means that the button displays the appropriate label and performs a login or logout when it's tapped. One other feature of FBLoginView
is it fetches the public profile when the person is logged in. The public profile fetch is a typical request apps make after login, and the UI control does this automatically for you.
You can receive notifications on events that affect FBLoginView
by conforming to theFBLoginViewDelegate
protocol and implementing certain methods. Your delegate would then get notifications for the following events:
FBLoginView
has fetched public profile. Your app is notified if you implement theloginViewFetchedUserInfo:user:
delegate method.- A person is logged in. Your app is notified if you implement the
loginViewShowingLoggedInUser:
method. - A person is logged out. Your app is notified if you implement the
loginViewShowingLoggedOutUser:
delegate method. - A communication or authorization error occurred. Your app is notified if you implement the
loginView:handleError:
delegate method.
To show you how to these callbacks work, we have created an example with a view controller that contains three outlets
: one used to display the person's profile picture when the person is logged in, a second to display their name when they're logged in, and a third to display the status ("You're logged in as" or "You're not logged in!"). Our app's UI will look like this:
And our interface will look like this:
@interface LoginUIViewController ()
@property (strong, nonatomic) IBOutlet FBProfilePictureView *profilePictureView;
@property (strong, nonatomic) IBOutlet UILabel *nameLabel;
@property (strong, nonatomic) IBOutlet UILabel *statusLabel;
@end
Note: If you want to see the complete code of these example or see the login control in action, you can checkout the FBLoginUIControlSample app from github.com/fbsamples/ios-howtos. However, this tutorial is self-contained and you don't need to check out the app code to be able to follow it.
To set up the delegate methods, first add FBLoginViewDelegate
to your class' protocol list.
@interface LoginUIViewController : UIViewController <FBLoginViewDelegate>
Next, assign the delegate to your view controller class.
If you defined the login UI control programmatically, modify your code to do the following:
FBLoginView *loginView = [[FBLoginView alloc] init];
loginView.delegate = self;
...
If you defined the login UI control using Interface Builder:
- Select the
View
that represents the login button. - In the Connections inspector, connect the
delegate
outlet to the File's Owner.
Then, implement the optional delegate methods.
Implementing the loginViewFetchedUserInfo:user:
delegate method allows you to add a personal touch to your app. In this example , the user data returned in the FBGraphUser
object is used to set the user's profile picture and set their name:
// This method will be called when the user information has been fetched
- (void)loginViewFetchedUserInfo:(FBLoginView *)loginView
user:(id<FBGraphUser>)user {
self.profilePictureView.profileID = user.id;
self.nameLabel.text = user.name;
}
Implementing the loginViewShowingLoggedInUser:
delegate method allows you to modify your app's UI to show a logged in experience. In the example below, we notify the user that they are logged in by changing the status:
// Logged-in user experience
- (void)loginViewShowingLoggedInUser:(FBLoginView *)loginView {
self.statusLabel.text = @"You're logged in as";
}
Implementing the loginViewShowingLoggedOutUser:
delegate method allows you to modify your app's UI to show a logged out experience. In the example below, the user's profile picture is removed, the user's name set to blank, and the status is changed to reflect that the user is not logged in:
// Logged-out user experience
- (void)loginViewShowingLoggedOutUser:(FBLoginView *)loginView {
self.profilePictureView.profileID = nil;
self.nameLabel.text = @"";
self.statusLabel.text= @"You're not logged in!";
}
Finally, implementing the loginView:handleError:
delegate method allows you to detect and respond to errors gracefully. See the API errors doc for more details on possible authentication errors and recommended recovery tactics.
In this example, the person using your app is notified if: * They need to perform an action outside of your app to recover from the error (for example, if the login attempt fails because the user's Facebook password has changed or their Facebook account is not verified) * The session is no longer valid (for example is the user logged out of your app from the Facebook UI) * For any other error, except if the user is denying your app the requested permissions
// Handle possible errors that can occur during login
- (void)loginView:(FBLoginView *)loginView handleError:(NSError *)error {
NSString *alertMessage, *alertTitle;
// If the user should perform an action outside of you app to recover,
// the SDK will provide a message for the user, you just need to surface it.
// This conveniently handles cases like Facebook password change or unverified Facebook accounts.
if ([FBErrorUtility shouldNotifyUserForError:error]) {
alertTitle = @"Facebook error";
alertMessage = [FBErrorUtility userMessageForError:error];
// This code will handle session closures that happen outside of the app
// You can take a look at our error handling guide to know more about it
// https://developers.facebook.com/docs/ios/errors
} else if ([FBErrorUtility errorCategoryForError:error] == FBErrorCategoryAuthenticationReopenSession) {
alertTitle = @"Session Error";
alertMessage = @"Your current session is no longer valid. Please log in again.";
// If the user has cancelled a login, we will do nothing.
// You can also choose to show the user a message if cancelling login will result in
// the user not being able to complete a task they had initiated in your app
// (like accessing FB-stored information or posting to Facebook)
} else if ([FBErrorUtility errorCategoryForError:error] == FBErrorCategoryUserCancelled) {
NSLog(@"user cancelled login");
// For simplicity, this sample handles other errors with a generic message
// You can checkout our error handling guide for more detailed information
// https://developers.facebook.com/docs/ios/errors
} else {
alertTitle = @"Something went wrong";
alertMessage = @"Please try again later.";
NSLog(@"Unexpected error:%@", error);
}
if (alertMessage) {
[[[UIAlertView alloc] initWithTitle:alertTitle
message:alertMessage
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil] show];
}
}
Testing your login flow
Follow our test your login flow guide to ensure that your app's login will work under a range of common conditions.
Additional resources
For more information about adding login to your iOS apps, see the following:
Build Your Own Button
In this section:
- Overview
- Prerequisites
- Opening a session
- Handling session state changes
- Handling app cold starts
- Testing your login flow
- Additional resources
Overview
This tutorial walks you through how to add Facebook login to your app using your own custom UI for the login and logout controls. This way, you can stay in control of the look and feel of your app and provide a consistent user experience.
To implement this login, we will use one of the Facebook SDK core objects: FBSession
. TheFBSession
object is used to authenticate a user and manage the user's session. Opening a session will initiate the authentication flow after which a valid user session should be available and subsequently cached in the form of an access token. Cached access tokens, when still valid, can then be used to reopen a session any time. Closing a session logs the user out and can optionally clear the cached token. While an app can create instances of FBSession and manage them itself, FBSession provides helpers to simplify the common scenario of an app having a single logged-in person. We'll use those helpers here. If you want to learn more about how Facebook sessions work, you can read ourunderstanding sessions section.
This tutorial shows a simple use case where the user is presented with a custom login/logout button that the user can touch to toggle their authentication state. When the user is logged in, we will show then a logged-in UI that consists on a welcome message and changing the button title to "Log out", when the user is logged out we will show them a logout confirmation message and a "Log in with Facebook" button, like this:
We will also check for a cached session when the user opens the app, in order to log the user in automatically if possible. The sample code shown belongs to the FBLoginCustomUISample app. If you want to see the complete code of this sample, you can find it on github.com/fbsamples/ios-howtos. However, this tutorial is self-contained and you don't need to check out the app code to be able to follow it.
Prerequisites
Before you start this tutorial you will need:
- Your environment set up
- A Facebook app properly configured and linked to your iOS app, with Single Sign On enabled
- The Facebook SDK added to your project
- Your Facebook app ID and display name added your app's .plist file
If you haven't done this and need help doing so, you can follow our getting started guide.
Opening a session
When a user first logs into your app using Facebook, your app passes control to the Facebook iOS app or Facebook in a mobile browser, which will show the user a dialog prompting them to authorize your app. If all goes well, this will result in the creation of a session that will be stored in an access token and cached. However, when you're trying to get your users to log into your app, either with Facebook or otherwise, it is always good practice to minimize the friction by avoiding unnecessary steps. So, if a person has previously logged into your app with Facebook and there's still a valid cached token from that session, we recommend that you open a session using it. This way you eliminate the need for your user to touch a button to log in.
To implement this, in app delegate's application:didFinishLaunchingWithOptions:
method, we will add the following:
// Whenever a person opens the app, check for a cached session
if (FBSession.activeSession.state == FBSessionStateCreatedTokenLoaded) {
// If there's one, just open the session silently, without showing the user the login UI
[FBSession openActiveSessionWithReadPermissions:@[@"public_profile"]
allowLoginUI:NO
completionHandler:^(FBSession *session, FBSessionState state, NSError *error) {
// Handler for session state changes
// This method will be called EACH time the session state changes,
// also for intermediate states and NOT just when the session open
[self sessionStateChanged:session state:state error:error];
}];
We first check if there's a cached token by examining the active session's state. If there is a cached token the session state will be FBSessionStateCreatedTokenLoaded
. If this is the case, we can try opening the cached session using the FBSession
openActiveSessionWithReadPermissions:allowLoginUI:completionHandler:
method, with allowLoginUI:
set to NO
(this will prevent the login dialog from showing). The other arguments that need to be passed to this method are:
- The permissions you're requesting.
- The completion handler, which will be called whenever there's a session state change.
When someone connects with an app using Facebook login, the app may access data they have saved on Facebook. This is done through asking for permissions. Reading data requires asking for read permissions. Apps also need publish permissions in order to post content on the user's behalf. If you want to know more about permissions, you can check out our permissions section.
Apps that ask for more than public_profile
, email
and user_friends
will have to be reviewed by Facebook before they can be made available to the general public. Please see our documentation on reviews for more information.
The openActiveSessionWithReadPermissions:allowLoginUI:completionHandler:
accepts a handler that will be called whenever there's a session state change. This means that your handler method will be called for every intermediate session state change during login, and not only after the session is open (or opening fails). But also for every other session state change that occurs during the session's entire lifetime. If you want to read more about the FBSession
lifecycle, you can refer to our understanding sessions guide. We will take a closer look at this in the handling session state changes section of this tutorial.
So far, we have discussed how to open a session if there is a cached token. Opening a session without one is very similar. In our example, we have a single button that the user can touch to toggle authentication. When the user touches this button the app will pass control to the Facebook iOS app or Facebook in a mobile browser, which will show the user a dialog prompting them to authorize the app, like this:
To implement this we need to do three things:
- Add the session opening/closing calls to the login button
- Handle the incoming URL when the Facebook app or Facebook website return control to your app
- Handle the case when the user leaves your app while the Facebook login dialog is being shown
To wire up the button, in the button delegate, we do the following:
- (IBAction)buttonTouched:(id)sender
{
// If the session state is any of the two "open" states when the button is clicked
if (FBSession.activeSession.state == FBSessionStateOpen
|| FBSession.activeSession.state == FBSessionStateOpenTokenExtended) {
// Close the session and remove the access token from the cache
// The session state handler (in the app delegate) will be called automatically
[FBSession.activeSession closeAndClearTokenInformation];
// If the session state is not any of the two "open" states when the button is clicked
} else {
// Open a session showing the user the login UI
// You must ALWAYS ask for public_profile permissions when opening a session
[FBSession openActiveSessionWithReadPermissions:@[@"public_profile"]
allowLoginUI:YES
completionHandler:
^(FBSession *session, FBSessionState state, NSError *error) {
// Retrieve the app delegate
AppDelegate* appDelegate = [UIApplication sharedApplication].delegate;
// Call the app delegate's sessionStateChanged:state:error method to handle session state changes
[appDelegate sessionStateChanged:session state:state error:error];
}];
}
}
When the user touches the button, if the current session is open, we will close it. Alternatively, if the current session in any state other than open, we will attempt to open it. To check if the session is open, we check if the session state is any of the two open states: FBSessionStateOpen
orFBSessionStateOpenTokenExtended
. If this is the case, we close the session by callingcloseAndClearTokenInformation
on the active session. Closing the session will call the session state change handler automatically. If the session is not open, we will attempt to open it by calling theFBSession openActiveSessionWithReadPermissions:allowLoginUI:completionHandler:
method, with allowLoginUI:
set to YES
. Again, we need to specify the permissions (at leastpublic_profile
) and the completion handler. For the completion handler we are using the same method we used for our silent login.
Next, to handle the incoming URL when the Facebook app or Facebook website return control to your app, override application:openURL:sourceApplication:annotation
in your app delegate to call the FBsession object that handles the incoming URL:
// 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.
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
return [FBAppCall handleOpenURL:url sourceApplication:sourceApplication];
}
By default, the FBAppCall
will complete the flow for FBSession.activeSession
. If you are explicitly managing FBSession
instances, use the overload that takes an FBSession
instance.
And finally, you will need to handle the case when the user leaves your app while the Facebook login dialog is being shown. This can happen, for example, if the uses presses the iOS "home" button while the dialog is active. In order to handle this case, override the applicationDidBecomeActive:
method in your app delegate to call FBAppCall handleDidBecomeActive
. This method will check for any session opening processes that might have been left hanging and will clear the unresolved session so you can start anew.
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Handle the user leaving the app while the Facebook login dialog is being shown
// For example: when the user presses the iOS "home" button while the login dialog is active
[FBAppCall handleDidBecomeActive];
}
In the next session we will see how to handle the session state changes.
Handling session state changes
In the previous session, when we opened a session, we had to provide a handler that will be called whenever there's a session state change. This handler method will be called for every intermediate session state change during login, and not only after the session is open (or opening fails). But also for every other session state change that occurs during the session's entire lifetime. If you want to read more about the FBSession
lifecycle, you can refer to our understanding sessions section.
Here is an example of handler that deals with the different possible state changes and errors:
// This method will handle ALL the session state changes in the app
- (void)sessionStateChanged:(FBSession *)session state:(FBSessionState) state error:(NSError *)error
{
// If the session was opened successfully
if (!error && state == FBSessionStateOpen){
NSLog(@"Session opened");
// Show the user the logged-in UI
[self userLoggedIn];
return;
}
if (state == FBSessionStateClosed || state == FBSessionStateClosedLoginFailed){
// If the session is closed
NSLog(@"Session closed");
// Show the user the logged-out UI
[self userLoggedOut];
}
// Handle errors
if (error){
NSLog(@"Error");
NSString *alertText;
NSString *alertTitle;
// If the error requires people using an app to make an action outside of the app in order to recover
if ([FBErrorUtility shouldNotifyUserForError:error] == YES){
alertTitle = @"Something went wrong";
alertText = [FBErrorUtility userMessageForError:error];
[self showMessage:alertText withTitle:alertTitle];
} else {
// If the user cancelled login, do nothing
if ([FBErrorUtility errorCategoryForError:error] == FBErrorCategoryUserCancelled) {
NSLog(@"User cancelled login");
// Handle session closures that happen outside of the app
} else if ([FBErrorUtility errorCategoryForError:error] == FBErrorCategoryAuthenticationReopenSession){
alertTitle = @"Session Error";
alertText = @"Your current session is no longer valid. Please log in again.";
[self showMessage:alertText withTitle:alertTitle];
// Here we will handle all other errors with a generic error message.
// We recommend you check our Handling Errors guide for more information
// https://developers.facebook.com/docs/ios/errors/
} else {
//Get more error information from the error
NSDictionary *errorInformation = [[[error.userInfo objectForKey:@"com.facebook.sdk:ParsedJSONResponseKey"] objectForKey:@"body"] objectForKey:@"error"];
// Show the user an error message
alertTitle = @"Something went wrong";
alertText = [NSString stringWithFormat:@"Please retry. \n\n If the problem persists contact us and mention this error code: %@", [errorInformation objectForKey:@"message"]];
[self showMessage:alertText withTitle:alertTitle];
}
}
// Clear this token
[FBSession.activeSession closeAndClearTokenInformation];
// Show the user the logged-out UI
[self userLoggedOut];
}
}
In this case, if the session is opened successfully, we will show the user a logged-in UI. If the session is closed, either deliberately or if the login fails, we will show the user the logged-out UI.
If there are any errors we will handle them the following way. First we will call the FBErrorUtility shouldNotifyUserForError:
method passing it the error we got, in order to find out if the error requires people using your app to make an action outside of your app in order to recover. This can be the case, for example, if the user hasn't verified their Facebook account and they have to complete that process before they can use this account to log into your app. In cases like this, the SDK provides an error message with instructions that you can show your user. So, if shouldNotifyUserForError:
returns true
, then we will notify the user by showing them the message we get from theFBErrorUtility
by calling userMessageForError:error
. If not, we will have to handle the error within the app's error logic. To handle the error, we will need more information about it. This information is available in the error category, to retrieve it we can call [FBErrorUtility errorCategoryForError:error]
.
In the case where the user touches the "cancel" when shown the Facebook login dialog, the error category will be FBErrorCategoryUserCancelled
. In this example, we have chosen to ignore this, but you can also choose to show the user a message if you feel that cancelling login will result in the user not being able to complete a task they had initiated in your app (like accessing FB-stored information or posting to Facebook).
We have mentioned that this handler method will be called for every session state change during the entire lifetime of the session, so we also need to handle session closures that can happen outside of your app, even if they are unrelated to the login process. An example of this would be a session closure that happens because the user removed your app from the Facebook UI. For these cases, the error category will be FBErrorCategoryAuthenticationReopenSession
. To recover from this error, we tell our users to log in again.
In this example, we only specifically handle two error categories, providing a generic message for the rest, but you may need to handle other cases depending on your app. You can find more information on the different error categories and what they mean in our FBError
reference. You can also read more about error handling in our error handling guide.
Finally, if there has been any error, we will clear the cached token by callingcloseAndClearTokenInformation
on the active session, so next time we attempt a login we start with a fresh session.
Handling app cold starts
Since most login flows require an app switch to complete, it's possible your app gets terminated by iOS in low memory conditions (or if your app does not support backgrounding). In that case, the state change handler block supplied to your open call is lost. To handle that scenario, you can explicitly assign a state change handler block to the FBSession instance any time prior to the handleOpenURL:
call. For example,
// 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.
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
// Note this handler block should be the exact same as the handler passed to any open calls.
[FBSession.activeSession setStateChangeHandler:
^(FBSession *session, FBSessionState state, NSError *error) {
// Retrieve the app delegate
AppDelegate* appDelegate = [UIApplication sharedApplication].delegate;
// Call the app delegate's sessionStateChanged:state:error method to handle session state changes
[appDelegate sessionStateChanged:session state:state error:error];
}];
return [FBAppCall handleOpenURL:url sourceApplication:sourceApplication];
}
Testing your login flow
Follow our test your login flow guide to ensure that your app's login will work under a range of common conditions.
Additional resources
Understanding sessions
In this section:
Overview
When someone successfully connects with an app using the Facebook login, the app will be able to obtain an access token which provides temporary, secure access to the Facebook APIs. The level of access will depend on the permissions the user has granted to the app. The FBSession
class is used to manage, store, and refresh access tokens by default.
This section outlines the concepts behind the FBSession
state lifecycle to provide you with a background of the session management features. For information on how to override the default behavior using your own session token caching, you can read our managing your own token cache section.
FBSession State Lifecycle
The Facebook SDK uses a session state machine to control the session lifecycle. These states are used by the Facebook SDK to allow or deny access to the other Facebook APIs, handle access token storage or retrieval, and deal with any error scenarios.
A typical lifecycle from login through logout is illustrated below. In the example, we look for a cached token at app launch to provide an initial logged in experience if a token is available. If the token is not present, we show a login UI, where the user can touch a button to initiate the login process. After that, an authenticated user can be asked for additional permissions during app use, which kicks off a new permissions request flow that will trigger a UI to ask the user for the permissions. Finally, the authenticated user can log out of the app by clicking on a logout button.
For each of these processes (app launch, login dialog, asking for additional permissions, and logout) we will show a diagram showing all the intermediate states a session will go through until the operation is completed. You only need to concern yourself with the final session state for each of them, which will determine the actions your app can make on behalf of the user. You can safely ignore the intermediate states and have your completion handlers act on the final states only.
App Launch
In order to check for a cached token, when the app is launched we call the FBSession
class methodopenActiveSessionWithReadPermissions:allowLoginUI:completionHandler:
withallowLoginUI
set to NO
. This will prevent the login UI from showing and the session will be opened only if a cached token is found and without displaying the login UI. Any of the otheropenActiveSession*
methods will trigger the same flow, as long as allowLoginUI
is set to NO
.
The following image shows the states a session may go through after an openActiveSession*
method is called with allowLoginUI
set to NO
. First, a FBSession.activeSession
object is created in the state FBSessionStateCreated
. FBSessionStateCreated
is the state a session is in when it exists but no token has been found (yet). After that, if a token is found, the session will transition toFBSessionStateCreatedTokenLoaded
. And, from FBSessionStateCreatedTokenLoaded
, when the session has finished opening it will transition to FBSessionStateOpen
. Only when the session is open, you can make calls to the other Facebook APIs, provided you have the right permissions. If a token is not found, the session will remain in FBSessionStateCreated
.
Note: Once you have created a FBSession.activeSession
object, whether the session is opened successfully or not, the active session will continue to exist unless you clear the access token associated with it by calling closeAndClearTokenInformation
on the active session.
Instead of calling one of the static openActiveSession*
methods, your app can achieve the same flow following these steps:
- Call one of the
FBSession
instanceinit
functions. - Check if the state value has transitioned to
FBSessionStateCreated
. - If the state value is
FBSessionStateCreated
, call one of theFBSession
instanceopen
functions.
You would want to use this flow if you can't use the static methods, for example: if you are caching the tokens for the FBSession yourself, instead of using the built-in mechanisms, and need to pass that information in when initializing a session.
Login
The login UI is triggered by calling the FBSession
class method,openActiveSessionWithReadPermissions:allowLoginUI:completionHandler:
withallowLoginUI
set to YES
to force the display of the login UI.
On iOS6+, an attempt is made to login using the OS Facebook system account. If the Facebook system account is not set up, log in falls back to using fast app switching to the Facebook for iOS app or Mobile Safari.
The iOS6+ login flow is shown below:
Initially the session is in FBSessionStateCreated
. As we are forcing the SDK to show the login dialog through the use of allowLoginUI:YES
, our session will immediately transition toFBSessionStateCreatedOpening
and then show the login dialog.FBSessionStateCreatedOpening
is the state a session is in when an attempt to log in a user through the UI is underway. If the user clicks OK, the state transitions to FBSessionStateOpen
. If the user clicks on Don't Allow, the state transitions to FBSessionStateClosedLoginFailed
. A failure to open the session returns error information that you can display to the user, we will see more of that when we tackle handling errors in this same document.
Alternatively, the fast-app switch login flow is shown below:
As we have forced the login UI with allowLoginUI:YES
, the session in FBSessionStateCreated
transitions to FBSessionStateCreatedOpening
before the app is switched to Facebook for iOS or to Mobile Safari. Like before, if the user accepts the permissions in the login screen, the state transitions toFBSessionStateOpen
. If the user touches the Cancel button, the state transitions toFBSessionStateClosedLoginFailed
and the error information is provided as well. However, when shown the dialog, and while the session state is FBSessionStateCreatedOpening
, the user may press the iOS Home button. In this scenario, Facebook for iOS has no path to send information about the interruption in the login flow. You handle this by calling FBAppCall
's handleDidBecomeActive
method, inside your applicationDidBecomeActive:
method in your app delegate. This will cleanup the flow and eventually transition the state to the FBSessionStateClosedLoginFailed
state.
Asking for Additional Permissions
We mentioned before that once a session is open, you can make calls to the other Facebook APIsprovided that you have the necessary permissions. In case you need to, your app can ask for additional permissions when the user is authenticated. The FBSession
instance methods requestNew*
are used to initiate these flows.
The diagram above shows the flow for an iOS6 device with the user logged into the system account. The flow is essentially the same for the fast app switch cases. If the user accepts the additional permissions, the session state transitions to FBSessionStateOpenTokenExtended
.
The Facebook SDK automatically refreshes the user's session if necessary. When it does this, the state also transitions to the FBSessionStateOpenTokenExtended
state.
If the user refuses to accept the additional permissions then the state does not transition and remains in either FBSessionStateOpen
or FBSessionStateOpenTokenExtended
.
Logout
When the user clicks on a logout button, the closeAndClearTokenInformation
method is called on the active session, resulting on the following flow:
This transitions the state from either FBSessionStateOpen
orFBSessionStateOpenTokenExtended
, the ''open'' states, to FBSessionStateClosed
.
closeAndClearTokenInformation
closes the session and clears any cached info, destroying the session. This means that when the app is re-launched the cached token will not be available. The login view will then be shown.
Alternatively, calling the close
method on a FBSession
also transitions the state toFBSessionStateClosed
but the cached info is not cleared and the token is available to be reused the next time the app is launched. Using the close
method in the app delegate'sapplicationWillTerminate:
method is a good practice to trigger the clean up any dependent objects that rely on an open session.
Some points to note
-
Sessions can only be opened once. When a session is closed, it cannot be re-opened. Instead, a new session should be created. Typical apps will only require one active session at any time. The Facebook SDK provides static active session methods that take care of opening new session instances.
-
The different methods available with the SDK to open a session may take in completion handlers. The handlers become associated with that session instance and are invoked for the following state changes:
Open
,OpenTokenExtended
,Closed
, andClosedLoginFailed
.
Managing permissions
In this section:
- Overview
- Requesting permissions at login
- Checking for permissions
- Handling missing permissions
- Requesting additional permissions
- Revoking permissions
- Revoking login
Overview
During the basic login flow, your app receives access to a person's public profile and friend list. In order to access additional elements of their Facebook profile, or to publish content to Facebook on their behalf, you need to request the necessary permissions. These permissions can be requested at the time of initial login or at any other point during the app experience.
If you want to make a request to Facebook on behalf of the user, you need to either check that you have the necessary permissions and if not request them, or make the request directly and deal with the missing permissions error by asking for the permissions and retrying.
Also, people using your app can revoke permissions from the Facebook UI, but you can also implement a mechanism to do this from within your app if you wish to do so. You can also let users remove basic login privileges from your app, by implementing a mechanism to revoke login entirely.
In this part of the guide we will walk you through:
- Requesting permissions at login
- Checking for permissions
- Handling missing permissions
- Requesting additional permissions
- Revoking permissions
- Revoking login
You can find information about the different available permissions and what they mean in ourpermissions reference.
Requesting permissions at login
When someone connects with an app using Facebook login, the app can access their public profile and friend list, the pieces of information that are visible to everyone. To create this basic connection, apps must always request access to a person's public profile information by asking for the public_profile
permission. Similarly, all other pieces of information that someone adds to their Facebook profile are secured behind other read permissions. Apps also need publish permissions in order to post content on the user's behalf. If you want to know more about permissions, you can check out our permissions reference.
You can ask for any number of read permissions or for publish permissions during login. However, as a general rule, the more permissions an app requests, the less likely it is that people accept the permission request and continue with the login process. Our research shows that apps that ask for more than four permissions experience a significant drop off in the number of completed logins. Requesting publishing permissions with publish_actions
during login will prompt a second step in the login UI, as they have their own publish permissions request screen that's shown after the login screen, which can cause fewer people to log in. As a result, we suggest that you request the bare minimum of read permissions during login and then request any additional or publish permissions when a person actually needs them.
You can learn about how to optimize your permission requests here.
To ask for permissions on login you can use the openActiveSessionWith*Permissions:*
methods. Here is an example that asks for the public_profile
(required) and user_birthday
read permissions:
// Open session with public_profile (required) and user_birthday read permissions
[FBSession openActiveSessionWithReadPermissions:@[@"public_profile", @"user_birthday"]
allowLoginUI:YES
completionHandler:
^(FBSession *session, FBSessionState state, NSError *error) {
__block NSString *alertText;
__block NSString *alertTitle;
if (!error){
// If the session was opened successfully
if (state == FBSessionStateOpen){
// Your code here
} else {
// There was an error, handle it
if ([FBErrorUtility shouldNotifyUserForError:error] == YES){
// Error requires people using an app to make an action outside of the app to recover
// The SDK will provide an error message that we have to show the user
alertTitle = @"Something went wrong";
alertText = [FBErrorUtility userMessageForError:error];
[[[UIAlertView alloc] initWithTitle:alertTitle
message:alertText
delegate:self
cancelButtonTitle:@"OK!"
otherButtonTitles:nil] show];
} else {
// If the user cancelled login
if ([FBErrorUtility errorCategoryForError:error] == FBErrorCategoryUserCancelled) {
alertTitle = @"Login cancelled";
alertText = @"Your birthday will not be entered in our calendar because you didn't grant the permission.";
[[[UIAlertView alloc] initWithTitle:alertTitle
message:alertText
delegate:self
cancelButtonTitle:@"OK!"
otherButtonTitles:nil] show];
} else {
// For simplicity, in this sample, for all other errors we show a generic message
// You can read more about how to handle other errors in our Handling errors guide
// https://developers.facebook.com/docs/ios/errors/
NSDictionary *errorInformation = [[[error.userInfo objectForKey:@"com.facebook.sdk:ParsedJSONResponseKey"]
objectForKey:@"body"]
objectForKey:@"error"];
alertTitle = @"Something went wrong";
alertText = [NSString stringWithFormat:@"Please retry. \n
If the problem persists contact us and mention this error code: %@",
[errorInformation objectForKey:@"message"]];
[[[UIAlertView alloc] initWithTitle:alertTitle
message:alertText
delegate:self
cancelButtonTitle:@"OK!"
otherButtonTitles:nil] show];
}
}
}
}
}];
If you're initializing and then opening the session manually, you can add permissions to a session you're about to open by initializing your session using initWithPermissions:
, or by assigning your session's permissions
property once the session has been initialized.
Checking for permissions
Facebook offers people full control over the permissions they grant to app. Your users can revoke permissions they have granted to your app from their Facebook Privacy settings at any time. They can also decline to share permissions with your app at login time. So apps should check for the current validity of permissions before attempting to perform an API call, for example, checking that publish_actions is still granted before attempting to publish an Open Graph story. Here is an example:
// Check for publish permissions
[FBRequestConnection startWithGraphPath:@"/me/permissions"
completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
__block NSString *alertText;
__block NSString *alertTitle;
if (!error){
// Walk the list of permissions looking to see if publish_actions has been granted
NSArray *permissions = (NSArray *)[result data];
BOOL publishActionsSet = FALSE;
for (NSDictionary *perm in permissions) {
if ([[perm objectForKey:@"permission"] isEqualToString:@"publish_actions"] &&
[[perm objectForKey:@"status"] isEqualToString:@"granted"]) {
publishActionsSet = TRUE;
NSLog(@"publish_actions granted.");
break;
}
}
if (!publishActionsSet){
// Publish permissions not found, ask for publish_actions
[self requestPublishPermissions];
} else {
// Publish permissions found, publish the OG story
[self publishStory];
}
} else {
// There was an error, handle it
// See https://developers.facebook.com/docs/ios/errors/
}
}];
Handling missing permissions
If you check for the corresponding permissions before trying to use APIs that require them, you should never run into missing permission errors. However, you may choose to attempt an API call and only request the permissions in case the first call failed due to permissions not being present. In case you need to detect missing permissions errors, you can use the UserInfo
property of the NSError that the SDK will return. If an error was caused by missing permissions, its UserInfo
will look like this:
{
"com.facebook.sdk:ErrorSessionKey" = "<FBSession: 0xa2115f0, state: FBSessionStateOpen, loginHandler: 0xa211800, appID: 462857150476058, urlSchemeSuffix: , tokenCachingStrategy:<FBSessionTokenCachingStrategy: 0xa1567a0>, expirationDate: 2013-11-24 22:25:07 +0000, refreshDate: 2013-09-25 22:25:08 +0000, attemptedRefreshDate: 0001-12-30 00:00:00 +0000, permissions:(\n \"public_profile\",\n \"user_birthday\"\n)>";
"com.facebook.sdk:HTTPStatusCode" = 403;
"com.facebook.sdk:ParsedJSONResponseKey" = {
body = {
error = {
code = 200;
message = "(#200) Requires extended permission: publish_actions";
type = OAuthException;
};
};
code = 403;
};
}
You can find more information on best practices when handling missing permissions here.
Requesting additional permissions
You can request additional permissions for an active session at any time. In fact we recommend that you ask for permissions when your app actually needs them to complete an action initiated by the user. You can read more about optimizing permission requests here.
The following example shows how to request for publish_actions
publish permissions for an active session.
// Request publish_actions
[FBSession.activeSession requestNewPublishPermissions:[NSArray arrayWithObject:@"publish_actions"]
defaultAudience:FBSessionDefaultAudienceFriends
completionHandler:^(FBSession *session, NSError *error) {
__block NSString *alertText;
__block NSString *alertTitle;
if (!error) {
if ([FBSession.activeSession.permissions
indexOfObject:@"publish_actions"] == NSNotFound){
// Permission not granted, tell the user we will not publish
alertTitle = @"Permission not granted";
alertText = @"Your action will not be published to Facebook.";
[[[UIAlertView alloc] initWithTitle:alertTitle
message:alertText
delegate:self
cancelButtonTitle:@"OK!"
otherButtonTitles:nil] show];
} else {
// Permission granted, publish the OG story
[self publishStory];
}
} else {
// There was an error, handle it
// See https://developers.facebook.com/docs/ios/errors/
}
}];
Revoking permissions
Apps can let people remove, or revoke, specific permissions that were previously granted. For example, the app could have a settings page that lets people disable specific features, such as publishing actions, and the app could revoke the publish_actions
permission at the same time.
You can revoke a specific permission by making a call to a Graph API endpoint. If the request is successful, you will receive a response of true. In this example, we revoke the publish_actions
permission:
[FBRequestConnection startWithGraphPath:@"/me/permissions/publish_actions"
parameters:nil
HTTPMethod:@"delete"
completionHandler:^(FBRequestConnection *connection, id result, NSError *error) {
__block NSString *alertText;
__block NSString *alertTitle;
if (!error && result == true) {
// Revoking the permission worked
alertTitle = @"Permission successfully revoked";
alertText = @"This app will no longer post to Facebook on your behalf."
} else {
// There was an error, handle it
// See https://developers.facebook.com/docs/ios/errors/
}
[[[UIAlertView alloc] initWithTitle:alertTitle
message:alertText
delegate:self
cancelButtonTitle:@"OK!"
otherButtonTitles:nil] show];
}];
Revoking login
Similarly, you can revoke all privileges for an app, by making a Graph API call that deletes all permissions. To do this, you can use the code in the example above and change the path of the request to /me/permissions
.
Login dialogs
In this section:
- Overview
- Facebook App Native
- iOS Native
- Facebook App Web
- Mobile Safari
- Embedded WebView
- Controlling the login dialogs
Overview
When a person using your app tries to log in with Facebook, they will be shown one of the Facebook login dialogs. The function of these dialogs is to allow the person logging in to authenticate and grant the necessary permissions to your app. Also, they ensure that the person logging in and granting the permissions is aware that they're doing so. As a result, the login dialogs cannot be by-passed.
There are different dialogs that the Facebook SDK for iOS can present to a person who is logging in to your app with Facebook. The options are independent of the way you choose to implement login (either through the Facebook login button or a custom UI using API calls): the SDK will automatically choose the best option for you, there's nothing you need to change in your code.
This section describes the different login dialogs, the advantages and disadvantages of each one, as well as the available options if you wish to customize the experience.
Facebook App Native Login DialogThe Facebook App Native Login Dialog is fully native. This dialog is presented within the Facebook for iOS app by doing a fast app-switch that brings the Facebook for iOS app to the foreground while the dialog is being shown, and returns control to your app once the user has finished interacting with it. In order for this dialog to work, the Facebook app name must be specified in your app's .plist under the key Advantages:
Disadvantages:
|
iOS Native Login DialogThe iOS native login dialog is a feature of Apple's Facebook integration. If people have logged in to Facebook via their iOS settings, they can be authenticated into your app using these credentials. This is a device-level authentication, which means that a person has to log in to your app on every device they use. Advantages:
Disadvantages:
Special considerations:
|
Facebook App Web Login DialogThis login dialog is presented within the Facebook for iOS app. It is the web-based equivalent to the Facebook App Native Login Dialog. It is displayed when the native version isn't supported because an old version of the Facebook for iOS app is installed. Advantages:
Disadvantages:
|
Mobile Safari Login DialogThis Login Dialog is presented in mobile Safari. It's the fallback dialog displayed when other dialogs are not available on a device - for example, when a user doesn't have the Facebook app installed or isn't logged into Facebook via iOS. Advantages:
Disadvantages:
|
Embedded WebView Login DialogThis Login Dialog is presented in an embedded WebView in your app. This is the only dialog that will never be automatically displayed by the SDK. You have to specifically configure the code to trigger this dialog to be presented. When this dialog is presented the person's username and password must be filled in before the requested permissions are presented. Advantages:
Disadvantages:
Special considerations:
|
Controlling the login dialogs
The Facebook SDK automatically selects the optimal login dialog flow based on the account settings and capabilities of a person's device. This is the default sequence that the Facebook SDK implements:
- Facebook App Native Login Dialog
- Facebook App Web Login Dialog
- Mobile Safari Login Dialog
If the Facebook account is not set up, the Facebook SDK checks to see if the Facebook app is installed on the device. If the Facebook app is installed, the Facebook app native login dialog is presented if it's supported. If the Facebook app native login dialog can't be displayed because there's an old version of the app, the Facebook app web login dialog is presented. If the Facebook app isn't installed, mobile Safari is launched to display the Mobile Safari login dialog.
You may wish to control the fallback flow of the login dialogs.
You can do this by opening a Facebook session by calling theopenWithBehavior:completionHandler:
method on an FBSession
instance. Pass in anyFBSessionLoginBehavior*
parameter, ex:
// Initialize a session object
FBSession *session = [[FBSession alloc] init];
// Set the active session
[FBSession setActiveSession:session];
// Open the session
[session openWithBehavior:FBSessionLoginBehaviorWithNoFallbackToWebView
completionHandler:^(FBSession *session,
FBSessionState status,
NSError *error) {
// Respond to session state changes,
// ex: updating the view
}];
By default, the embedded WebView isn't part of the default flow. To add it as an option, open a Facebook session using the openWithBehavior:completionHandler:
method of the FBSession
class and pass in one of these FBSessionLoginBehavior
parameters:
Parameter | Description of flow |
---|---|
| Facebook app native > Facebook app web > mobile Safari > embedded WebView |
| embedded WebView |
The FBSessionLoginBehaviorWithNoFallbackToWebView
setting bypasses the embedded WebView.
Managing your own token cache
In this section:
- Overview
- Prerequisites
- Sample Overview
- Step 1: Set Up Local Caching
- Step 2: Set Up Remote Caching
- Troubleshooting
- Additional Info
Overview
The Facebook SDK for iOS automatically takes care of storing and fetching data related to Facebook session management for your app. The Facebook SDK does this by caching FBSession
token-related data by default. You can choose to manage the cached data yourself, by perhaps storing this data on your server. For most apps you will want to let the Facebook SDK handle the token caching, but there are several cases when you may want to do it yourself:
- If you have multiple users logging in on the same device (for which you will need to handle multiple tokens).
- If you want to provide a seamless login experience for the user accross different devices (if user logged in in one device, you show him logged in in another device using the same token).
- If you simply prefer to store the token in your server rather than locally as an extra layer of security.
Remote caching is typically done in conjunction with apps that provide their own login mechanism in addition Facebook Login.
You may also have tokens that were created through some other means. You could use your own caching strategy to import these tokens into your app.
In this section we will cover what you need to know to manage your own token cache. If you haven't done so, we recommend that you read through the understanding sessions section of this document to get an overview of how Facebook sessions work.
We will first explain how token caching works, and then we will go through an example of token caching implementation. If you want to implement support for multiple users logging in to your app, as is common for tablet apps, you may want to check in the SwitchUserSample
app, bundled with the SDK, which showcases this scenario. The SwitchUserSample
app shows how you can allow a certain number of users to log in to the app and store each user's session data under a different key inNSUserDefaults
.
If you cache session data remotely then you should disable the iOS6 native Login Dialog flow. The native login flow requires users to log in from each device. A central token caching mechanism violates this principle and may result in an inconsistent user experience during authentication.
How token caching works
The FBSessionTokenCachingStrategy
class is responsible for managing the cached data. The data is an NSDictionary
and is stored in NSUserDefaults
under a specific key. In order to handle the caching yourself, you will need to create a custom class that subclassesFBSessionTokenCachingStrategy
and overrides the methods to:
-
Save the token data:
cacheTokenInformation:
orcacheFBAccessTokenData:
. These methods are called whenFBSession
transitions to theFBSessionStateOpen
orFBSessionStateOpenTokenExtended
states, during the login flow or when additional permissions are granted. Override thecacheFBAccessTokenData:
method unless you're caching additional data that's not inFBAccessTokenData
. If you need to cache additional data, overridecacheTokenInformation:
. ThecacheFBAccessTokenData:
method takes in anFBAccessTokenData
input. -
Retrieve the token data:
fetchTokenInformation
orfetchFBAccessTokenData
. These methods are called when checking for a valid token, for example whenopenActiveSession*:allowLoginUI:NO
is called. Override thefetchFBAccessTokenData
method unless you're caching additional data that's not inFBAccessTokenData
. If caching additional data, overridefetchTokenInformation
. ThefetchFBAccessTokenData
method should return anFBAccessTokenData
object. -
Clear the token data:
clearToken
. Called whencloseAndClearTokenInformation
is called on anFBSession
object.
The FBAccessTokenData
token data contains the user's access_token
and expiration date as well as additional data that includes the date at which the token was last refreshed and the type of login that triggered the login flow, for example if it was an iOS6+ system account login.
When the Facebook SDK manages the token data cache, it stores it in NSUserDefaults
under a key named ''FBAccessTokenInformationKey''. To modify the key where the data is stored, you need to create an instance of the FBSessionTokenCachingStrategy
class using theinitWithUserDefaultTokenInformationKeyName:
method and pass it the key name that you wish to use. Then you need to pass your instance of FBSessionTokenCachingStrategy
to the FBSession class' init
method. The Facebook SDK will store the token data under the key of your choice.
Prerequisites
The code for the starting point for this doc can be found on GitHub. If you haven't done so, clone the how-to samples for the Facebook SDK 3.5:
git clone https://github.com/fbsamples/ios-3.5-howtos.git
Then open the Xcode project under the TokenCacheHowTo/Initial
folder.
Sample Overview
The completed sample lets users log in with Facebook. Behind the scenes the caching behavior is altered to showcase the following two scenarios:
-
Local device caching: In this scenario, the token data is cached locally on the device but in a different location from the default Facebook SDK location. From a user point of view the experience is no different than the default.
-
Remote server caching: In this scenario, the token data is cached on a server. The token data can now be accessed from multiple devices. This allows a first time user to log in on one device, go to a second device, launch the same app and start off with a logged in experience.
The user interface (UI) for this sample takes the person through a login flow and used Xcode debug messages to verify caching functionality:
For your reference, the completed sample is also available on GitHub. The completed sample has a flag called kLocalCache
in the MyTokenCachingStrategy.m
file that allows you to test local caching versus remote caching.
Overview of initial code
The initial Xcode project has Facebook Login implemented. It includes all the user interface components you'll need to set up the sample. What's missing is Facebook functionality that you'll add to manage your own cache.
The main classes and nib files used in the projects are:
-
AppDelegate.m
: Includes supporting code for Facebook Login implementation. -
ViewController.m
: Includes a method to handle session state callbacks that are triggered through notifications from the app delegate. The login button's method in this class calls corresponding methods in the app delegate implementation file to log in or log out a person. -
ViewController.xib
: Contains anButton
object with anaction
tied to a method in theViewController
implementation class. The button is tied to anoutlet
to control the button text to show the logged in or logged out status.
Step 1: Set Up Local Caching
In this step, you'll set up a custom class to handle the token caching tasks and modify the FBSession
open method to use your custom class.
The data that is cached is an NSDictionary
object so you can store it in a property list and make use of NSDictionary
methods that allow you to write to and read from a property list.
Step 1a: Create a custom subclass
Create a new class file by right-clicking on the project folder > New File > Objective-C class template. Name the class ''MyTokenCachingStrategy'' and select NSObject
as the subclass.
Next, open up the MyTokenCachingStrategy
header file and make the following code changes:
#import#import@interface MyTokenCachingStrategy : NSObject@interface MyTokenCachingStrategy : FBSessionTokenCachingStrategy
Open up the MyTokenCachingStrategy
implementation file. First, create properties and helper methods that define the location for the property list that's used to cache the data locally:
...
#import "MyTokenCachingStrategy.h"
// Local cache - unique file info
static NSString* kFilename = @"TokenInfo.plist";
@interface MyTokenCachingStrategy ()
@property (nonatomic, strong) NSString *tokenFilePath;
- (NSString *) filePath;
@end
@implementation MyTokenCachingStrategy
- (id) init
{
self = [super init];
if (self) {
_tokenFilePath = [self filePath];
}
return self;
}
- (NSString *) filePath {
NSArray *paths =
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *documentsDirectory = [paths lastObject];
return [documentsDirectory stringByAppendingPathComponent:kFilename];
}
...
Next, add helper methods that read and write to the property list file:
- (void) writeData:(NSDictionary *) data {
NSLog(@"File = %@ and Data = %@", self.tokenFilePath, data);
BOOL success = [data writeToFile:self.tokenFilePath atomically:YES];
if (!success) {
NSLog(@"Error writing to file");
}
}
- (NSDictionary *) readData {
NSDictionary *data = [[NSDictionary alloc] initWithContentsOfFile:self.tokenFilePath];
NSLog(@"File = %@ and data = %@", self.tokenFilePath, data);
return data;
}
Finally, implement the FBSessionTokenCachingStrategy
class methods that handle persistence, retrieval, and clearing of the cached token info:
- (void)cacheFBAccessTokenData:(FBAccessTokenData *)accessToken {
NSDictionary *tokenInformation = [accessToken dictionary];
[self writeData:tokenInformation];
}
- (FBAccessTokenData *)fetchFBAccessTokenData
{
NSDictionary *tokenInformation = [self readData];
if (nil == tokenInformation) {
return nil;
} else {
return [FBAccessTokenData createTokenFromDictionary:tokenInformation];
}
}
- (void)clearToken
{
[self writeData:[NSDictionary dictionaryWithObjectsAndKeys:nil]];
}
Step 1b: Open the session using the custom class
Open up the app delegate implementation file.
First, import the custom class header file:
#import "MyTokenCachingStrategy.h"
Next, add a private property for the token caching object:
@interface AppDelegate ()
@property (nonatomic, strong) MyTokenCachingStrategy *tokenCaching;
@end
...
Next, open up the app delegate implementation class and replace theopenSessionWithAllowLoginUI:
definition with the following:
- (BOOL)openSessionWithAllowLoginUI:(BOOL)allowLoginUI {
BOOL openSessionResult = NO;
// Set up token strategy, if needed
if (nil == _tokenCaching) {
_tokenCaching = [[MyTokenCachingStrategy alloc] init];
}
// Initialize a session object with the tokenCacheStrategy
FBSession *session = [[FBSession alloc] initWithAppID:nil
permissions:@[@"public_profile"]
urlSchemeSuffix:nil
tokenCacheStrategy:_tokenCaching];
// If showing the login UI, or if a cached token is available,
// then open the session.
if (allowLoginUI || session.state == FBSessionStateCreatedTokenLoaded) {
// For debugging purposes log if cached token was found
if (session.state == FBSessionStateCreatedTokenLoaded) {
NSLog(@"Cached token found.");
}
// Set the active session
[FBSession setActiveSession:session];
// Open the session.
[session openWithBehavior:FBSessionLoginBehaviorUseSystemAccountIfPresent
completionHandler:^(FBSession *session,
FBSessionState state,
NSError *error) {
[self sessionStateChanged:session
state:state
error:error];
}];
// Return the result - will be set to open immediately from the session
// open call if a cached token was previously found.
openSessionResult = session.isOpen;
}
return openSessionResult;
}
Build and run the project to make sure it runs without errors. Before the login button is clicked, you should see a debug message that the token data found is null
. Tap the ''Login'' button to log in with Facebook. Once authenticated, the button text should change to ''Logout''. After the login flow is completed, you should see messages showing the token data that is saved.
Stop the running app from Xcode. On your test device, double-tap the Home button and stop the app from running there as well. Launch the app to test the cached data fetching flow. The button should say ''Logout'' as cached data is read from your cached location.
Restart the app from Xcode. Tap the ''Logout'' button and verify that the button text changes to ''Login''. You should see a debug message that the token data is empty. Stop the running app from Xcode once more and make sure the app is not running on your test device. Launch the app. The button should say ''Login'' as no token data has been found in the cache.
Test with an iOS6+ device where you've logged in to the Facebook account on the system. Verify that the login flow uses the iOS native Login Dialog.
Step 2: Set Up Remote Caching
In this step, you'll store the token data on a server instead of locally.
You'll add server-side code to process the incoming token data and client-side code to send and receive this data.
Step 2a: Set up the server-side code
To test out server-side cached data storage, you'll set up a simple endpoint that handles HTTP POST requests to store token data and HTTP GET requests to fetch token data. The endpoint returns a JSON response with a ''success'' parameter that is set to ''true'' when the data is stored or retrieved successfully and is set to ''false'' in other cases.
If you don't have your own back-end server, consider using Parse. The server-side sample code is written in PHP but you can take the same concepts and apply it to the implementation stack you support.
Create a file and name it ''token.php'' and host it on your server. Add the following content to the file:
<?php
// Copyright 2004-present Facebook. All Rights Reserved.
// Enforce https on production
if ($_SERVER['HTTP_X_FORWARDED_PROTO'] == "http" && $_SERVER['REMOTE_ADDR'] != '127.0.0.1') {
header("Location: https://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"]);
exit();
}
// If POST, save token data and return a success flag.
// If GET, check the unique info to send back a saved token data.
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (isset($_REQUEST['token_info']) && isset($_REQUEST['unique_id'])) {
// Get the unique id passed in
$unique_id = strip_tags($_REQUEST['unique_id']);
// Use the unique id to create the data storage file
$token_filename = $unique_id . '.txt';
$file = dirname(__FILE__) . '/data/' . $token_filename;
// Get the token data info
$auth_response['token_info'] = strip_tags($_REQUEST['token_info']);
// JSON encode the data
$data = json_encode($auth_response);
// Create a new file or overwrite file
if (file_put_contents($file, $data) === false) {
$response['status'] = 'false';
$response['errorCode'] = '50001';
$response['errorMessage'] = 'Could not write file contents.';
} else {
$response['status'] = 'true';
}
} else {
$response['status'] = 'false';
$response['errorCode'] = '30001';
$response['errorMessage'] = 'Invalid data input.';
}
} else if ($_SERVER['REQUEST_METHOD'] == 'GET') {
if (isset($_REQUEST['unique_id'])) {
// Get the unique id passed in
$unique_id = strip_tags($_REQUEST['unique_id']);
// Use the unique id to find the file to check
$token_filename = $unique_id . '.txt';
$file = dirname(__FILE__) . '/data/' . $token_filename;
// Get the file contents and decode the JSON info
$data = json_decode(file_get_contents($file));
if (($data === false) || empty($data)) {
$response['status'] = 'false';
$response['errorCode'] = '50002';
$response['errorMessage'] = 'Could not read file contents or data empty.';
} else {
// Return the token data
$response['status'] = 'true';
$response['token_info'] = $data->token_info;
}
} else {
$response['status'] = 'false';
$response['errorCode'] = '30001';
$response['errorMessage'] = 'Invalid data input.';
}
} else {
$response['status'] = 'false';
$response['errorCode'] = '50003';
$response['errorMessage'] = 'Unsupported method: ' . $_SERVER['REQUEST_METHOD'];
}
echo json_encode($response);
Create a directory called ''data'' in the same directory where the PHP file is hosted. If you store the data in a different directory, ex: outside the document web root, be sure to modify the ''token.php'' file accordingly.
Note: For security reasons, in a production set up you would not store the token data in the filesystem, especially if it's easily accessible from the web. A real world scenario would involve storing the info in a database. Additionally, the unique_id
could be represented by a third party session that's generated by your server. So one possible user flow that's more secure could be the following:
- User opens up your app and is authenticated against your server via a different login mechanism. Your app gets a session (third-party session) from the server.
- Your app uses the third-party session info to make a call to your server to check for a cached Facebook token.
- When the Facebook token data needs to be stored, your server is called using the the third-party session info. This session info is used to retrieve user info and the user info is in turn used to store the Facebook token data.
- The next time the user opens your app and the cached token needs to be checked, the server code maps the third-party session to a user, checks it's database to retrieve the token data.
Step 2b: Modify the custom class for remote storage
Open up the MyTokenCachingStrategy
header file and add a public property that is used to identify the user for remote caching support:
// In a real app this uniquely identifies the user and is something
// the app knows before an FBSession open is attempted.
@property (nonatomic, strong) NSString *thirdPartySessionId;
Open up the MyTokenCachingStrategy
implementation file. Make the following modifications to remove the property and helper methods related to local file caching and add those used for remote data caching:
... #import "MyTokenCachingStrategy.h"// Local cache - unique file info static NSString* kFilename = @"TokenInfo.plist"; @interface MyTokenCachingStrategy () @property (nonatomic, strong) NSString *tokenFilePath; - (NSString *) filePath; @end@implementation MyTokenCachingStrategy - (id) init { self = [super init]; if (self) {_tokenFilePath = [self filePath];_thirdPartySessionId = @""; } return self; }- (NSString *) filePath { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths lastObject]; return [documentsDirectory stringByAppendingPathComponent:kFilename]; }...
You'll see errors in the writeData:
and readData
methods due to the deleted tokenFilePath
property. You'll be swapping out these methods shortly so ignore the errors for now.
Next, add code to set up the remote caching:
// Remote cache - back-end server
static NSString* kBackendURL = @"<YOUR_BACKEND_SERVER>/token.php";
// Remote cache - date format
static NSString* kDateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZZZ";
@implementation MyTokenCachingStrategy
...
Replace <YOUR_BACKEND_SERVER>
with the path to the endpoint where you've stored token.php
or your token caching endpoint.
Next, add helper code used to process the server's response. This is be used by the writeData:
andreadData
methods:
/*
* Helper method to look for strings that represent dates and
* convert them to NSDate objects.
*/
- (NSMutableDictionary *) dictionaryDateParse: (NSDictionary *) data {
// Date format for date checks
NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:kDateFormat];
// Dictionary to return
NSMutableDictionary *resultDictionary = [[NSMutableDictionary alloc] init];
// Enumerate through the input dictionary
[data enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
// Check if strings are dates
if ([obj isKindOfClass:[NSString class]]) {
NSDate *objDate = nil;
BOOL isDate = [dateFormatter getObjectValue:&objDate
forString:obj
errorDescription:nil];
if (isDate) {
resultDictionary[key] = objDate;
[resultDictionary setObject:objDate forKey:key];
} else {
resultDictionary[key] = obj;
}
} else {
// Non-string, just keep as-is
resultDictionary[key] = obj;
}
}];
return resultDictionary;
}
/*
* Helper method to check the back-end server response
* for both reads and writes.
*/
- (NSDictionary *) handleResponse:(NSData *)responseData {
NSError *jsonError = nil;
id result = [NSJSONSerialization JSONObjectWithData:responseData
options:0
error:&jsonError];
if (jsonError) {
return nil;
}
// Check for a properly formatted response
if ([result isKindOfClass:[NSDictionary class]] &&
result[@"status"]) {
// Check if we got a success case back
BOOL success = [result[@"status"] boolValue];
if (!success) {
// Handle the error case
NSLog(@"Error: %@", result[@"errorMessage"]);
return nil;
} else {
// Check for returned token data (in the case of read requests)
if (result[@"token_info"]) {
// Create an NSDictionary of the token data
NSData *jsonData = [result[@"token_info"]
dataUsingEncoding:NSUTF8StringEncoding];
if (jsonData) {
jsonError = nil;
NSDictionary *tokenResult =
[NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:&jsonError];
if (jsonError) {
return nil;
}
// Check if valid data returned, i.e. not nil
if ([tokenResult isKindOfClass:[NSDictionary class]]) {
// Parse the results to handle conversion for
// date values.
return [self dictionaryDateParse:tokenResult];
} else {
return nil;
}
} else {
return nil;
}
} else {
return nil;
}
}
} else {
NSLog(@"Error, did not get any data back");
return nil;
}
}
The handleResponse:
helper method is called after a response is received from reads or writes. In the case of reads, the code calls the dictionaryDateParse:
method to convert any NSString
objects that represent dates into NSDate
objects. This is to make sure that the returned token data is in the expected format.
Next, add the code that writes token data to the server. Replace the previously existing writeData:
method:
- (void) writeData:(NSDictionary *) data {
NSLog(@"Write - Data = %@", data);
NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:kDateFormat];
NSError *error = nil;
NSString *jsonDataString = @"";
if (nil != data) {
NSMutableDictionary *copyData = [data mutableCopy];
// Enumerate through the input dictionary
[data enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop) {
if([object isKindOfClass:[NSDate class]]) {
copyData[key] = [dateFormatter stringFromDate:object];
} else {
copyData[key] = object;
}
}];
NSData *jsonData = [NSJSONSerialization
dataWithJSONObject:copyData
options:0
error:&error];
if (error) {
NSLog(@"JSON error: %@", error);
return;
}
jsonDataString = [[NSString alloc] initWithData:jsonData
encoding:NSUTF8StringEncoding];
}
NSURLResponse *response = nil;
error = nil;
// Set up a URL request to the back-end server
NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:
[NSURL URLWithString:kBackendURL]];
// Configure an HTTP POST
[urlRequest setHTTPMethod:@"POST"];
// Pass in post data: the unique ID and the JSON string
// representation of the token data.
NSString *postData = [NSString stringWithFormat:@"unique_id=%@&token_info=%@",
self.thirdPartySessionId,jsonDataString];
[urlRequest setHTTPBody:[postData dataUsingEncoding:NSUTF8StringEncoding]];
// Make a synchronous request
NSData *responseData = (NSMutableData *)[NSURLConnection
sendSynchronousRequest:urlRequest
returningResponse:&response
error:&error];
// Process the returned data
[self handleResponse:responseData];
}
Finally, add code to read the cached token data from the server and make use of the helper methods you've just defined. This code replaces the previous readData
method:
- (NSDictionary *) readData {
NSURLResponse *response = nil;
NSError *error = nil;
// Set up a URL request to the back-end server, a
// GET request with the unique ID passed in.
NSString *urlString = [NSString stringWithFormat:@"%@?unique_id=%@",
kBackendURL, self.thirdPartySessionId];
NSURLRequest *urlRequest = [[NSURLRequest alloc] initWithURL:
[NSURL URLWithString:urlString]];
// Make a synchronous request
NSData *responseData = (NSMutableData *)[NSURLConnection
sendSynchronousRequest:urlRequest
returningResponse:&response
error:&error];
if (nil != responseData) {
// Process the returned data
return [self handleResponse:responseData];
} else {
return nil;
}
}
You'll notice that the token data reads and writes are synchronous HTTP calls that block the user interface. This is a simple mechanism for this sample app to ensure that the FBSession
is in the correct state whenever the app is launched. You should consider making your real-world app requests asynchronous, especially if Facebook Login is not an immediate requirement for your app's functionality.
Step 2c: Modify the session open code
Make code changes to set the property that uniquely identifies a user. You'll set this property when you initialize the MyTokenCachingStrategy
instance. Open up the app delegate implementation file and add modify the relevant code in the openSessionWithAllowLoginUI:
method:
...
if (nil == _tokenCaching) {
_tokenCaching = [[MyTokenCachingStrategy alloc] init];
// Hard-code for demo purposes, should be set to
// a unique value that identifies the user of the app.
[_tokenCaching setThirdPartySessionId:@"213465780"];
}
...
Limit the type of login you allow, to guard against inconsistencies if one of the user's devices does not have iOS6. You'll basically disable the ability to log in using the iOS system's Facebook account credentials. Make the following change in the openSessionWithAllowLoginUI:
method found in the app delegate implementation file:
[session openWithBehavior:FBSessionLoginBehaviorUseSystemAccountIfPresent[session openWithBehavior:FBSessionLoginBehaviorWithFallbackToWebView
Delete the app from your test device.
Build and run the project to make sure it runs without errors. Before the login button is clicked, you should see a debug message that no data was found. Tap the ''Login'' button to log in with Facebook. Once authenticated, the button text should change to ''Logout''. After the login flow is completed, you should see messages showing the token data that is saved.
Stop the running app from Xcode. On your test device, double-tap the Home button and stop the app from running there as well. Launch the app to test the cached data fetching flow. The button should say ''Logout'' as cached data is read from your cached location.
Restart the app from Xcode. Tap the ''Logout'' button and verify that the button text changes to ''Login''. You should see a debug message that the token data is empty. Stop the running app from Xcode once more and make sure the app is not running on your test device. Launch the app. The button should say ''Login'' as no token data has been found in the cache.
To test the central caching feature, log in to cache the token data on the server. Delete the app from the test device to simulate the user going to a new device. Restart the app from Xcode. The app should start in an authenticated state as the cached token data is read from the server. You should see a debug message that a cached token was found.
Test with an iOS6+ device where you've logged in to the Facebook account on the system. Verify that the login flow does not use the iOS native Login Dialog.
Troubleshooting
Additional Info
- FBSession: reference for the session class.
- FBSessionTokenCachingStrategy: reference for the token caching strategy class.