OAuth2 и DotNetOpenAuth - внедрение пользовательского клиента Google

У меня возникла проблема с использованием пользовательского OAuth2Client для Google с использованием DotNetOpenAuth и MVC4.

У меня есть точка, где я могу успешно сделать запрос авторизации на конечную точку google https://accounts.google.com/o/oauth2/auth

и Google спрашивает, разрешит ли пользователь доступ к моему аккаунту. Все хорошо до сих пор. Когда пользователь нажимает "ОК", Google затем вызывает мой обратный URL-адрес, как ожидалось.

Проблема заключается в том, что я вызываю метод VerifyAuthentication в классе OAuthWebSecurity (Microsoft.Web.WebPages.OAuth)

var authenticationResult = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));

Он всегда возвращает AuthenticationResult с IsSuccessful = false и Provider = ""

Я просмотрел код для этого, и класс OAuthWebSecurity пытается получить имя поставщика из

Request.QueryString["__provider__"]

но Google не отправляет эту информацию в запрос. Другой поставщик, которого я реализовал (LinkedIn), отправляет имя провайдера обратно, и все работает отлично.

Я не уверен, что я могу сделать с этого момента, кроме отказа от классов Microsoft.Web.WebPages.OAuth и просто использовать DotNetOpenAuth без них, но я надеялся, что у кого-то может быть другое решение, которое я могу попробовать...

Я искал экстенсивно, но не могу найти ничего, чтобы помочь... Мне было очень сложно даже найти примеры людей, делающих то же самое, что меня действительно удивило.

Любая помощь очень ценится!

Ответы

Ответ 1

Обновление: как упоминает Мэтт Джонсон ниже, он упаковал решение, которое вы можете получить от GitHub: https://github.com/mj1856/DotNetOpenAuth.GoogleOAuth2

Как он отмечает: DNOA и OAuthWebSecurity для ASP.Net MVC 4 поставляются только с поставщиком OpenId для Google. Это клиент OAuth2, который вы можете использовать вместо этого.

ВАЖНО - Если вы используете ASP.Net MVC 5, этот пакет не применим. Вместо этого следует использовать Microsoft.Owin.Security.Google. (Он также поставляется с шаблонами стартеров MVC 5 в VS 2013).


Я обошел это в конце, поймав запрос, когда он входит, и сделав мой собственный чек, чтобы узнать, от какого провайдера он пришел. Google позволяет вам отправлять параметр в запрос OAuth, называемый "state", который они просто передают вам обратно, когда они делают обратный вызов, поэтому я использую это для передачи имени поставщика для google, и я проверяю это на отсутствие "__provider__".

что-то вроде этого:

 public String GetProviderNameFromQueryString(NameValueCollection queryString)
    {
        var result = queryString["__provider__"];

        if (String.IsNullOrWhiteSpace(result))
        {
            result = queryString["state"];
        }

        return result;
    }

Затем я реализовал пользовательский OAuth2Client для Google, и я вручную вызываю метод VerifyAuthentication сам, минуя материал оболочки Microsoft.

 if (provider is GoogleCustomClient)
        {
            authenticationResult = ((GoogleCustomClient)provider).VerifyAuthentication(context, new Uri(String.Format("{0}/oauth/ExternalLoginCallback", context.Request.Url.GetLeftPart(UriPartial.Authority).ToString())));
        }
        else
        {
            authenticationResult = OAuthWebSecurity.VerifyAuthentication(returnUrl);
        } 

Это позволило мне сохранить материал, который я уже имел для других поставщиков, использующих оболочки Microsoft.

В соответствии с требованиями @1010100 1001010, вот мой пользовательский OAuth2Client для Google (ПРИМЕЧАНИЕ: НУЖДАЕТСЯ НЕКОТОРЫЕ УБЫТКИ! Я НЕ СДЕЛАЛ КРУГЛЫЙ, КОТОРЫЙ СДЕЛАЛ КОД ИСПОЛНЕНИЯ).

public class GoogleCustomClient : OAuth2Client
{
    ILogger _logger;

    #region Constants and Fields

    /// <summary>
    /// The authorization endpoint.
    /// </summary>
    private const string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/auth";

    /// <summary>
    /// The token endpoint.
    /// </summary>
    private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token";

    /// <summary>
    /// The _app id.
    /// </summary>
    private readonly string _clientId;

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

    #endregion


