Пользовательский клиент OAuth в MVC4 / DotNetOpenAuth - отсутствует секретный токен доступа

В настоящее время я работаю над реализацией OAuth-клиента Dropbox для моего приложения. Это был довольно безболезненный процесс, пока я не достиг конца. После авторизации, когда я пытаюсь получить доступ к данным пользователя, я получаю 401 обратно от Dropbox о том, что токен недействителен. Я спросил на форумах Dropbox, и, похоже, в моем запросе отсутствует access_token_secret, который Dropbox возвращает обратно. Я смог использовать Fiddler, чтобы найти секрет и добавить его в URL-адрес моего запроса, и он работал нормально, так что это определенно проблема. Так почему же DotNetOpenAuth не возвращает обратно секрет токена доступа, когда возвращает токен доступа?

Для справки мой код:

public class DropboxClient : OAuthClient
{
    public static readonly ServiceProviderDescription DropboxServiceDescription = new ServiceProviderDescription
    {
        RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
        UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.dropbox.com/1/oauth/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
        AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
        TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new PlaintextSigningBindingElement() }
    };

    public DropboxClient(string consumerKey, string consumerSecret) : 
        this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
    {
    }

    public DropboxClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) : 
        base("dropbox", DropboxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
    {
    }

    protected override DotNetOpenAuth.AspNet.AuthenticationResult VerifyAuthenticationCore(DotNetOpenAuth.OAuth.Messages.AuthorizedTokenResponse response)
    {            
        var profileEndpoint = new MessageReceivingEndpoint("https://api.dropbox.com/1/account/info", HttpDeliveryMethods.GetRequest);
        HttpWebRequest request = this.WebWorker.PrepareAuthorizedRequest(profileEndpoint, response.AccessToken);

        try
        {
            using (WebResponse profileResponse = request.GetResponse())
            {
                using (Stream profileResponseStream = profileResponse.GetResponseStream())
                {
                    using (StreamReader reader = new StreamReader(profileResponseStream))
                    {
                        string jsonText = reader.ReadToEnd();
                        JavaScriptSerializer jss = new JavaScriptSerializer();
                        dynamic jsonData = jss.DeserializeObject(jsonText);
                        Dictionary<string, string> extraData = new Dictionary<string, string>();
                        extraData.Add("displayName", jsonData.display_name ?? "Unknown");
                        extraData.Add("userId", jsonData.uid ?? "Unknown");
                        return new DotNetOpenAuth.AspNet.AuthenticationResult(true, ProviderName, extraData["userId"], extraData["displayName"], extraData);
                    }
                }
            }
        }
        catch (WebException ex)
        {
            using (Stream s = ex.Response.GetResponseStream())
            {
                using (StreamReader sr = new StreamReader(s))
                {
                    string body = sr.ReadToEnd();
                    return new DotNetOpenAuth.AspNet.AuthenticationResult(new Exception(body, ex));
                }
            }
        }
    }
}

4 ответа

Решение

Я нашел ваш вопрос, когда искал решение похожей проблемы. Я решил это, создав 2 новых класса, о которых вы можете прочитать в этом посте Coderwall.

Я также скопирую и вставлю полный пост здесь:


DotNetOpenAuth.AspNet 401 Секретное исправление несанкционированной ошибки и постоянного маркера доступа

При разработке QuietThyme, нашего Cloud Ebook Manager, мы знали, что все ненавидят создание новых учетных записей так же, как и мы. Мы начали искать библиотеки OAuth и OpenId, которые могли бы использовать для социальной регистрации. Мы закончили тем, что использовали DotNetOpenAuth.AspNet библиотека для аутентификации пользователей, потому что она поддерживает Microsoft, Twitter, Facebook, LinkedIn и Yahoo и многие другие прямо из носа. Несмотря на то, что у нас были некоторые проблемы с настройкой всего этого, в конце нам потребовалось всего лишь сделать несколько небольших настроек, чтобы большинство из них работало (описано в предыдущем посте о coderwall). Мы заметили, что, в отличие от всех остальных, клиент LinkedIn не будет проходить аутентификацию, возвращая 401 несанкционированную ошибку от DotNetOpenAuth. Быстро стало очевидно, что это было связано с проблемой подписи, и после просмотра источника мы смогли определить, что извлеченный секрет Access Token не используется с запросом информации профиля аутентифицированного профиля.

На самом деле это имеет смысл, причина в том, что класс OAuthClient не включает в себя извлеченный секрет токена доступа, состоит в том, что он обычно не нужен для целей аутентификации, что является основной целью библиотеки OAuth ASP.NET.

