MVC4/DotNetOpenAuth中的自定义OAuth客户端-缺少访问令牌机密



我目前正在为我的应用程序实现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));
}
}
}
}
}

我在寻找类似问题的解决方案时发现了您的问题。我通过制作两个新类解决了这个问题,你可以在这篇代码墙文章中阅读。

我也会在这里复制并粘贴完整的帖子:


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

在设计我们的云Ebook Manager QuietTheme时,我们知道每个人都和我们一样讨厌创建新帐户。我们开始寻找OAuth和OpenId库,我们可以利用这些库进行社交登录。我们最终使用DotNetOpenAuth.AspNet库进行用户身份验证,因为它支持微软、推特、脸书、领英和雅虎,以及许多其他公司。虽然我们在设置这一切时遇到了一些问题,但最终我们只需要做一些小的定制就可以让大部分工作正常进行(在之前的coderwall文章中描述过)。我们注意到,与其他所有客户端不同,LinkedIn客户端不会进行身份验证,从DotNetOpenAuth返回401未授权错误。很快就很明显,这是由于签名问题造成的,在查看了来源后,我们能够确定检索到的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);
}
}
}

以下部分与微软编写的基本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未授权问题。

现在,如果您想在身份验证过程后保留AccessToken凭据,会发生什么?这可能对DropBox客户端很有用,例如,在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))
{}

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

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

经过一些挖掘,我能够通过如下更改构造函数逻辑来解决这个问题:

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。我不知道为什么,但现在我的访问令牌秘密被正确地附加到了授权请求中的签名中,一切都正常了。希望这能帮助其他人。与此同时,我可能会把它整理成一篇博客文章,因为网上有指导(我可以找到)。

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

至于您最初的问题,即机密没有在响应中提供——当您在verifyAuthenticationCore函数中获得响应时,机密就在那里。它们都是这样的:

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

相关内容

最新更新