Facebook Login Flow & Best Practices

Best practice for Facebook login flow with the JavaScript SDK and PHP SDK v4.1

Jan 09, 2015

I get a lot of people asking me about the best practice for Facebook login flow. Most of us are used to logging people in using an email address and a hash of their password, but how do you log a user in from Facebook when they never enter a password on your site?

Let's look at the best practices for logging a Facebook user into your web app.

Note: As of writing (Jan 9, 2015) the latest stable version of the Facebook PHP SDK is v4.0. Once v4.1 is released (probably within a month or so), it will have a very different implementation. For this reason, all of the examples included in this post are for v4.1 of the SDK which is currently still in development mode.

Create A Facebook App

If you haven't already, become a Facebook developer and create an app.

You'll need to configure your app to use the "website" platform. To do this you'll need to provide the root URL of your web app.

Make sure to take note of the app ID and app secret because you'll need those later on.

Understanding User Permissions

Your app will need to ask the user to grant your app certain permissions. The minium permission you can ask a user for is the public_profile permission which grants your app access to the user's public profile. This access is very limited and does not give you access to the user's email or friends list.

Here are a few examples of actions that will require user approval:

Meet The Graph API

Before you log a user in, you'll need to understand where the user's information is coming from. You can perform CRUD (create, read, update, delete) operations against data on Facebook via the Graph API.

Tip: Facebook provides a neat little tool called the Graph API explorer which allows you to play with features of the Graph API within the context of a nice GUI.

Understanding the Graph API is a fairly big subject. Here's a TL;DR on the Graph API:

A Note On Graph Endpoints: Some people read the Graph API reference docs and erroneously assume that since the docs refer to named endpoints that you use the name of the endpoint in your request like /node/{node-id}. You don't need to use the name of the node in the endpoint. Using the /comment endpoint as an example, the endpoint would just contain the comment ID /{comment-id} and would not be prefixed with the name of the endpoint in the documentation /comment/{comment-id}.

How To Log A User In

Facebook users are authenticated via the Graph API using OAuth. If OAuth sounds scary to you, no worries, there are tools to help you authenticate a user without knowing how OAuth works.

For a web app, there are two ways to log a Facebook user into your site.

  1. Using the JavaScript SDK (easiest)
  2. Manual OAuth 2.0 authentication

Manual OAuth 2.0 authentication can be done using the Facebook PHP SDK v4.1 which we'll discuss below.

Logging in with the JavaScript SDK

You can authenticate a Facebook user using just a few lines of JavaScript with the JavaScript SDK. The SDK does all the OAuth heavy lifting behind the scenes for you.

Once you've included the required JavaScript snippet in your HTML to load the SDK, you can make use of FB.login() to log the user in.

Configuring the JavaScript SDK

In your initialization snippet, I recommend setting the cookie option to true. This will tell the JavaScript SDK to set a cookie on your domain with information about the user. This will allow you to access the authentication data in the back-end later on using the PHP SDK for example. For more on this see, "Using The JavaScript SDK And PHP SDK Together" below.

FB.init({
    appId   : '{your-app-id}',
    cookie  : true,
    version : 'v2.2'
});

Grabbing User Data From A Signed Request

The cookie that the JavaScript SDK created contains a signed request.

A signed request contains a payload of data about the user who authorized your app. The payload is delivered as a base64-encoded JSON string that has been escaped for use in a URL. It also contains a signature hash to validate against to ensure the data is coming from Facebook.

The cookie is named fbsr_{your-app-id} and it contains data that looks something like this:

CBVDhaIKcEfoKiL-Hxu6TONsY62edUMdIVHhszTBxcI=.eyJoZWxsbyI6IllvdSBhcmUgYSBzbWFydCBjb29raWUgZm9yIGZpbmRpbmcgdGhpcyEgTG92ZSwgU2FtbXlLIiwiYWxnb3JpdGhtIjoiSE1BQy1TSEEyNTYiLCJpc3N1ZWRfYXQiOjE0MjA3MzcxMTh9

That gibberish is the signed request.

Extra credit: The signed request above is one I made myself using the app secret foo_secret. See if you can decode & validate the payload to see a hidden message. If you succeed, let me know!

Making Graph Requests in JavaScript

The JavaScript SDK provides a method called, FB.api() which sends requests to the Graph API on behalf of the logged in user.

In this example we grab the id, name and email of the logged in user. This assumes we've already logged the user in with FB.login().

FB.api('/me?fields=id,name,email', function(response) {
  console.log(response);
});

Alternatively, we could have written the same request like so:

FB.api('/me', {fields: 'id,name,email'}, function(response) {
  console.log(response);
});

Note that we didn't specify an access_token parameter. We could have specified one, but the JavaScript SDK will fallback to the access token of the authenticated user.

Logging in with the PHP SDK v4.1

