jbouy
6/11/2013 - 10:32 PM

Amazon OAuth2 Client for DotNetOpenAuth

Amazon OAuth2 Client for DotNetOpenAuth

    ...
    OAuthWebSecurity.RegisterClient(new AmazonClient(
        clientId: "amzn1.application-oa2-client.123abc", 
        clientSecret: "123abc"), "Amazon", null);
    ...
/* -------------------------------------------------
 * Amazon Login Client for DotNetOpenAuth 
 * Maarten Sikkema, Macaw
 * MIT Licence (http://opensource.org/licenses/MIT)
 *
 * ------------------------------------------------- */

namespace DotNetOpenAuth.AspNet.Clients
{
    using DotNetOpenAuth.Messaging;
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Net;
    using System.Runtime.Serialization;
    using System.Runtime.Serialization.Json;
    using System.Text;
    using System.Web;
    using Validation;


    /// <summary>
    /// The amazon client.
    /// </summary>
    public sealed class AmazonClient : OAuth2Client
    {
        #region Constants and Fields

        /// <summary>
        /// The authorization endpoint.
        /// </summary>
        private const string AuthorizationEndpoint = "https://www.amazon.com/ap/oa";

        /// <summary>
        /// The token endpoint.
        /// </summary>
        private const string TokenEndpoint = "https://api.amazon.com/auth/o2/token";
        
        /// <summary>
        /// The _app id.
        /// </summary>
        private readonly string clientId;

        /// <summary>
        /// The _app secret.
        /// </summary>
        private readonly string clientSecret;

        /// <summary>
        /// The scope.
        /// </summary>
        private readonly string[] scope;

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// Initializes a new instance of the <see cref="FacebookClient"/> class
        /// with "email" as the scope.
        /// </summary>
        /// <param name="appId">
        /// The app id.
        /// </param>
        /// <param name="appSecret">
        /// The app secret.
        /// </param>
        public AmazonClient(string clientId, string clientSecret)
            : this(clientId, clientSecret, "profile")
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="FacebookClient"/> class.
        /// </summary>
        /// <param name="clientId">
        /// The app id.
        /// </param>
        /// <param name="clientSecret">
        /// The app secret.
        /// </param>
        /// <param name="scope">
        /// The scope of authorization to request when authenticating with Facebook. The default is "profile".
        /// </param>
        public AmazonClient(string clientId, string clientSecret, params string[] scope)
            : base("amazon")
        {
            Requires.NotNullOrEmpty(clientId, "clientId");
            Requires.NotNullOrEmpty(clientSecret, "clientSecret");
            Requires.NotNullOrEmpty(scope, "scope");

            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.scope = scope;
        }

        #endregion

        #region Methods

        /// <summary>
        /// The get service login url.
        /// </summary>
        /// <param name="returnUrl">
        /// The return url.
        /// </param>
        /// <returns>An absolute URI.</returns>
        protected override Uri GetServiceLoginUrl(Uri returnUrl)
        {
            var builder = new UriBuilder(AuthorizationEndpoint);
            builder.AppendQueryArgument("client_id", this.clientId);
            // We must circumvert DotNetOpenAuth adding its stuff to the returnUrl to the Redirect Uri... The OAuth spec does not allow it and Amazon will reject it
            builder.AppendQueryArgument("redirect_uri", returnUrl.GetLeftPart(UriPartial.Path));
            // we must send the extra stuff in the state parameter. On return we'll add it back and redirect to make DotNetOpenAuth happy
            builder.AppendQueryArgument("state", returnUrl.Query);
            builder.AppendQueryArgument("response_type", "code");
            builder.AppendQueryArgument("scope", string.Join(" ", this.scope));

            return builder.Uri;
        }