После того, как пользователь вошел в систему, нам нужно было выполнять аутентифицированные запросы к API, чтобы получить некоторую стандартную информацию профиля, включая адрес электронной почты и полное имя. Мы смогли решить эту проблему, временно используя InMemoryOAuthTokenManager.

public class LinkedInCustomClient : OAuthClient
{
    private static XDocument LoadXDocumentFromStream(Stream stream)
    {
        var settings = new XmlReaderSettings
        {
            MaxCharactersInDocument = 65536L
        };
        return XDocument.Load(XmlReader.Create(stream, settings));
    }

    /// Describes the OAuth service provider endpoints for LinkedIn.
    private static readonly ServiceProviderDescription LinkedInServiceDescription =
            new ServiceProviderDescription
            {
                AccessTokenEndpoint =
                        new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/accessToken",
                        HttpDeliveryMethods.PostRequest),
                RequestTokenEndpoint =
                        new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/requestToken?scope=r_basicprofile+r_emailaddress",
                        HttpDeliveryMethods.PostRequest),
                UserAuthorizationEndpoint =
                        new MessageReceivingEndpoint("https://www.linkedin.com/uas/oauth/authorize",
                        HttpDeliveryMethods.PostRequest),
                TamperProtectionElements =
                        new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
                //ProtocolVersion = ProtocolVersion.V10a
            };

    private string ConsumerKey { get; set; }
    private string ConsumerSecret { get; set; }

    public LinkedInCustomClient(string consumerKey, string consumerSecret)
        : this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager()) { }

    public LinkedInCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
        : base("linkedIn", LinkedInServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
    {
        ConsumerKey = consumerKey;
        ConsumerSecret = consumerSecret;
    }

    //public LinkedInCustomClient(string consumerKey, string consumerSecret) :
    //    base("linkedIn", LinkedInServiceDescription, consumerKey, consumerSecret) { }

    /// Check if authentication succeeded after user is redirected back from the service provider.
    /// The response token returned from service provider authentication result. 
    [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
        Justification = "We don't care if the request fails.")]
    protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response)
    {
        // See here for Field Selectors API http://developer.linkedin.com/docs/DOC-1014
        const string profileRequestUrl =
            "https://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,industry,summary,email-address)";

        string accessToken = response.AccessToken;

        var profileEndpoint =
            new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest);

        try
        {
            InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
            imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
            WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);

            HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);

            using (WebResponse profileResponse = request.GetResponse())
            {
                using (Stream responseStream = profileResponse.GetResponseStream())
                {
                    XDocument document = LoadXDocumentFromStream(responseStream);
                    string userId = document.Root.Element("id").Value;

                    string firstName = document.Root.Element("first-name").Value;
                    string lastName = document.Root.Element("last-name").Value;
                    string userName = firstName + " " + lastName;

                    string email = String.Empty;
                    try
                    {
                        email = document.Root.Element("email-address").Value;
                    }
                    catch(Exception)
                    {
                    }

                    var extraData = new Dictionary<string, string>();
                    extraData.Add("accesstoken", accessToken);
                    extraData.Add("name", userName);
                    extraData.AddDataIfNotEmpty(document, "headline");
                    extraData.AddDataIfNotEmpty(document, "summary");
                    extraData.AddDataIfNotEmpty(document, "industry");

                    if(!String.IsNullOrEmpty(email))
                    {
                        extraData.Add("email",email);
                    }

                    return new AuthenticationResult(
                        isSuccessful: true, provider: this.ProviderName, providerUserId: userId, userName: userName, extraData: extraData);
                }
            }
        }
        catch (Exception exception)
        {
            return new AuthenticationResult(exception);
        }
    }
}

Вот раздел, который изменился по сравнению с базовым клиентом LinkedIn, написанным Microsoft.

InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);

HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);

К сожалению, IOAuthTOkenManger.ReplaceRequestTokenWithAccessToken(..) метод не выполняется, пока после VerifyAuthentication() метод возвращает, поэтому мы вместо этого должны создать новый TokenManager и и создать WebConsumer а также HttpWebRequest используя учетные данные Access Token, которые мы только что получили.

Это решает нашу простую 401 Несанкционированную проблему.

Что произойдет, если вы захотите сохранить учетные данные Access Token после процесса аутентификации? Это может быть полезно, например, для клиента DropBox, где вы хотите асинхронно синхронизировать файлы с DropBox пользователя. Проблема восходит к тому, как была написана библиотека AspNet, предполагалось, что DotNetOpenAuth будет использоваться только для аутентификации пользователей, а не в качестве основы для дальнейших вызовов API OAuth. К счастью, исправление было довольно простым, все, что мне нужно было сделать, это изменить базу AuthetnicationOnlyCookieOAuthTokenManger таким образом ReplaceRequestTokenWithAccessToken(..) Метод хранит новый ключ и секреты Access Token.