Another way of logging a user in with Facebook is by using the Facebook PHP SDK v4.1.

Documentation on v4.1: You can see the most up-to-date docs for v4.1 in the GitHub repo since as of writing v4.1 has not yet been officially released and the documentation hasn't been updated on Facebook's documentation site.

The Flow For Manual Login With Redirect URL's

If we didn't have the PHP SDK, we would manually log a user in using the following flow:

  1. Present a user with a special link to log in with Facebook.
  2. The user clicks the link and is taken to www.facebook.com where they are immediately prompted with a dialog asking them to grant your app access to whatever permissions you wanted to ask for.
  3. After the user responds (accept or reject), they will be redirected back to the callback URL you specified in the login link. The callback URL will contain several GET parameters.
  4. If the user accepted the request the response will contain a code GET param (which can be exchanged for an access token).
  5. If the user rejected the request a number of "error" GET params (error, error_code, error_reason, & error_description) will be returned.
  6. A state GET param will also be returned containing the cryptographically-secure random string you originally generated in the login URL to validate against CSRF attacks and ensure that the response came from Facebook.

Many of those steps are handled by the PHP SDK for you. So you don't have to worry about generating and validating the CSRF or exchanging the code for an access token for example.

Installing the PHP SDK v4.1 With Composer

If you're using composer, you can just add the SDK to your composer.json file.

{
    "require": {
        "facebook/php-sdk-v4": "~4.1.0@dev"
    }
}

Normally you wouldn't want to use the @dev stability flag but since v4.1 of the SDK has not been released yet, it's still in development mode. Composer won't install packages in development mode by default so you'll need to allow the minium stability of "development" before Composer will installed it. After the v4.1 is officially released (I'm going to guess end of Feb 2015?) you should be able to install it without setting the stability flag to @dev.

{
    "require": {
        "facebook/php-sdk-v4": "~4.1.0"
    }
}

A Note About Versioning: The Facebook PHP SDK does not follow semantic versioning so major releases happen at the second level of the version number. The release versions look like: 4.MAJOR.MINOR|PATCH. So if you were to set the version number to "~4.1" in your composer.json, the next major release v4.2 with breaking changes would automatically be upgraded when you run composer update. To prevent this, make sure to include the version number down to MINOR|PATCH: "~4.1.0".

Installing the PHP SDK v4.1 Without Composer

If you're not using Composer (you're still not using Composer!?), you can manually download the code, unzip it and include the autoloader.

require('/path/to/src/Facebook/autoload.php');

Initializing The PHP SDK

Grab your app ID and app secret from the app dashboard and use them to instantiate a new Facebook\Facebook() service object.

$fb = new Facebook\Facebook([
  'app_id' => '{app-id}',
  'app_secret' => '{app-secret}',
  ]);

Generating The Login Link

The login link contains a number of key parameters including:

  1. The app ID in the client_id param
  2. The state param which is a cryptographically-secure random string to validate against CSRF attacks
  3. The callback URL param redirect_uri which is where the user will be redirected to after the user responds to the app authentication request
  4. The scope param which is a comma-separated list of permissions

...and a few others. The SDK makes it easy to generate the login with the FacebookRedirectLoginHelper.

# login.php

// Get the FacebookRedirectLoginHelper
$helper = $fb->getRedirectLoginHelper();

$permissions = ['email', 'user_likes']; // optional
$loginUrl = $helper->getLoginUrl('https://{your-website}/login-callback.php', $permissions);

echo '<a href="' . $loginUrl . '">Log in with Facebook!</a>';

Callback URL: The callback URL that you specify such as https://{your-website}/ in the example above, needs to match the URL you provided in your Facebook app settings.

Handling The Callback Response

In the callback URL /login-callback.php you can do a check for a successful response and obtain the access token; otherwise display an error.

# login-callback.php

// Get the FacebookRedirectLoginHelper
$helper = $fb->getRedirectLoginHelper();
// @TODO This is going away soon
$facebookClient = $fb->getClient();

try {
    $accessToken = $helper->getAccessToken($facebookClient);
} catch(Facebook\Exceptions\FacebookResponseException $e) {
    // When Graph returns an error
    echo 'Graph returned an error: ' . $e->getMessage();
    exit;
} catch(Facebook\Exceptions\FacebookSDKException $e) {
    // When validation fails or other local issues
    echo 'Facebook SDK returned an error: ' . $e->getMessage();
    exit;
}

if (isset($accessToken)) {
    // Logged in
    // Store the $accessToken in a PHP session
    // You can also set the user as "logged in" at this point
} elseif ($helper->getError()) {
    // There was an error (user probably rejected the request)
    echo '<p>Error: ' . $helper->getError();
    echo '<p>Code: ' . $helper->getErrorCode();
    echo '<p>Reason: ' . $helper->getErrorReason();
    echo '<p>Description: ' . $helper->getErrorDescription();
    exit;
}

