The use of the require_login() function is very common in Facebook apps. However the official documentation seems to omit one crucial fact -- it is not designed to be used from within an iframe. The situation is made worse by the fact that it often seems to work, but then will fail only for certain users, on certain browsers, or just at seemingly random times. And worse still, the problems may seem completey unrelated to what require_login() is supposed to do, so many app builders will have no idea that this standard function call is the origin of the failure. The common symptoms are:
These problems can appear either in IFRAME mode apps or within an <fb:iframe> tag used within an FBML mode app. Whichever way the iframe is created, if the code within it calls require_login() you could have problems.
There are really two separate issues at work. One is the way that the necessary authorizing parameters are conveyed to the iframe code, and the other is the way require_login() behaves when those parameters aren't found. Let's look at the second issue first since it is more obvious.
When require_login() can't find the user info, it tries to do a redirect to facebook.com/login.php to get a new set of session parameters. The code at login.php will then do a redirect back to the original url with an auth_token parameter added. It's great in theory, but unfortunately the exact coding for the redirect basically emits this:
echo "<script type=\"text/javascript\">\ntop.location.href = \"$url\";\n</script>"; exit;
As you can see, there is an explicit use of "top" when setting the location. This is what causes it to "break out" of the iframe. When it redirects back to the original url, it will no longer be within the original frame but instead will load directly into the top-level page. Thus the address bar will now show the external address, and the Facebook "chrome" will be gone.
It's clear from this coding that require_login() was not designed to be used with iframes.
So what is the solution? Well we can either try to ensure the redirect never happens, or we can eliminate require_login() altogether. I prefer to eliminate it, but I'll briefly cover some other options.
When the PHP Facebook object is created (e.g. $facebook=new Facebook('APPID','APPKEY');) the current user is determined by looking first at POST parameters, then at GET paramters, and finally at COOKIE parameters. The first time the app page is loaded, the necessary values are automatically added as GET parameters so everything works as expected. At the same time the initialization code tries to set cookies with these values so that subsequent accesses will be able to get the values there even if there are no GET or POST parameters. Unfortunately this involves trying to set a cookie for your external domain from within a page in the facebook.com domain. Depending on your browser's security settings, this may not be allowed. In particular, Internet Explorer will by default block third-party cookies unless a proper P3P header is present. You can include the header with this line of code at the top of your PHP:
header('P3P: CP="CAO PSA OUR"');
However, I don't recommend this except as a quick fix for most (but not all) issues. The problem is that although this addresses the default configuration of most browsers, it does nothing if the browser has been set for something more restrictive. If the browser is set to reject all third-party cookies, having a P3P header won't override that.
Since cookies can be unreliable, the best way to ensure the necessary parameters are available is to add them yourself. For links or GET forms, you can create a parameter string like this:
$parms='';
foreach ($_REQUEST as $name=>$val) {
if (substr($name,0,6)!='fb_sig') continue;
if ($parms!='') $parms.='&';
$parms.=$name.'='.$val;
}
Then you can append the $parms string to the url in links or the action parameter in a form tag. For POST forms you can do somethng similar by adding all the necessary parameters as hidden input tags:
foreach ($_REQUEST as $name=>$val) if (substr($name,0,6)=='fb_sig') echo "<input type='hidden' name='$name' value='$val' />";
Since the main problem with the standard require_login() code is the explicit top-level redirect, we can substitute a version without that flaw. Add this code to your facebook.php file:
public function iframe_require_login() {
if ($user = $this->get_loggedin_user()) {
return $user;
}
$this->iframe_redirect($this->get_login_url(self::current_url(), $this->in_frame()));
}
public function iframe_redirect($url) {
header('Location: ' . $url);
exit;
}
Then instead of calling require_login() call iframe_require_login() instead. This works the same way but if it needs to redirect it does it within the iframe instead of at page level. Note however that this will only solve the problems with breakouts and not ones involving lost form parameters. Whenever a redirect occurs all POST parameters will be lost, whether it is a frame-level redirect or a page-level redirect.
This is really the most logical method, since if we don't want the redirect behavior of require_login() all it is actually doing is returning the value of get_loggedin_user(). The only reason most apps have require_login() in their code is to retrieve the current userid, so all we really need is a way to do that manually. It's easy enough to pass the userid into the iframe as a parameter, so this really boils down to finding a way to implement proper security. We can make an MD5 signature of the expected userid plus a secret string and pass that along with the userid, and then compare that to an MD5 signature generated the same way in the iframe code. It may sound complicated but is actually simple to implement.
In an FBML app, put code like this in the main page:
$facebook=new Facebook($apikey, $apisecret); $user=$facebook->require_login(); // This is fine in the main canvas page $secret='SOMESECRETSTRING'; // This can be the same as your app secret, or anything else unguessable $key=$facebook->api_client->session_key; $token=md5($user.$secret); echo "<fb:iframe src='http://yourdomain.com/yourapp/inner.php?uid=$user&key=$key&token=$token' />";
Then in inner.php, put code like this:
$user=$_REQUEST['uid'];
$key=$_REQUEST['key'];
$token=$_REQUEST['token'];
$secret='SOMESECRETSTRING'; // This must be the same string used in the main page
$check=md5($user.$secret);
if ($check!=$token) { echo "Invalid Signature"; exit(); }
$facebook=new Facebook($apikey, $apisecret);
$facebook->set_user($user, $key); // Now we can call whatever API methods we want
In an IFRAME app, you can use the same idea. It's a little trickier because there is no "main" page where it is safe to use require_login() to initially get the userid. Instead we can check if the userid has been properly set already by the creation of the Facebook object (which will happen when the page is first loaded). Otherwise we set it manually from the custom parameters we have added to all links and forms. And if neither of those are available, the user must not have authorized the app yet so we redirect to get the authorization. Our code is something like this:
$_COOKIE=array(); //Optional -- keeps stale cookies from interfering with our GET data
$secret='SOMESECRETSTRING';
$facebook=new Facebook($apikey, $apisecret);
$user=$facebook->get_loggedin_user();
if (!$user) {
$user=$_REQUEST['uid'];
if (!$user) $facebook->redirect($facebook->get_login_url('http://apps.facebook.com/YOURAPP', 1));
$key=$_REQUEST['key'];
$token=$_REQUEST['token'];
$check=md5($user.$secret);
if ($check!=$token) { echo "Invalid Signature"; exit(); }
$facebook->set_user($user, $key);
}
$key=$facebook->api_client->session_key;
$token=md5($user.$secret);
$parms="uid=$user&key=$key&token=$token"; // This parameter string must be added to all links and forms urls
Note that it is still our responsibility to add $parms to all our links, so the next page load will have the info needed to verify the user and set up a session. This is also true in an FBML app where the code in the iframe will contain links that are meant to load another page within the same iframe.