Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Facebook PHP/JS SDK "Session has expired at unix time" with long life token

I am trying to use a stored long-life access token, but after 2 hours I get the below error from the graph API. I have written some code to send the user to Facebook to get a new code that can be exchanged for an access token, which works just fine except this happens on every subsequent page request, Facebook continues to invalidate my access token with the below error, despite that access token being returned by their server.

Error validating access token: Session has expired at unix time 1338300000. The current unix time is 1338369365.

The complete test page example is below. Omitting my keys for obvious reasons. Hit the page, login and then leave it for a few hours and hit the page again (you'll get the redirect to facebook and back with the code in the url), reload the page and it will continue to redirect to facebook and back, event though I am telling it to use the access token it just returned for said code.

<?php
  require 'facebook.php';

  $app_id = APP_ID;
  $app_secret = APP_SERCRET;
  $my_url = URL;

  $facebook = new Facebook(array(
    'appId'   => $app_id,
    'secret'  => $app_secret
  ));

  // known valid access token stored in a database
  $access_token = isset($_COOKIE["FB_LONG_AC_TOKEN"]) ? $_COOKIE["FB_LONG_AC_TOKEN"] : false;

  $code = $_REQUEST["code"];

  // If we get a code, it means that we have re-authed the user
  //and can get a valid access_token.
  if (isset($code)) {
    $token_url="https://graph.facebook.com/oauth/access_token?client_id="
      . $app_id . "&redirect_uri=" . urlencode($my_url)
      . "&client_secret=" . $app_secret
      . "&code=" . $code . "&display=popup";
    $response = file_get_contents($token_url);
    $params = null;
    parse_str($response, $params);
    $access_token = $params['access_token'];
  }


  // Attempt to query the graph:
  $graph_url = "https://graph.facebook.com/me?"
    . "access_token=" . $access_token;
  $response = curl_get_file_contents($graph_url);
  $decoded_response = json_decode($response);

  //Check for errors
  if ($decoded_response->error) {
  // check to see if this is an oAuth error:
    if ($decoded_response->error->type== "OAuthException") {
      // Retrieving a valid access token.
      $dialog_url= "https://www.facebook.com/dialog/oauth?"
        . "client_id=" . $app_id
        . "&redirect_uri=" . urlencode($my_url);
      echo("<script> top.location.href='" . $dialog_url
      . "'</script>");
    }
    else {
      echo "other error has happened";
    }
  }
  else {
  // success
    echo("Success: ".$decoded_response->name."<br />");
    echo($access_token."<br />");

    // Attempt to convert access token to longlife token if we don't have one stored.
    if (!isset($_COOKIE["FB_LONG_AC_TOKEN"]))
    { // don't have long life token, so let's get one.
      $ch = curl_init("https://graph.facebook.com/oauth/access_token?client_id=".$app_id."&client_secret=".$app_secret."&grant_type=fb_exchange_token&fb_exchange_token=".$access_token);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
      curl_setopt($ch, CURLOPT_TIMEOUT, 10);
      $data = curl_exec($ch);
      curl_close($ch);

      $params = null;
      parse_str($data, $params);

      if (isset($params["access_token"]))
      {
        $access_token = $params["access_token"];

        echo("Got long life token.<br />");
        setcookie("FB_LONG_AC_TOKEN", $access_token, time() + (3600 * 24 * 60), "/");
      }
    }
    else {
      echo("Have long life token already.<br />");
    }
  }

  if ($access_token) {
    $facebook->setAccessToken($access_token);

    // See if there is a user from a cookie
    $user = $facebook->getUser();

    if ($user) {
      try {
        // Proceed knowing you have a logged in user who's authenticated.
        $user_profile = $facebook->api('/me');
      } catch (FacebookApiException $e) {
        echo '<pre>'.htmlspecialchars(print_r($e, true)).'</pre>';
        $user = null;
      }
    }
  }

  // note this wrapper function exists in order to circumvent PHP’s
  //strict obeying of HTTP error codes.  In this case, Facebook
  //returns error code 400 which PHP obeys and wipes out
  //the response.
  function curl_get_file_contents($URL) {
    $c = curl_init();
    curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($c, CURLOPT_URL, $URL);
    $contents = curl_exec($c);
    $err  = curl_getinfo($c,CURLINFO_HTTP_CODE);
    curl_close($c);
    if ($contents) return $contents;
    else return FALSE;
  }
?>
<!doctype html>
<html xmlns:fb="http://www.facebook.com/2008/fbml">
<head>
    <title>Facebook Auth</title>
</head>
<body>
  <?php if ($user) { ?>
    Your user profile is
    <pre>
      <?php print htmlspecialchars(print_r($user_profile, true)) ?>
    </pre>
  <?php } else { ?>
    <fb:login-button></fb:login-button>
  <?php } ?>

  <div id="fb-root"></div>
  <script>
      window.fbAsyncInit = function () {
        FB.init({
          appId: <?php echo($app_id); ?>,
          cookie: true, // enable cookies to allow the server to access the session
          oauth: true, // enable OAuth 2.0
          xfbml: true  // parse XFBML
        });

        FB.getLoginStatus(function (res) {
          console.log(res.status);
        });
      };

      (function(d){
        var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {return;}
        js = d.createElement('script'); js.id = id; js.async = true;
        js.src = "//connect.facebook.net/en_US/all.js";
        d.getElementsByTagName('head')[0].appendChild(js);
      }(document));
  </script>
</body>
</html>

What am I doing wrong? or this is a problem with Facebook?

UPDATE:

I have updated my code to follow the flow posted by @cpilko however I am still having the same problem. I can log in and out just fine. However if I visit the test page after some hours, say the next day I get the Session expired exception with the supplied long-life access token (FB JS SDK thinks I am connected, but the server did not), I refresh the page and I am shown as logged in by both the server and FB JS SDK and the long life token I get from facebook is the same as the one I original tried (stored in my cookies). What I don't understand is why I am unable to use the long-life token the first time. Updated code below.

<?php
  require 'facebook.php';

  $app_id = "XXXXXXXXXXXXX";
  $app_secret = "XXXXXXXXXXXXXXXXXXXX";
  $my_url = "http://swan.magicseaweed.local/facebook/";

  $facebook = new Facebook(array(
    'appId'   => $app_id,
    'secret'  => $app_secret
  ));

  $valid_user = false;

var_dump($_COOKIE);
echo("<br />");

  if (isset($_COOKIE["FB_LONG_AC_TOKEN"]))
  { // Have long term token, attempt to validate.
    // Attempt to query the graph:
    $graph_url = "https://graph.facebook.com/me?"
      . "access_token=" . $_COOKIE["FB_LONG_AC_TOKEN"];
    $response = curl_get_file_contents($graph_url);
    $decoded_response = json_decode($response);

    // If we don't have an error then it's valid.
    if (!$decoded_response->error) {
      $valid_user = true;
      $access_token = $_COOKIE["FB_LONG_AC_TOKEN"];
      echo("Have long life token.<br />");
    }
    else {
      // Stored token is invalid.
      // Attempt to renew token.

      // Exchange short term token for long term.
      $ch = curl_init("https://graph.facebook.com/oauth/access_token?client_id=".$app_id."&client_secret=".$app_secret."&grant_type=fb_exchange_token&fb_exchange_token=".$facebook->getAccessToken());
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
      curl_setopt($ch, CURLOPT_TIMEOUT, 10);
      $data = curl_exec($ch);
      curl_close($ch);

      $params = null;
      parse_str($data, $params);

      if (isset($params["access_token"]))
      {
        $access_token = $params["access_token"];

        echo("Got long life token.<br />");
        setcookie("FB_LONG_AC_TOKEN", $access_token, time() + (3600 * 24 * 60), "/");
      }
      else
      { // Clear invalid token.
        setcookie("FB_LONG_AC_TOKEN", "false", time() - 3600, "/");
        echo("Long life token invalid.<br />");
      }
    }
  }
  else if ($facebook->getUser())
  { // Have short term access token.
    // Verify short term token is valid still.
    try {
        // Proceed knowing you have a logged in user who's authenticated.
        $user_profile = $facebook->api('/me');
    }
    catch (FacebookApiException $e) { }

    if (is_array($user_profile)) { // Have user.
      $valid_user = true;

      // Exchange short term token for long term.
      $ch = curl_init("https://graph.facebook.com/oauth/access_token?client_id=".$app_id."&client_secret=".$app_secret."&grant_type=fb_exchange_token&fb_exchange_token=".$facebook->getAccessToken());
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
      curl_setopt($ch, CURLOPT_TIMEOUT, 10);
      $data = curl_exec($ch);
      curl_close($ch);

      $params = null;
      parse_str($data, $params);

      if (isset($params["access_token"]))
      {
        $access_token = $params["access_token"];

        echo("Got long life token.<br />");
        setcookie("FB_LONG_AC_TOKEN", $access_token, time() + (3600 * 24 * 60), "/");
      }
    }
  }

  if ($access_token) {
    $facebook->setAccessToken($access_token);

    // See if there is a user from a cookie
    $user = $facebook->getUser();

    if ($user) {
      try {
        // Proceed knowing you have a logged in user who's authenticated.
        $user_profile = $facebook->api('/me');
      } catch (FacebookApiException $e) {
        echo '<pre>'.htmlspecialchars(print_r($e, true)).'</pre>';
        $user = null;
      }
    }
  }

  // note this wrapper function exists in order to circumvent PHP’s
  //strict obeying of HTTP error codes.  In this case, Facebook
  //returns error code 400 which PHP obeys and wipes out
  //the response.
  function curl_get_file_contents($URL) {
    $c = curl_init();
    curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($c, CURLOPT_URL, $URL);
    $contents = curl_exec($c);
    $err  = curl_getinfo($c,CURLINFO_HTTP_CODE);
    curl_close($c);
    if ($contents) return $contents;
    else return FALSE;
  }
?>
<!doctype html>
<html xmlns:fb="http://www.facebook.com/2008/fbml">
<head>
    <title>Facebook Auth</title>
</head>
<body>
  <?php if ($user) { ?>
    Your user profile is
    <pre>
      <?php print htmlspecialchars(print_r($user_profile, true)) ?>
    </pre>
  <?php } else { ?>
    <fb:login-button></fb:login-button>
  <?php } ?>

  <div id="fb-root"></div>
  <script>
      window.fbAsyncInit = function () {
        FB.init({
          appId: <?php echo($app_id); ?>,
          cookie: true, // enable cookies to allow the server to access the session
          oauth: true, // enable OAuth 2.0
          xfbml: true  // parse XFBML
        });

        FB.getLoginStatus(function (res) {
          console.log(res.status);
        });
      };

      (function(d){
        var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {return;}
        js = d.createElement('script'); js.id = id; js.async = true;
        js.src = "//connect.facebook.net/en_US/all.js";
        d.getElementsByTagName('head')[0].appendChild(js);
      }(document));
  </script>
</body>
</html>
like image 552
Gcoop Avatar asked May 30 '12 09:05

Gcoop


1 Answers

The reason you get stuck in the loop when your original token expires is your code isn't allowing Facebook to give you a new token. When you get an OAuth error, you are invoking the OAuth dialog, which doesn't repopulate your cookie with a fresh token.

The other problem you have is you're overwriting the long-term access token with your short term access token before making an api call:

// known valid access token stored in a database
$access_token = isset($_COOKIE["FB_LONG_AC_TOKEN"]) ? $_COOKIE["FB_LONG_AC_TOKEN"] : false;
 ...
if ($access_token) {
$facebook->setAccessToken($access_token); //Loads the short-term token from a cookie!

If it were my code, I'd use two variables: $access_token_temp and $access_token_long just so I could keep things straight.

EDIT

Your workflow on page load needs to be:

+ IF one exists, retrieve the long-term token from `$_COOKIE['FB_LONG_AC_TOKEN']` 
    + If it does exist, test to see if it is valid.
    + If valid, use the renew the token and update the cookie if one is returned. (Only one token will be returned per day)
    + Else mark the long-term token as invalid and clear the cookie.
    + Set a `$vaild_user` flag.
+ ELSE IF a new short-term token is available
    + Read the short-term token cookie with the PHP API and exchange this for a long-term token.
    + Store the long-term token in the cookie
    + Clear the short-term token cookie
    + Set a `$valid_user` flag
+ ELSE: The token does not exist or is invalid
    + Redirect the user to the JS API auth dialog and store the returned short term token in a cookie.
    + Reload the page to process this.
+ ENDIF 
+ IF `$valid_user`: Do stuff.

You've got all the parts in your code. You just need to clean up the logic to make it work.

Edit #2:

I ran your code on my server. It mostly works, but the debugging information you're outputting is sending headers prematurely, inhibiting the ability of setcookie() to set a cookie.

I got your code to run by declaring $out = array(); near the beginning, then by changing all your echo and print statements to $out[] = "What you were echoing or printing before"; To still display this, add echo implode("\n", $out); within the <body> of your document.

Once I did that, I was able to store a valid token in a cookie, and validate that it is indeed a long-life token with a 60-day expiration.

like image 166
cpilko Avatar answered Sep 29 '22 02:09

cpilko