The Access Token Is Not A String: The PHP SDK returns the access token as a Facebook\AccessToken entity which contains the full access token, the expiration date & machine ID if it exists. It also contains a number of methods to easily manage access tokens. To get the access token as a string, you can type cast it using the (string) syntax: $tokenAsString = (string) $accessToken;. The Facebook\AccessToken entity can also be serialize()'ed to maintain all of the original data.

After you obtain a user access token, you can just store it in a PHP session (either serialized or as a string) to make requests to the Graph API on behalf of the user.

This is also the point in which you would mark the user as "logged in" in your web framework (see "Managing The User's 'Logged In' State With The Web Framework" below).

Setting The Default Access Token

Once you have an access token stored in a PHP session or in your database, you can set it as the default fallback access token in the constructor of the Facebook\Facebook() service class. The default access token will be used as a fallback access token if one is not provided for a specific request.

$fb = new Facebook\Facebook([
  'app_id' => '{app-id}',
  'app_secret' => '{app-secret}',
  'default_access_token' => '{default-access-token}',
  ]);

Alternatively if you already have an instance of Facebook\Facebook(), you can set the default fallback access token using the setDefaultAccessToken() method.

$fb->setDefaultAccessToken('{default-access-token}');

Making Graph Requests With The PHP SDK v4.1

The PHP SDK supports GET, POST, & DELETE requests using the get(), post(), & delete() methods respectfully.

In this example we grab the id, name and email of the logged in user. This assumes we've already obtained an access token for the user.

try {
    // Returns a `Facebook\FacebookResponse` object
    $response = $fb->get('/me?fields=id,name,email', '{access-token}');
} catch(Facebook\Exceptions\FacebookResponseException $e) {
    echo 'Graph returned an error: ' . $e->getMessage();
    exit;
} catch(Facebook\Exceptions\FacebookSDKException $e) {
    echo 'Facebook SDK returned an error: ' . $e->getMessage();
    exit;
}

// Returns a `Facebook\GraphNodes\GraphUser` collection
$user = $response->getGraphUser();

echo 'Name: ' . $user['name'];
// OR
// echo 'Name: ' . $user->getName();

There's a lot going on behind the scenes so let's explain step-by-step:

  1. The get() method uses {access-token} to obtain the endpoint /me. If {access-token} wasn't specified, the default fallback access token would be used if it was set as described above.
  2. A FacebookResponseException is thrown if there was an error response from the Graph API.
  3. A FacebookSDKException is thrown if there was an error building the request (like unable to read a local file that was set for upload for example).
  4. The get() method returns a Facebook\FacebookResponse object which represents an HTTP response. This contains methods for debugging the response like getHttpStatusCode() and getBody().
  5. The SDK does a really great job of analyzing the JSON response from Graph and converting it into something useful like a GraphObject collection. To convert the response into a GraphObject collection we use $user = $response->getGraphUser().
  6. The $user variable is an instance of the Facebook\GraphNodes\GraphUser collection which has handy methods like getEmail() and getHometown().

Using the JavaScript SDK and PHP SDK together

We could code all of our Facebook integration stuff in just JavaScript or just PHP, but if we use the JavaScript SDK and PHP SDK's in tandem we get a bit more power and flexibility.

Login With JavaScript And Get Access Token With PHP

It's very common to log a user in with the JavaScript SDK and then grab the access token it generates with the PHP SDK. This is a better experience for the user as the login dialog is displayed directly on top of your web app and the user is never redirected to the Facebook website.

After you've logged a user in with the JavaScript SDK (as described above), you can use window.location.replace("fb-login-callback.php"); to redirect them to a callback URL. In the callback URL you can make use of the FacebookJavaScriptHelper in the Facebook PHP SDK to obtain the access token from the cookie that was set by the JavaScript SDK.

# fb-login-callback.php
$jsHelper = $fb->getJavaScriptHelper();
// @TODO This is going away soon
$facebookClient = $fb->getClient();

try {
    $accessToken = $jsHelper->getAccessToken($facebookClient);
} catch(Facebook\Exceptions\FacebookResponseException $e) {
    // When Graph returns an error
    echo 'Graph returned an error: ' . $e->getMessage();
} catch(Facebook\Exceptions\FacebookSDKException $e) {
    // When validation fails or other local issues
    echo 'Facebook SDK returned an error: ' . $e->getMessage();
}

if (isset($accessToken)) {
    // Logged in.
} else {
    // Unable to read JavaScript SDK cookie
}

Troubleshooting: If the $jsHelper->getAccessToken() is just returning null and not throwing any exceptions, that means the cookie from the JavaScript SDK is not set. Make sure you have the option cookie: true in your FB.init() as described earier. If this is not set, the JavaScript SDK will not save a cookie on your site.