        /// <summary>
        /// The get user data.
        /// </summary>
        /// <param name="accessToken">
        /// The access token.
        /// </param>
        /// <returns>A dictionary of profile data.</returns>
        protected override IDictionary<string, string> GetUserData(string accessToken)
        // protected override NameValueCollection GetUserData(string accessToken)
        {
            AmazonProfileData userProfile;
            var request = WebRequest.Create("https://api.amazon.com/user/profile?access_token=" + HttpUtility.UrlEncode(accessToken));
            request.Headers.Add(HttpRequestHeader.Authorization, string.Format(CultureInfo.InvariantCulture, "Bearer {0}", accessToken));
            request.PreAuthenticate = true;

            using (var response = request.GetResponse())
            {
                using (var responseStream = response.GetResponseStream())
                {
                    userProfile = JsonHelper.Deserialize<AmazonProfileData>(responseStream);
                }
            }

            // this dictionary must contains 
            var userData = new Dictionary<string, string>();
            // var userData = new NameValueCollection();
            if (!string.IsNullOrEmpty(userProfile.CustomerId)) userData.Add("id", userProfile.CustomerId);
            if (!string.IsNullOrEmpty(userProfile.PrimaryEmail)) userData.Add("email", userProfile.PrimaryEmail);
            if (!string.IsNullOrEmpty(userProfile.Name)) userData.Add("name", userProfile.Name);
            if (!string.IsNullOrEmpty(userProfile.PostalCode)) userData.Add("postal_code", userProfile.PostalCode);
            return userData;
        }

        /// <summary>
        /// Obtains an access token given an authorization code and callback URL.
        /// </summary>
        /// <remarks>
        /// Amazon uses the state parameter and does not allow adding data to the reply address, as per the OAuth 2.0 spec
        /// Add the following code in the top to ExternalLoginCallback to translate the state parameters back where FotNetOpenAuth expects them
        /// 
        ///     if (!String.IsNullOrEmpty(Request.QueryString["state"]))
        ///     {
        ///         var routeValues = this.RouteData.Values;
        ///         foreach (string key in Request.QueryString.AllKeys)
        ///         if (key.ToLower() == "state")
        ///         {
        ///             NameValueCollection stateCollection = HttpUtility.ParseQueryString(Request.QueryString["state"]);
        ///             foreach (string stateKey in stateCollection.AllKeys)
        ///             routeValues.Add(stateKey, stateCollection[stateKey]);
        ///         }
        ///         else
        ///         {
        ///             routeValues.Add(key, Request.QueryString[key]);
        ///         }
        ///         return RedirectToRoute(routeValues);
        ///     }
        ///     
        /// </remarks>
        /// <param name="returnUrl">
        /// The return url.
        /// </param>
        /// <param name="authorizationCode">
        /// The authorization code.
        /// </param>
        /// <returns>
        /// The access token.
        /// </returns>
        protected override string QueryAccessToken(Uri returnUrl, string authorizationCode)
        {
            var sb = new StringBuilder();
            sb.Append("grant_type=authorization_code");
            sb.Append("&code=").Append(HttpUtility.UrlEncode(authorizationCode));
            // we need to circumvert DotNetOpenAuth adding its stuff to the returnUrl to the Redirect Uri... Amazon will reject it
            sb.Append("&redirect_uri=").Append(HttpUtility.UrlEncode(returnUrl.GetLeftPart(UriPartial.Path)));
            sb.Append("&client_id=").Append(HttpUtility.UrlEncode(this.clientId));
            sb.Append("&client_secret=").Append(HttpUtility.UrlEncode(this.clientSecret));

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(TokenEndpoint);
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            byte[] bytes = Encoding.UTF8.GetBytes(sb.ToString());
            request.ContentLength = bytes.Length;

            using (Stream requestStream = request.GetRequestStream())
            {
                requestStream.Write(bytes, 0, bytes.Length);

                using (WebResponse response = request.GetResponse())
                {
                    using (Stream stream = response.GetResponseStream())
                    {
                        AmazonTokenResponse result = JsonHelper.Deserialize<AmazonTokenResponse>(stream);
                        return result.AccessToken;
                    }
                }

            }
        }


        #endregion

        /// <summary>
        /// The json helper.
        /// </summary>
        internal static class JsonHelper
        {

            #region Public Methods and Operators

            /// <summary>
            /// The deserialize.
            /// </summary>
            /// <param name="stream">
            /// The stream.
            /// </param>
            /// <typeparam name="T">The type of the value to deserialize.</typeparam>
            /// <returns>
            /// The deserialized value.
            /// </returns>
            public static T Deserialize<T>(Stream stream) where T : class
            {
                Requires.NotNull(stream, "stream");

                var serializer = new DataContractJsonSerializer(typeof(T));
                return (T)serializer.ReadObject(stream);
            }