/// <summary>
/// Stores OAuth tokens in the current request's cookie
/// </summary>
public class PersistentCookieOAuthTokenManagerCustom : AuthenticationOnlyCookieOAuthTokenManager
{
    /// <summary>
    /// Key used for token cookie
    /// </summary>
    private const string TokenCookieKey = "OAuthTokenSecret";

    /// <summary>
    /// Primary request context.
    /// </summary>
    private readonly HttpContextBase primaryContext;

    /// <summary>
    /// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
    /// </summary>
    public PersistentCookieOAuthTokenManagerCustom() : base()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
    /// </summary>
    /// <param name="context">The current request context.</param>
    public PersistentCookieOAuthTokenManagerCustom(HttpContextBase context) : base(context)
    {
        this.primaryContext = context;
    }

    /// <summary>
    /// Gets the effective HttpContext object to use.
    /// </summary>
    private HttpContextBase Context
    {
        get
        {
            return this.primaryContext ?? new HttpContextWrapper(HttpContext.Current);
        }
    }


    /// <summary>
    /// Replaces the request token with access token.
    /// </summary>
    /// <param name="requestToken">The request token.</param>
    /// <param name="accessToken">The access token.</param>
    /// <param name="accessTokenSecret">The access token secret.</param>
    public new void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret)
    {
        //remove old requestToken Cookie
        //var cookie = new HttpCookie(TokenCookieKey)
        //{
        //    Value = string.Empty,
        //    Expires = DateTime.UtcNow.AddDays(-5)
        //};
        //this.Context.Response.Cookies.Set(cookie);

        //Add new AccessToken + secret Cookie
        StoreRequestToken(accessToken, accessTokenSecret);

    }

}

Тогда использовать это PersistentCookieOAuthTokenManager все, что вам нужно сделать, это изменить конструктор DropboxClient или любой другой клиент, где вы хотите сохранить секрет Access Token

    public DropBoxCustomClient(string consumerKey, string consumerSecret)
        : this(consumerKey, consumerSecret, new PersistentCookieOAuthTokenManager()) { }

    public DropBoxCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
        : base("dropBox", DropBoxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
    {}

Причина, по которой класс OAuthClient не включает в себя секрет токена доступа, заключается в том, что он обычно не требуется для проверки подлинности, которая является основной целью библиотеки OAuth ASP.NET.

Тем не менее, если вы хотите получить секрет токена доступа в вашем случае, вы можете переопределить метод VerifyAuthentication() вместо VerifyAuthenticationCore(), как вы делали выше. Внутри VerifyAuthentication() вы можете вызвать WebWorker.ProcessUserAuthorization() для проверки имени входа, и из возвращенного объекта AuthorizedTokenResponse вы получите доступ к секрету токена.

Что касается вашего исходного вопроса о том, что секрет не предоставлен в ответе - секрет находится здесь, когда вы получаете ответ в функции verifyAuthenticationCore. Вы получаете их обоих так:

  string token = response.AccessToken; ;
  string secret = (response as ITokenSecretContainingMessage).TokenSecret; 

После некоторых копаний я смог решить эту проблему, изменив логику конструктора следующим образом:

public DropboxClient(string consumerKey, string consumerSecret) : 
    this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
{
}

public DropboxClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) : 
    base("dropbox", DropboxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{
}

становится

public DropboxClient(string consumerKey, string consumerSecret) : 
        base("dropbox", DropboxServiceDescription, consumerKey, consumerSecret)
    {
    }

Копание источника DNOA показывает, что если вы создаете OAuthClient (мой базовый класс) только с ключом и секретом потребителя, он использует InMemoryOAuthTokenManager вместо SimpleConsumerTokenManager. Я не знаю почему, но теперь мой секретный токен правильно добавлен к моей подписи в авторизованном запросе, и все работает. Надеюсь, это поможет кому-то еще. В то же время я, скорее всего, исправлю это для поста в блоге, поскольку в сети нет никаких рекомендаций (которые я могу найти) для этого.

РЕДАКТИРОВАТЬ: Я собираюсь отменить свой ответ, поскольку, как указал коллега, это будет обрабатывать один запрос, но теперь, когда я использую менеджер в памяти, который будет сбрасываться, когда я полностью возвращаюсь к браузер (я предполагаю). Поэтому я думаю, что основная проблема здесь в том, что мне нужно получить секрет токена доступа, чего я до сих пор не знаю, как это сделать.

Другие вопросы по тегам