    public GoogleCustomClient(string clientId, string clientSecret)
        : base("Google")
    {
        if (string.IsNullOrWhiteSpace(clientId)) throw new ArgumentNullException("clientId");
        if (string.IsNullOrWhiteSpace(clientSecret)) throw new ArgumentNullException("clientSecret");

        _logger = ObjectFactory.GetInstance<ILogger>();

        this._clientId = clientId;
        this._clientSecret = clientSecret;
    }

    protected override Uri GetServiceLoginUrl(Uri returnUrl)
    {
        StringBuilder serviceUrl = new StringBuilder();

        serviceUrl.AppendFormat("{0}?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile", AuthorizationEndpoint);
        serviceUrl.Append("&state=google");
        serviceUrl.AppendFormat("&redirect_uri={0}", returnUrl.ToString());
        serviceUrl.Append("&response_type=code");
        serviceUrl.AppendFormat("&client_id={0}", _clientId);

        return new Uri(serviceUrl.ToString());

    }

    protected override IDictionary<string, string> GetUserData(string accessToken)
    {
        RestClient client = new RestClient("https://www.googleapis.com");
        var request = new RestRequest(String.Format("/oauth2/v1/userinfo?access_token={0}", accessToken), Method.GET);
        IDictionary<String, String> extraData = new Dictionary<String, String>();

        var response = client.Execute(request);
        if (null != response.ErrorException)
        {
            return null;
        }
        else
        {
            try
            {
                var json = JObject.Parse(response.Content);

                string firstName = (string)json["given_name"];
                string lastName = (string)json["family_name"];
                string emailAddress = (string)json["email"];
                string id = (string)json["id"];

                extraData = new Dictionary<String, String>
                {
                    {"accesstoken", accessToken}, 
                    {"name", String.Format("{0} {1}", firstName, lastName)},
                    {"firstname", firstName},
                    {"lastname", lastName},
                    {"email", emailAddress},
                    {"id", id}                                           
                };
            }
            catch(Exception ex)
            {
                _logger.Error("Error requesting OAuth user data from Google", ex);
                return null;
            }
            return extraData;
        }

    }

    protected override string QueryAccessToken(Uri returnUrl, string authorizationCode)
    {
        StringBuilder postData = new StringBuilder();
        postData.AppendFormat("client_id={0}", this._clientId);
        postData.AppendFormat("&redirect_uri={0}", HttpUtility.UrlEncode(returnUrl.ToString()));
        postData.AppendFormat("&client_secret={0}", this._clientSecret);
        postData.AppendFormat("&grant_type={0}", "authorization_code");
        postData.AppendFormat("&code={0}", authorizationCode);


        string response = "";
        string accessToken = "";

        var webRequest = (HttpWebRequest)WebRequest.Create(TokenEndpoint);

        webRequest.Method = "POST";
        webRequest.ContentType = "application/x-www-form-urlencoded";

        try
        {

            using (Stream s = webRequest.GetRequestStream())
            {
                using (StreamWriter sw = new StreamWriter(s))
                    sw.Write(postData.ToString());
            }

            using (WebResponse webResponse = webRequest.GetResponse())
            {
                using (StreamReader reader = new StreamReader(webResponse.GetResponseStream()))
                {
                    response = reader.ReadToEnd();
                }
            }

            var json = JObject.Parse(response);
            accessToken = (string)json["access_token"];
        }
        catch(Exception ex)
        {
            _logger.Error("Error requesting OAuth access token from Google", ex);
            return null;
        }

        return accessToken;

    }

    public override AuthenticationResult VerifyAuthentication(HttpContextBase context, Uri returnPageUrl)
    {

        string code = context.Request.QueryString["code"];
        if (string.IsNullOrEmpty(code))
        {
            return AuthenticationResult.Failed;
        }

        string accessToken = this.QueryAccessToken(returnPageUrl, code);
        if (accessToken == null)
        {
            return AuthenticationResult.Failed;
        }

        IDictionary<string, string> userData = this.GetUserData(accessToken);
        if (userData == null)
        {
            return AuthenticationResult.Failed;
        }

        string id = userData["id"];
        string name;

        // Some oAuth providers do not return value for the 'username' attribute. 
        // In that case, try the 'name' attribute. If it still unavailable, fall back to 'id'
        if (!userData.TryGetValue("username", out name) && !userData.TryGetValue("name", out name))
        {
            name = id;
        }

        // add the access token to the user data dictionary just in case page developers want to use it
        userData["accesstoken"] = accessToken;

        return new AuthenticationResult(
            isSuccessful: true, provider: this.ProviderName, providerUserId: id, userName: name, extraData: userData);
    }