Managing The User's "Logged In" State With The Web Framework

When logging a user in with Facebook, it can be confusing to know how and when to mark a user as "logged in" within the context of your web framework (Laravel, Symfony, a custom framework, etc).

The Login States

There are three login states of a user on Facebook:

And in your own web framework you have two states (more or less):

It gets even hairier when we start working with extended permissions. When a user logs in they can choose not to grant certain permissions while still granting other permissions. And a user can always revoke specific permissions in the future or even revoke your whole app (which you can track using a deauthorize callback URL).

So how do we juggle all those states? Simple - boil them down into two states: "logged in & "not logged in".

Determining If User Is "Logged In"

In a typical web app, we would would determine that the user was logged in by matching their email/username and a hash of their password in the database. If a match is found, the user's status is set to "logged in" with the web framework.

The JavaScript SDK and PHP SDK have all the proper data & communication validation features to ensure that the "login with Facebook" process is secure and coming from Facebook. So as long as we use the official SDK's to log the user in we can be sure that once we obtain the user access token, we can mark that user as "logged in" in our web framework.

We just need to check the database for their Facebook user ID - no need to check passwords or store & validate the access token or anything like that. (See "Saving The Facebook User ID In The Database" below).

Logging A User Out

The JavaScript SDK provides a method to log a user out called FB.logout(). And the PHP SDK provides a method to generate a logout URL called getLogoutUrl().

It's important to note that these logout methods will revoke the user's access token for your app and also log the user out of Facebook. So it's not common to use this functionality as most people like to just stay logged into Facebook all the time.

Plus it would be weird that every time a user logged out of your web app, "Acme, Inc." they also get logged out of Facebook.

So all you need to do to log a user out is log them out using your web framework (like deleting the session or whatever logout convention your web framework uses). You don't need to tell Facebook about the log out action in any way.

Saving The User In The Database

Deciding what information about a user to save in the database isn't always trivial. And not to mention Facebook requires that you give users control of the data you store about them and that the data you store isn't used improperly.

Saving The Facebook User ID In The Database

After you've authenticated a user, you will have access to their Facebook user ID which has been scoped to your Facebook app. At minimun you'll want to store the user's Facebook ID in your database and the user ID is the only thing you'll need to store in the database to authenticate a user with your web framework.

Watch Out For Big Numbers: User ID's on Facebook are really big. Bigger than a normal signed INT in most cases. So you'll want to store the user ID as an unsigned BIGINT in your database. I've seen developers use an indexed VARCHAR 255 to store the Facebook ID's as well. In any case, make sure to avoid a plain-old signed INT to store the user ID as that will cause some unexpected behavior. Learn more about signed vs unsigned.

My usual flow for logging a user in and saving their ID in the database goes something like this:

  1. Obtain a user access token (via the JavaScript or PHP SDK).
  2. Grab the user id and any other user data, e.g. name & email from the Graph API using the user access token.
  3. Check the database for the existence of the user ID.
  4. New Users: If the user ID cannot be found in the datase, create a new entry for the user in the database using the data you obtained from Graph in step 2.
  5. Returning Users: If the user ID is found in the datase, this is a returning user.
  6. Set the user's log-in status as "logged in" with the web framework.

Saving The User Access Token In The Database

You don't need to save the user access token in the database unless you plan on making requests to the Graph API on the user's behalf later on when the user is not actively using your web app.

Access Tokens Don't Last Long: By default an access token will only last about 2 hours so you'll want to extend the short-lived access token with a long-lived access token. Then you can save the long-lived access token in your database.

Asking For More Permissions

It's a best practice to ask for the bare minimum permissions that your app needs to function when a user first logs into your app. You'll get more people logging into your app if you do this.

If you have a feature that posts something to a user's timeline on their behalf, for example, you'll need the publish_actions permission. This is an ugly permission to ask for and most people (myself included) won't grant an app permission to post on their behalf - especially if the app asks for it right up front.

So just initially log the user in with the most basic permissions you need and then when you need to use the feature that posts on the user's behalf, ask the user for the publish_actions permission.

To ask for new permissions, go through one of the login processes above with the publish_actions permission in the scope list.

Update your access token! Since access tokens are tied to the permissions the user granted your app, you'll need to use the new access token returned from Facebook after requesting additional permissions from the user.

Outro

Logging users in with Facebook is pretty easy with the JavaScript SDK, but knowing what to do after the user granted your app permissions isn't exactly trivial.

Once you wrap your head around what it means for a user to be "logged in" to your app, things should become much easier for you.

Good luck with integrating Facebook Login into your web app! And if you were able to decode the signed request I created above, let me know you cracked it!

If you found this guide helpful, say, "Hi" on twitter! I'd love to hear from you. :)