I think if we make calls to Facebook using REST or Graph API, we will know if it is fake because it will come back saying fake session / auth_token / access_token. But what if we are showing our own DB info, such as the user's "most preferred product list", then we don't make any call to Facebook but show our DB's data. How do we know it is really the user, not somebody faking the cookie?
views:
68answers:
5don't store it in a cookie. Put it in a session variable, that way you have control
Do not put a user id in a cookie. The session cookie should just be a random number that maps to a record in your server-side session database. Any data associated to that session is only stored server-side.
That way, in order to fake a session, an attacker would have to guess a random number that actually in use at that time. Given that there are a lot of random numbers and sessions expire, that is almost impossible.
The only thing you can trust is session_key
for old REST api and access_token
for Graph API. Once you got it, pass it to a server side with your data retrirval request. On the server side call facebook api and get current userid. Once you got userid you can store it in a session and use it later.
There are a few approaches here.
Inefficient: Whenever you perform an authenticated operation, grab the FB cookie and use the data in it to make a dummy API call to see that the access token is valid and matches the user (i.e. grab /me?fields=id).
More efficient: The first time you see a FB cookie for a user, store that cookie in a server-side session for the user (with a sufficiently-hard-to-guess session ID passed to the client in a cookie).
Another approach, and does not require server-side session state: The first time you see a FB cookie for a user, HMAC the cookie using a secret only your servers have, and store that resulting hash in a cookie. Then you can check if there is a valid hash of the FB cookie, and if so, you trust it. Otherwise, you fall back to the validation.
When you read a cookie with facebook it contains a value called 'sig'. With this value, the other cookie values, and your app secret you hash the contents of the cookie and validate it against the sig. If they match, then the cookie is valid. You can trust this result because only you and Facebook have access to the app secret. Here is the example of how Facebook's PHP SDK does it. Any respectable Facebook SDK will do this all for you internally.
/**
* Validates a session_version=3 style session object.
*
* @param Array $session the session object
* @return Array the session object if it validates, null otherwise
*/
protected function validateSessionObject($session) {
// make sure some essential fields exist
if (is_array($session) &&
isset($session['uid']) &&
isset($session['access_token']) &&
isset($session['sig'])) {
// validate the signature
$session_without_sig = $session;
unset($session_without_sig['sig']);
$expected_sig = self::generateSignature(
$session_without_sig,
$this->getApiSecret()
);
if ($session['sig'] != $expected_sig) {
self::errorLog('Got invalid session signature in cookie.');
$session = null;
}
// check expiry time
} else {
$session = null;
}
return $session;
}
Here is the same thing in C# (Facebook C# SDK):
/// <summary>
/// Validates a session_version=3 style session object.
/// </summary>
/// <param name="session">The session to validate.</param>
protected override void ValidateSessionObject(FacebookSession session)
{
if (session == null)
{
return;
}
var signature = this.GenerateSignature(session);
if (session.Signature == signature.ToString())
{
return;
}
session = null;
}
/// <summary>
/// Generates a MD5 signature for the facebook session.
/// </summary>
/// <param name="session">The session to generate a signature.</param>
/// <returns>An MD5 signature.</returns>
/// <exception cref="System.ArgumentNullException">If the session is null.</exception>
/// <exception cref="System.InvalidOperationException">If there is a problem generating the hash.</exception>
protected override string GenerateSignature(FacebookSession session)
{
var args = session.Dictionary;
StringBuilder payload = new StringBuilder();
var parts = (from a in args
orderby a.Key
where a.Key != "sig"
select string.Format(CultureInfo.InvariantCulture, "{0}={1}", a.Key, a.Value)).ToList();
parts.ForEach((s) => { payload.Append(s); });
payload.Append(this.ApiSecret);
byte[] hash = null;
using (var md5 = System.Security.Cryptography.MD5CryptoServiceProvider.Create())
{
if (md5 != null)
{
hash = md5.ComputeHash(Encoding.UTF8.GetBytes(payload.ToString()));
}
}
if (hash == null)
{
throw new InvalidOperationException("Hash is not valid.");
}
StringBuilder signature = new StringBuilder();
for (int i = 0; i < hash.Length; i++)
{
signature.Append(hash[i].ToString("x2", CultureInfo.InvariantCulture));
}
return signature.ToString();
}