如果任何人碰到這個問題,我在這裏介紹我所發現的(有比較難看的解決辦法在一起)。
使用Fiddler檢查DotNetOpenAuth
和Twitter之間的HTTP流量,很明顯身份驗證請求包含force_login=false
查詢字符串參數,這表明DNOA正常工作。但是,如果我使用Fiddler的腳本功能來修改出站請求並完全刪除參數,則所有內容都將開始正常工作。我猜測Twitter的實現在這裏是錯誤的,將任何force_login
參數的存在視爲相當於force_login=true
。
因爲我不認爲有可能讓Twitter來修改他們的API的行爲,我調查了是否有一個更容易訪問的解決方案。
綜觀DNOA代碼,我看到force_login=false
參數由DotNetOpenAuthWebConsumer.RequestAuthentication()
方法無條件添加到HTTP請求(並隨後在需要時修改以true
)。
因此,理想的解決方案是讓DNOA對其認證請求參數進行更細粒度的控制,並且TwitterClient
明確地刪除參數force_login=false
。不幸的是,目前的DNOA代碼庫並不直接支持這一點,但可以通過創建兩個自定義類來實現相同的效果。
首先是一個自定義實現的IOAuthWebWorker
這是原始DotNetOpenAuthWebConsumer
類的直接拷貝,除了單行變化初始化重定向參數字典作爲一個空的字典:
using System;
using System.Collections.Generic;
using System.Net;
using DotNetOpenAuth.AspNet.Clients;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth;
using DotNetOpenAuth.OAuth.ChannelElements;
using DotNetOpenAuth.OAuth.Messages;
namespace CustomDotNetOpenAuth
{
public class CustomDotNetOpenAuthWebConsumer : IOAuthWebWorker, IDisposable
{
private readonly WebConsumer _webConsumer;
public CustomDotNetOpenAuthWebConsumer(ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager)
{
if (serviceDescription == null) throw new ArgumentNullException("serviceDescription");
if (tokenManager == null) throw new ArgumentNullException("tokenManager");
_webConsumer = new WebConsumer(serviceDescription, tokenManager);
}
public HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint profileEndpoint, string accessToken)
{
return _webConsumer.PrepareAuthorizedRequest(profileEndpoint, accessToken);
}
public AuthorizedTokenResponse ProcessUserAuthorization()
{
return _webConsumer.ProcessUserAuthorization();
}
public void RequestAuthentication(Uri callback)
{
var redirectParameters = new Dictionary<string, string>();
var request = _webConsumer.PrepareRequestUserAuthorization(callback, null, redirectParameters);
_webConsumer.Channel.PrepareResponse(request).Send();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_webConsumer.Dispose();
}
}
}
}
另要求是自定義OAuthClient
類,基於原始TwitterClient
類。請注意,這需要比原始TwitterClient
類多一點的代碼,因爲它也需要複製一對夫婦的是內部的DNOA基類或其他實用工具類方法:
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using DotNetOpenAuth.AspNet;
using DotNetOpenAuth.AspNet.Clients;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth;
using DotNetOpenAuth.OAuth.ChannelElements;
using DotNetOpenAuth.OAuth.Messages;
namespace CustomDotNetOpenAuth
{
public class CustomTwitterClient : OAuthClient
{
private static readonly string[] UriRfc3986CharsToEscape = new[] { "!", "*", "'", "(", ")" };
public static readonly ServiceProviderDescription TwitterServiceDescription = new ServiceProviderDescription
{
RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
};
public CustomTwitterClient(string consumerKey, string consumerSecret)
: this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
{
}
public CustomTwitterClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
: base("twitter", new CustomDotNetOpenAuthWebConsumer(TwitterServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager)))
{
}
protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response)
{
var accessToken = response.AccessToken;
var userId = response.ExtraData["user_id"];
var userName = response.ExtraData["screen_name"];
var profileRequestUrl = new Uri("https://api.twitter.com/1/users/show.xml?user_id=" + EscapeUriDataStringRfc3986(userId));
var profileEndpoint = new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest);
var request = WebWorker.PrepareAuthorizedRequest(profileEndpoint, accessToken);
var extraData = new Dictionary<string, string> { { "accesstoken", accessToken } };
try
{
using (var profileResponse = request.GetResponse())
{
using (var responseStream = profileResponse.GetResponseStream())
{
var document = xLoadXDocumentFromStream(responseStream);
AddDataIfNotEmpty(extraData, document, "name");
AddDataIfNotEmpty(extraData, document, "location");
AddDataIfNotEmpty(extraData, document, "description");
AddDataIfNotEmpty(extraData, document, "url");
}
}
}
catch
{
// At this point, the authentication is already successful. Here we are just trying to get additional data if we can. If it fails, no problem.
}
return new AuthenticationResult(true, ProviderName, userId, userName, extraData);
}
private static XDocument xLoadXDocumentFromStream(Stream stream)
{
const int maxChars = 0x10000; // 64k
var settings = new XmlReaderSettings
{
MaxCharactersInDocument = maxChars
};
return XDocument.Load(XmlReader.Create(stream, settings));
}
private static void AddDataIfNotEmpty(Dictionary<string, string> dictionary, XDocument document, string elementName)
{
var element = document.Root.Element(elementName);
if (element != null)
{
AddItemIfNotEmpty(dictionary, elementName, element.Value);
}
}
private static void AddItemIfNotEmpty(IDictionary<string, string> dictionary, string key, string value)
{
if (key == null)
{
throw new ArgumentNullException("key");
}
if (!string.IsNullOrEmpty(value))
{
dictionary[key] = value;
}
}
private static string EscapeUriDataStringRfc3986(string value)
{
var escaped = new StringBuilder(Uri.EscapeDataString(value));
for (var i = 0; i < UriRfc3986CharsToEscape.Length; i++)
{
escaped.Replace(UriRfc3986CharsToEscape[i], Uri.HexEscape(UriRfc3986CharsToEscape[i][0]));
}
return escaped.ToString();
}
}
}
已經創建這兩個自定義類,實現只需要在MVC4 AuthConfig.cs
文件註冊新的CustomTwitterClient
類的一個實例:
OAuthWebSecurity.RegisterClient(new CustomTwitterClient("myTwitterApiKey", "myTwitterApiSecret"));
我已經證實了這一工作。花了5分鐘。謝謝蒂姆。 – Matt