            #endregion
        }
    }


    /// <summary>
    /// Contains data of a Amazon user.
    /// </summary>
    /// <remarks>
    /// Technically, this class doesn't need to be public, but because we want to make it serializable in medium trust, it has to be public.
    /// </remarks>
    [DataContract]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Amazon", Justification = "Brand name")]
    public class AmazonProfileData
    {
        #region Public Properties

        /// <summary>
        /// Gets or sets the email.
        /// </summary>
        /// <value> The email. </value>
        [DataMember(Name = "email")]
        public string PrimaryEmail { get; set; }

        /// <summary>
        /// Gets or sets the id.
        /// </summary>
        /// <value> The id. </value>
        [DataMember(Name = "user_id")]
        public string CustomerId { get; set; }

        /// <summary>
        /// Gets or sets the name.
        /// </summary>
        /// <value> The name. </value>
        [DataMember(Name = "name")]
        public string Name { get; set; }


        /// <summary>
        /// Gets or sets the postal code.
        /// </summary>
        /// <value> The name. </value>
        [DataMember(Name = "postal_code")]
        public string PostalCode { get; set; }

        #endregion
    }

    [DataContract]
    [EditorBrowsable(EditorBrowsableState.Never)]
    [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Amazon", Justification = "Brand name")]
    public class AmazonTokenResponse
    {
        [DataMember(Name = "access_token")]
        public string AccessToken { get; set; }

        [DataMember(Name = "token_type")]
        public string TokenType { get; set; }

        [DataMember(Name = "expires_in")]
        public long ExpiresIn { get; set; }

        [DataMember(Name = "refresh_token")]
        public string RefreshToken { get; set; }

        [DataMember(Name = "scope")]
        public string Scope { get; set; }

        [DataMember(Name = "error")]
        public string Error { get; set; }

        [DataMember(Name = "error_description")]
        public string ErrorDescription { get; set; }

        [DataMember(Name = "error_uri")]
        public string ErrorUri { get; set; }
    }
 }
    ...
    //
    // GET: /Account/ExternalLoginCallback
    [AllowAnonymous]
    public ActionResult ExternalLoginCallback(string returnUrl)
    {
        // Hack around DotNetOpenAuth and OAuth providers using the returnUrl for things that are in the state parameter (Amazon, Google)
        // Our provider passed the querystring part of the replay address into the state field
        // On return we parse that state parameter and put the arguments back where DotNetOpenAuth expects them, than redirect back to ourselves
        if (!String.IsNullOrEmpty(Request.QueryString["state"]))
        {
            var routeValues = this.RouteData.Values;
            foreach (string key in Request.QueryString.AllKeys)
                if (key.ToLower() == "state")
                {
                    NameValueCollection stateCollection = HttpUtility.ParseQueryString(Request.QueryString["state"]);
                    foreach (string stateKey in stateCollection.AllKeys)
                        routeValues.Add(stateKey, stateCollection[stateKey]);
                }
                else
                {
                    routeValues.Add(key, Request.QueryString[key]);
                }
            return RedirectToRoute(routeValues);
        }

        AuthenticationResult result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
        if (!result.IsSuccessful)
        {
            return RedirectToAction("ExternalLoginFailure");
        }

        if (OAuthWebSecurity.Login(result.Provider, result.ProviderUserId, createPersistentCookie: false))
        {
            return RedirectToLocal(returnUrl);
        }

        if (User.Identity.IsAuthenticated)
        {
            // If the current user is logged in add the new account
            OAuthWebSecurity.CreateOrUpdateAccount(result.Provider, result.ProviderUserId, User.Identity.Name);
            return RedirectToLocal(returnUrl);
        }
        else
        {
            // User is new, ask for their desired membership name
            string loginData = OAuthWebSecurity.SerializeProviderUserId(result.Provider, result.ProviderUserId);
            ViewBag.ProviderDisplayName = OAuthWebSecurity.GetOAuthClientData(result.Provider).DisplayName;
            ViewBag.ReturnUrl = returnUrl;
            return View("ExternalLoginConfirmation", new RegisterExternalLoginModel { UserName = result.UserName, ExternalLoginData = loginData });
        }
    }
    ...