【问题标题】:Custom OAuth client in MVC4 / DotNetOpenAuth - missing access token secretMVC4 / DotNetOpenAuth 中的自定义 OAuth 客户端 - 缺少访问令牌密码
【发布时间】:2012-09-18 21:12:13
【问题描述】:

我目前正在为我的应用程序实现一个 Dropbox OAuth 客户端。在我完成之前,这是一个相当轻松的过程。授权后,当我尝试访问用户数据时,我会从 Dropbox 收到关于令牌无效的 401。我在 Dropbox 论坛上问过,看起来我的请求缺少 Dropbox 返回的 access_token_secret。我能够使用 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));
                }
            }
        }
    }
}

【问题讨论】:

  • 我知道有更好的方法来格式化代码,但我一辈子都找不到它。单击问题中的代码按钮似乎不起作用。如果有人想就如何解决这个问题提出建议,非常感谢。
  • 代码格式现在基于标签,并且您的帖子中没有任何特定于语言的标签,因此它没有做任何事情。我在您的代码上方添加了 以强制它突出显示它。见meta.stackexchange.com/a/128910/190311

标签: asp.net-mvc-4 dotnetopenauth


【解决方案1】:

我在寻找类似问题的解决方案时发现了您的问题。我通过创建 2 个新课程解决了这个问题,您可以在 coderwall post 中阅读。

我还将在此处复制并粘贴完整的帖子:


DotNetOpenAuth.AspNet 401 未经授权的错误和持久访问令牌秘密修复

在设计我们的云电子书管理器 QuietThyme 时,我们知道每个人都和我们一样讨厌创建新帐户。我们开始寻找可以用来进行社交登录的 OAuth 和 OpenId 库。我们最终使用DotNetOpenAuth.AspNet 库进行用户身份验证,因为它支持Microsoft、Twitter、Facebook、LinkedIn 和Yahoo 以及许多其他公司。虽然我们在设置这一切时遇到了一些问题,但最终我们只需要进行一些小的自定义即可使其大部分工作(在previous coderwall post 中描述)。我们注意到,与其他所有客户端不同,LinkedIn 客户端不会进行身份验证,从 DotNetOpenAuth 返回 401 Unauthorized Error。很快就发现这是由于签名问题造成的,在查看源代码后,我们能够确定检索到的 AccessToken 密钥没有与经过身份验证的配置文件信息请求一起使用。

这很有意义,OAuthClient 类不包含检索到的访问令牌秘密的原因是它通常不需要用于身份验证目的,这是 ASP.NET OAuth 库的主要目的。

我们需要在用户登录后针对 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);
        }
    }
}

这是从 Microsoft 编写的基本 LinkedIn 客户端更改的部分。

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 并使用我们刚刚检索到的 AccessToken 凭据创建一个 WebConsumerHttpWebRequest .

这解决了我们简单的 401 Unauthorized 问题。

现在,如果您希望在身份验证过程之后保留 AccessToken 凭据,会发生什么?例如,这对于 DropBox 客户端可能很有用,您希望将文件异步同步到用户的 DropBox。问题可以追溯到 AspNet 库的编写方式,假设 DotNetOpenAuth 仅用于用户身份验证,而不是作为进一步 OAuth api 调用的基础。值得庆幸的是,修复相当简单,我所要做的就是修改基础 AuthetnicationOnlyCookieOAuthTokenManger 以便 ReplaceRequestTokenWithAccessToken(..) 方法存储新的 AccessToken 密钥和秘密。

/// <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 构造函数,或者您希望保留 AccessToken Secret 的任何其他客户端

    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))
    {}

【讨论】:

  • 我最终通过不使用 ASP.NET 内置的东西并回到直接的 DNOA 来解决这个问题,但我也喜欢这种方法。
【解决方案2】:

OAuthClient 类不包含访问令牌秘密的原因是它通常不需要用于身份验证目的,这是 ASP.NET OAuth 库的主要目的。

也就是说,如果您想在您的情况下检索访问令牌秘密,您可以覆盖 VerifyAuthentication() 方法,而不是像上面那样做的 VerifyAuthenticationCore()。在 VerifyAuthentication() 中,您可以调用 WebWorker.ProcessUserAuthorization() 来验证登录,并从返回的 AuthorizedTokenResponse 对象中访问令牌密钥。

【讨论】:

  • 但是 VerifyAuthenticationCore 方法有一个 AuthorizedTokenResponse 参数应该包含相同的数据。
  • 抱歉,分心了,没有完成我的评论编辑。从 OAuthClient 派生时,VerifyAuthenticationCore 是一个抽象方法,所以我必须实现它。当然,我可以调用 VerifyAuthentication 并将 HttpContext 传递给它,但这似乎是多余的。另外,VerifyAuthenticationCore 需要一个 AuthorizedTokenResponse,所以那不应该有我需要的吗?事实上,我注意到这个秘密在 AuthorizedTokenResponse 上,但它是受内部保护的。我应该通过其他方式访问它吗?
【解决方案3】:

在进行了一些挖掘之后,我能够通过更改构造函数逻辑来解决这个问题,如下所示:

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。我不知道为什么,但现在我的访问令牌秘密已正确附加到授权请求中的签名中,并且一切正常。希望这对其他人有帮助。同时,我可能会为博客文章清理此内容,因为网络上(我可以找到)指导这样做。

编辑:我将撤消我的回答,因为正如一位同事指出的那样,这将处理一个请求,但现在我正在使用内存管理器,一旦我完全往返就会刷新回到浏览器(我假设)。所以我认为这里的根本问题是我需要获取访问令牌秘密,我还没有看到如何去做。

【讨论】:

    【解决方案4】:

    至于您最初的问题,即未在响应中提供秘密 - 当您在 verifyAuthenticationCore 函数中获得响应时,秘密就在那里。你可以像这样得到它们:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-24
      • 2016-05-31
      • 1970-01-01
      • 2021-09-12
      • 2013-11-26
      • 2018-11-22
      • 1970-01-01
      • 2017-05-31
      相关资源
      最近更新 更多