Created
February 7, 2011 19:12
-
-
Save prabirshrestha/814990 to your computer and use it in GitHub Desktop.
FacebookSignedRequest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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