asp.net-mvc-4 – MVC4/DotNetOpenAuth中的自定义OAuth客户端 – 缺少访问令牌密钥

我目前正在为我的应用程序实现DropBox OAuth客户端.直到我结束,这是一个相当轻松的过程.一旦我获得授权,当我尝试访问用户数据时,我从DropBox返回401关于该令牌无效的消息.我在DropBox论坛上问过,看起来我的请求缺少DropBox返回的access_token_secret.我能够使用fiddler来挖掘秘密,并将其添加到我的请求网址,它工作正常,所以这绝对是问题所在.那么为什么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",AccesstokenEndpoint = new MessageReceivingEndpoint("https://api.dropBox.com/1/oauth/access_token",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,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));
                }
            }
        }
    }
}

解决方法

当我在寻找类似问题的解决方案时,我发现了你的问题.我通过制作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",UserAuthorizationEndpoint =
                        new MessageReceivingEndpoint("https://www.linkedin.com/uas/oauth/authorize",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,new AuthenticationOnlyCookieOAuthTokenManager()) { }

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

    //public LinkedInCustomClient(string consumerKey,string consumerSecret) :
    //    base("linkedIn",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,(response as ITokenSecretContainingMessage).TokenSecret);
WebConsumer w = new WebConsumer(LinkedInServiceDescription,imoatm);

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

不幸的是,IOAuthTOkenmanger.ReplaceRequestTokenWithAccesstoken(..)方法直到VerifyAuthentication()方法返回后才会执行,因此我们必须创建一个新的TokenManager,并使用我们刚刚检索的Accesstoken凭据创建一个WebConsumer和HttpWebRequest.

解决了我们简单的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,new PersistentCookieOAuthTokenManager()) { }

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

相关文章

### 创建一个gRPC服务项目(grpc服务端)和一个 webapi项目(...
一、SiganlR 使用的协议类型 1.websocket即时通讯协议 2.Ser...
.Net 6 WebApi 项目 在Linux系统上 打包成Docker镜像,发布为...
一、 PD简介PowerDesigner 是一个集所有现代建模技术于一身的...
一、存储过程 存储过程就像数据库中运行的方法(函数) 优点:...
一、Ueditor的下载 1、百度编辑器下载地址:http://ueditor....