NOTE: I had a version of this blog post in draft mode for months addressing the old (PHP SDK v2.1.2) Facebook PHP library. In a fit of momentum, I am publishing this post now, updated to use the new library (PHP SDK v3.1.1). Since I am not as familiar with the new one, there may be better ways to do the following, although the code below works.
Facebook authentication, much like the rest of the platform, can be maddening. It appears easy enough at 1st glance, until you realize the contrived XFBML examples in the docs will not get you very far. So then you might take a look at the advanced document. And then realize that the advance document, that giant, complicated, very detailed and thorough explanation of OAuth 2.0 as implemented by Facebook, does not actually help you code your website.
This blog post is more for my benefit than yours, and serves to summarize my current Facebook authentication strategy, so I don’t have to figure it out over and over. At least, until Facebook changes it.
My Requirements
First off, realize that there are 3 ways to do any particular thing on the Facebook platform: XFBML, JS, and PHP (server-side).
That said, I do not use the XFBML <fb:login/>
button. XFBML is great for non-developers who can copy and paste the documentation into a website and impress their friends. But XFBML is slower, ties your user’s application session to your user’s Facebook session, and you can’t make general API calls, so you may as well learn how to use the JS or PHP SDKs anyway.
Implementation
First, I need a login login link that non-JS browsers can use.
<a id="fb-login" href="<?= $loginUrl ?>">Login</a>
That $loginUrl
comes from:
$loginUrl = $facebook->getLoginUrl(array('scope' => 'offline_access'));
The login link will ask the user for offline_access
permission, as an example.
Then we need some the FB JS SDK to handle the link for those of us with modern browsers.
FB.init( { appId: 'XXXXXX', status: true, cookie: false, oauth: true } );
$('#fb-login').click(function(e) {
e.preventDefault();
FB.getLoginStatus(function(response) {
// maintain application anchor/query string, if any
q = window.location.search.substring(1);
if (window.location.hash) {
q = window.location.hash.split('?')[1];
}
// if already logged in, redirect
if (response.authResponse) {
window.location.href = '/?signed_request=' + response.authResponse.signedRequest + '&' + q;
} else {
// else present user with FB auth popup
FB.login(function(response) {
// if user logs in successfully, redirect
if (response.authResponse) {
window.location.href = '/?signed_request=' + response.authResponse.signedRequest + '&' + q;
}
}, { scope:'offline_access' } );
}
});
});
Couple interesting things to point out:
FB.init()
, the cookie
flag is set to false
. In most FB examples, this flag is set to true
. If this flag is true
, the JS will set a cookie (that begins with ‘fbsr_
’) that will keep the user connected to your Facebook app during a browser session. This can confuse your app, because if you do not clear this cookie when your user logs out, your app may automatically re-authenticate your user. If you do intend to use your user’s Facebook session as your website’s session, set this flag to true
. window.location.reload()
might appear. This only works if the JS sets the Facebook cookie, as this cookie will tell the server that the user is authenticated after the reload. Since we can’t use the cookie, we need another way to tell the server if this login attempt was successful. Server side
On the server-side, Facebook will tell us if this user is who he says he is with the getUser()
method. If this method returns a non-zero user ID, then Facebook has authenticated this user, and we can create a normal PHP login session, however you usually do that.
$id = $facebook->getUser();
if ($id)
{
// login successful
$_SESSION['user'] = $id;
}
// redirect to user dashboard or something
header('HTTP/1.1 302 Found');
header('Location: /');
exit;
getUser()
tries to authenticate a few different ways. You can read the code in the PHP SDK if you’re really curious. One way is from the ‘fbsr_
’ cookie. Another is by checking for the signed_request
in the query string. Remember when we passed that in earlier via JS? The cool thing is, that even in the non-JS case, Facebook will send that signed_request
in the query string, so this code will function the same in both cases.
Keeping the user logged in as he makes requests is the same as usual. I simply check for the flag (or, in the example above, a user ID) to verify authenticated status.
To logout, I can simply clear the PHP session like normal.
// logout
session_unset();
session_destroy();
// redirect to external home page
header('HTTP/1.1 302 Found');
header('Location: /');
exit;
Again, this does not log me out of Facebook
Conclusion
When might you want to use your user’s Facebook session for your user’s website session? I think the only case you would do that is if your web app is a Facebook-centric application. That is, if every feature of your app involves Facebook somehow. In that case, maybe it makes sense because your user cannot use your app without being connected to Facebook.
You can find a fully working example of this flow on my GitHub. This really belongs in an MVC-type framework, but for simplicity I put everything in a giant if/else
statement. Throw the files in a webroot, add your app’s ID and secret to the PHP file, and try it out. Try it with JavaScript enabled and disabled. After logging into the example, try logging out of Facebook, and then logging out of the example, and vice-versa. And let me know if you spot anything fishy.
As Facebook has beaten into me via my RSS reader over the last few months:
All apps must migrate to OAuth 2.0 for authentication and expect an encrypted access token. The old SDKs, including the old JS SDK and old iOS SDK will no longer work.
<digression> In the spirit of typical Facebook developer documentation, even this ostensibly unambiguous statement raises questions. See, I have been using the “new” Graph API to authenticate for many months now, but not with the oauth
flag set to true
. (In other words, I am on Oauth 1.0?) Does this mean my code will break on Oct. 1st? Or when they say that legacy authentication will break on Oct. 1st, do they mean that only the really old legacy auth will break? In either case, I may as well upgrade to the latest and greatest now. </digression>
Here are the changes I had to make to get my stuff working with the oauth
flag set to true
. They are mostly field and param name changes. There may be other changes important to you, but this is what I needed to modify to upgrade.
oauth: true
‘ to FB.init()
So change this:
FB.init({ appId : XXXXXX });
to this:
FB.init({ appId : XXXXXX, oauth : true });
I infer from Facebook (which is usually dangerous) that this flag will be enabled for everyone on Oct. 1st.
2. Change ‘perms
‘ to ‘scope
‘
Anywhere you request permissions for your app upon Facebook login, you must change the ‘perms
’ field to ‘scope
’. So for a typical XFBML implementation, change this:
<fb:login-button perms=”offline_access”></fb:login-button>
to this:
<fb:login-button scope=”offline_access”></fb:login-button>
For a typical JS implementation, change this:
FB.login(function(response) { }, { perms : ‘offline_access’ });
to this:
FB.login(function(response) { }, { scope : ‘offline_access’ });
For a typical non-JS, server-side (PHP) implementation, change this:
<a href=”<?php echo $facebook->getLoginUrl(array(‘req_perms’ => ‘offline_acces's’)); ?>”>Connect with Facebook</a>
to this:
<a href=”<?php echo $facebook->getLoginUrl(array(‘scope’ => ‘offline_acces's’)); ?>”>Connect with Facebook</a>
3. Update JavaScript response object
Functions like FB.login()
and FB.getLoginStatus()
accept a callback that gets passed a response object from Facebook after an authentication attempt. The previous response looked like this:
response = { perms : ‘offline_access’,
session : {
access_token : ‘XXXXXX’,
base_domain : ‘mydomain.com’,
expires : ‘0’,
secret : ‘XXXXXX’,
session_key : ‘XXXXXX’,
sig : ‘XXXXXX’,
uid : ‘XXXXXX’ },
status : ‘connected’ };
The new response looks like this:
response = { authResponse : {
accessToken : ‘XXXXXX’,
expiresIn : 0,
signedRequest : ‘XXXXXX’,
userID : ‘XXXXXX’ },
status : ‘connected’ };
You must update your JS authentication logic accordingly.
4. Change server-side PHP call getSession() to getUser()
Versions of the PHP SDK prior to 3.0 had both a getSession()
and a getUser()
method. After 3.0, there is only the getUser()
call. If you were using getSession()
to check for authentication previously, you must use getUser()
now. If you don’t care about the internals of the new SDK, then getUser()
functions the same as before and returns the Facebook ID of the currently logged in user, or 0 if there is none.
If you do care about the new SDK, then the reason for this is because Facebook has done away with the session concept and embraced the OAuth2 signed_request
concept instead. For example, the JSON-ified session array is no longer passed back to your callback URL in the query string upon authentication. Now, an encoded signed_request is passed back in the query string, which contains the same info (user ID, access token, expiration time, etc.)
5. Change server-side PHP Facebook class constructor
The new PHP Facebook class no longer accepts a cookie
flag in the constructor. If you don’t care about the internals of the new SDK, then remove the cookie flag and you’re done.
If you do care about the new SDK, then the reason for this – well, I’m not sure what the reason is. Perhaps a commenter could enlighten me. Best as I can tell, they have removed overlapping features of the the JS SDK and PHP SDK. Prior to OAuth2, both SDKs had cookie flags, and both could set and read the session cookie, which could be confusing, especially if you were persisting your own authentication cookie. Now, the JS can still set and read the signed_request
cookie, but the PHP library has gotten out of the cookie game (although it can still read a signed_request
cookie set by JS, and now it stores the signed_request
data in the PHP session).
One more note about cookie: the new Facebook cookie begins with ‘fbsr_
’ and not ‘fbs_
’.
Conclusion
That’s it. As I said, these worked for me, but YMMV. Happy upgrading!
From Time’s Person of the Year article on Mark Zuckerberg:
You don’t get a lot of shy, retiring types at Facebook. These are the kinds of power nerds to whom the movies don’t do justice: fast-talking, user-friendly, laser-focused and radiating the kind of confidence that gives you a sunburn. Sorkin did a much better job of representing Facebook when he wrote The West Wing.
We’ve never had long-lived sessions. It was never a requirement. I think we had a “Remember me” checkbox that didn’t work at one point, but we soon removed it. But suddenly, customer requests started coming in. They asked, “why do I have to log in every time I use the site? Why can’t I stay logged in forever, like Facebook or Twitter?” That was a good question.
Basic User Login
Like most sites, we used the PHP session to maintain a logged in user for our site. We started a session, kept track of some data indicating if the user is logged in or not, and that was about it.
I never looked at sessions and cookies in-depth before. I knew generally how sessions worked. PHP sets a cookie in the client’s browser. The cookie contains a session ID. When a request comes in, PHP reads the session ID, looks for a file corresponding to the ID on disk (or in a database, memcached, etc.), reads in the file containing the session data, and loads the session into the request. When the request finishes, the session data is saved to the file again.
Implementing The “Remember Me” Checkbox
First, naively, I thought all I had to do was find the right php.ini directive to make sessions last forever. Browsing the PHP manual and googling, I came across the session.cookie_lifetime
directive, configured in either php.ini or by session_set_cookie_params()
.
session.cookie_lifetime specifies the lifetime of the cookie in seconds which is sent to the browser. The value 0 means “until the browser is closed.” Defaults to 0.
I set this to 24 hours. Well, that was easy, I thought.
Except it didn’t work. Users reported logging in, going out to lunch, coming back, and getting logged out on the first link clicked. I dug deeper and found another directive.
session.gc_maxlifetime specifies the number of seconds after which data will be seen as ‘garbage’ and cleaned up. Garbage collection occurs during session start.
It defaults to 1440 seconds, or 24 mins.
It’s important to know that session.cookie_lifetime
starts when the cookie is set, regardless of last user activity, so it is an absolute expiration time. session.gc_maxlifetime
starts from when the user was last active (clicked), so it’s more like a maximum idle time.
Starting To Understand
Now I could see that both of these directives must cooperate to get the desired effect. Specifically, the shorter of these two values determines my session duration.
For example, let’s say I have session.cookie_lifetime
set to its default of 0, and session.gc_maxlifetime
is set to its default of 24 mins. A user who logs in can stay logged in forever, provided he never closes his browser, and he never stops clicking for more than 24 mins.
Now, let’s say the same user takes a 30 min. lunch break, and leaves his browser open. When, he gets back, he’ll most likely have been logged out because his session data was garbage collected on the server, even though his browser cookie was still there.
Now, let’s change session.cookie_lifetime
to 1 hour. A user who logs in can stay logged in for up to an hour if he clicks away for the whole time. This is regardless of whether or not he closes/reopens his browser. If he takes his 30 min. lunch break after working for 15 mins. he will most likely be logged out when he returns, even though his browser cookie had 15 more mins. of life.
Now, keeping session.cookie_lifetime
at 1 hour, let’s set session.gc_maxlifetime
to 2 hours. A user who logs in can stay logged in for up to an hour, period. He does not have to click at all in that time, but he’ll be logged out after an hour.
The Real “Remember Me” Solution
Back to my problem. At this point, I could’ve just set both directives to something like 1 year. But since session.gc_maxlifetime
controls garbage collection of session data, I’d have session data up to a year old left on the server! I did a quick check on the PHP session directory. There were already several thousand sessions, and that was only for a 24-minute lifetime!
Clearly, this was not how Twitter did it. A little more digging, and I realized that sites like those do not keep your specific session around for long periods of time. What they do is set a long-lasting cookie that contains some sort of security token. From that token, they can authenticate you, and re-create your session, even if your session data has already been removed from the server. (The cookie name for Twitter is auth_token
and looks to have a lifetime of 20 years.)
With the session recreation method, I could control when and how to log out users, if at all. So this enabled us to give users indefinite sessions, while keeping all session directives at their default values.
Beyond Session Cookies
This only scratches the surface of authentication topics of course. We didn’t talk about security implications of the session re-creation method, though I will say that the best security practice against session-based attacks seems to prompt for a password if the user attempts to change or view sensitive account information. LinkedIn is the first example that comes to mind.
Shortly after implementing this, a request came down from high above to centralize the authentication for our multiple products. I began to investigate single sign-on (like Google accounts) and federated identity (like OpenID), but those are topics of another post.
Here are a couple blogs that got me on my way to the final solution. Be sure to read the comments: