Skip to content

Instantly share code, notes, and snippets.

@prabirshrestha
Created February 7, 2011 19:12
Show Gist options
  • Save prabirshrestha/814990 to your computer and use it in GitHub Desktop.
Save prabirshrestha/814990 to your computer and use it in GitHub Desktop.
FacebookSignedRequest
namespace Facebook.Web
{
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
/// <summary>
/// Represents a Facebook signed request.
/// </summary>
public sealed class FacebookSignedRequest
{
/// <summary>
/// The actual value of the signed request.
/// </summary>
private object data;
/// <summary>
/// The access token.
/// </summary>
private string accessToken;
/// <summary>
/// Initializes a new instance of the <see cref="FacebookSignedRequest"/> class.
/// </summary>
/// <param name="secret">
/// The secret.
/// </param>
/// <param name="signedRequestValue">
/// The signed request value.
/// </param>
/// <param name="maxAge">
/// The max age.
/// </param>
public FacebookSignedRequest(string secret, string signedRequestValue, int maxAge)
{
Contract.Requires(!String.IsNullOrEmpty(signedRequestValue));
Contract.Requires(!String.IsNullOrEmpty(secret));
Contract.Requires(maxAge >= 0);
Contract.Requires(signedRequestValue.Contains("."), Properties.Resources.InvalidSignedRequest);
this.Data = TryParse(secret, signedRequestValue, maxAge, FacebookUtils.ToUnixTime(DateTime.UtcNow), true);
}
/// <summary>
/// Initializes a new instance of the <see cref="FacebookSignedRequest"/> class.
/// </summary>
/// <param name="secret">
/// The secret.
/// </param>
/// <param name="signedRequestValue">
/// The signed request value.
/// </param>
public FacebookSignedRequest(string secret, string signedRequestValue)
: this(secret, signedRequestValue, 0)
{
Contract.Requires(!String.IsNullOrEmpty(signedRequestValue));
Contract.Requires(!String.IsNullOrEmpty(secret));
Contract.Requires(signedRequestValue.Contains("."), Properties.Resources.InvalidSignedRequest);
}
/// <summary>
/// Initializes a new instance of the <see cref="FacebookSignedRequest"/> class.
/// </summary>
/// <param name="data">
/// The signed request data.
/// </param>
internal FacebookSignedRequest(IDictionary<string, object> data)
{
Contract.Requires(data != null);
this.Data = data;
}
/// <summary>
/// Gets actual value of signed request.
/// </summary>
public object Data
{
get
{
Contract.Ensures(Contract.Result<object>() != null);
return this.data;
}
private set
{
Contract.Requires(value != null);
var data = (IDictionary<string, object>)(value is JsonObject ? value : FacebookUtils.ToDictionary(value));
if (data.ContainsKey("payload"))
{
// new signed_request: http://developers.facebook.com/docs/authentication/canvas/encryption_proposal
var payload = (IDictionary<string, object>)data["payload"];
if (payload != null)
{
this.accessToken = payload.ContainsKey("access_token") ? (string)payload["access_token"] : null;
//this.Expires = payload.ContainsKey("expires_in")
// ? FacebookUtils.FromUnixTime(Convert.ToInt64(payload["expires_in"]))
// : DateTime.MinValue;
//this.UserId = payload.ContainsKey("user_id") ? (string)payload["user_id"] : null;
}
}
else
{
// old signed_request: http://developers.facebook.com/docs/authentication/canvas
//this.UserId = data.ContainsKey("user_id") ? (string)data["user_id"] : null;
this.accessToken = data.ContainsKey("oauth_token") ? (string)data["oauth_token"] : null;
//this.Expires = data.ContainsKey("expires")
// ? FacebookUtils.FromUnixTime(Convert.ToInt64(data["expires"]))
// : DateTime.MinValue;
//this.ProfileId = data.ContainsKey("profile_id") ? (string)data["profile_id"] : null;
}
this.data = data;
}
}
/// <summary>
/// Gets the access token.
/// </summary>
public string AccessToken
{
get { return this.accessToken; }
}
/// <summary>
/// Parse the signed request string.
/// </summary>
/// <param name="secret">
/// The secret.
/// </param>
/// <param name="signedRequestValue">
/// The signed request value.
/// </param>
/// <param name="maxAge">
/// The max age.
/// </param>
/// <param name="currentTime">
/// The current time.
/// </param>
/// <param name="throws">
/// The throws.
/// </param>
/// <returns>
/// The FacebookSignedRequest.
/// </returns>
internal static IDictionary<string, object> TryParse(string secret, string signedRequestValue, int maxAge, double currentTime, bool throws)
{
Contract.Requires(!String.IsNullOrEmpty(signedRequestValue));
Contract.Requires(!String.IsNullOrEmpty(secret));
Contract.Requires(maxAge >= 0);
Contract.Requires(currentTime >= 0);
Contract.Requires(signedRequestValue.Contains("."), Properties.Resources.InvalidSignedRequest);
try
{
// NOTE: currentTime added to parameters to make it unit testable.
string[] split = signedRequestValue.Split('.');
if (split.Length != 2)
{
// need to have exactly 2 parts
throw new InvalidOperationException(Properties.Resources.InvalidSignedRequest);
}
string encodedSignature = split[0];
string encodedEnvelope = split[1];
if (string.IsNullOrEmpty(encodedSignature))
{
throw new InvalidOperationException(Properties.Resources.InvalidSignedRequest);
}
if (string.IsNullOrEmpty(encodedEnvelope))
{
throw new InvalidOperationException(Properties.Resources.InvalidSignedRequest);
}
var envelope = (IDictionary<string, object>)JsonSerializer.DeserializeObject(Encoding.UTF8.GetString(FacebookUtils.Base64UrlDecode(encodedEnvelope)));
string algorithm = (string)envelope["algorithm"];
if (!algorithm.Equals("AES-256-CBC HMAC-SHA256") && !algorithm.Equals("HMAC-SHA256"))
{
// TODO: test
throw new InvalidOperationException("Invalid signed request. (Unsupported algorithm)");
}
byte[] key = Encoding.UTF8.GetBytes(secret);
byte[] digest = FacebookUtils.ComputeHmacSha256Hash(Encoding.UTF8.GetBytes(encodedEnvelope), key);
if (!digest.SequenceEqual(FacebookUtils.Base64UrlDecode(encodedSignature)))
{
throw new InvalidOperationException("Invalid signed request. (Invalid signature.)");
}
IDictionary<string, object> result;
if (algorithm.Equals("HMAC-SHA256"))
{
// for requests that are signed, but not encrypted, we're done
result = envelope;
}
else
{
result = new Dictionary<string, object>();
result["algorithm"] = algorithm;
long issuedAt = (long)envelope["issued_at"];
if (issuedAt < currentTime)
{
throw new InvalidOperationException("Invalid signed request. (Too old.)");
}
result["issued_at"] = issuedAt;
// otherwise, decrypt the payload
byte[] iv = FacebookUtils.Base64UrlDecode((string)envelope["iv"]);
byte[] rawCipherText = FacebookUtils.Base64UrlDecode((string)envelope["payload"]);
var plainText = FacebookUtils.DecryptAes256CBCNoPadding(rawCipherText, key, iv);
var payload = (IDictionary<string, object>)JsonSerializer.DeserializeObject(plainText);
result["payload"] = payload;
}
return result;
}
catch
{
if (throws)
{
throw;
}
return null;
}
}
[SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented",
Justification = "Reviewed. Suppression is OK here.")]
[ContractInvariantMethod]
private void InvariantObject()
{
Contract.Invariant(this.data != null);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment