This document refers to the iOS SDK v3. For the new iOS SDK v4 please see our new iOS docs.
The Facebook SDK for iOS automatically takes care of storing and fetching data on Facebook session management for your app. There are some cases when you may want to do it yourself:
Remote caching is typically done in apps that provide their own login mechanism in addition Facebook Login.
We recommend that you read through the Sessions on iOS.
FBSessionTokenCachingStrategy class manages the cached data. The data is an NSDictionary and is stored in NSUserDefaults under a specific key. To handle the caching yourself, you will need to create a custom class that subclasses FBSessionTokenCachingStrategy and overrides the methods to:
Save the token data - When FBSession changes to FBSessionStateOpen or FBSessionStateOpenTokenExtended states during login or when additional permissions are granted override cacheTokenInformation: or cacheFBAccessTokenData:. Override cacheFBAccessTokenData: method unless you're caching additional data not in FBAccessTokenData. If you need to cache additional data, override cacheTokenInformation:. The cacheFBAccessTokenData: method takes in FBAccessTokenData input.
Retrieve the token data - Override fetchTokenInformation or fetchFBAccessTokenData when your app checks for a valid token, for example when it calls openActiveSession*:allowLoginUI:NO. Override the fetchFBAccessTokenData method unless you're caching additional data that's not in FBAccessTokenData. If caching additional data, override fetchTokenInformation. fetchFBAccessTokenData returns an FBAccessTokenData object.
Clear the token data - Override clearToken. This gets called when closeAndClearTokenInformation is called on an FBSession object.
The FBAccessTokenData token data contains the user's access_token and expiration date. It also contains the date when the token was last refreshed and the type of login that triggered the login, such as 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 the initWithUserDefaultTokenInformationKeyName: 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.
The examples below show two different caching scenarios:
Local device caching - Cache 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: - Cached on a server for access by 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 complete sample is at GitHub. The completed sample has a flag calledkLocalCache in the MyTokenCachingStrategy.m file that allows you to test local caching versus remote caching.
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 code for Facebook Login.
ViewController.m: Handles 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 an Button object with an action tied to a method in the ViewController implementation class. The button is tied to an outlet to control the button text to show the logged in or logged out status.
Set up a custom class to handle the token caching tasks and modify the FBSession open method to use your custom class. The data is cached as an NSDictionary object so you can store it in a property list and use of NSDictionary methods to write to and read from a property list.
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.
Open MyTokenCachingStrategy header file and make the following code changes:
#import <Foundation/Foundation.h> #import <FacebookSDK/FBSessionTokenCachingStrategy.h> @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 to 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]];
} 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 the openSessionWithAllowLoginUI: 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.
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.
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:
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: and readData 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.
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.
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.