同様の問題の解決策を探していたときに、あなたの質問を見つけました。私は2つの新しいクラスを作成することでそれを解決しました。これについては、このcoderwallの投稿で読むことができます。
また、投稿全体をここにコピーして貼り付けます。
DotNetOpenAuth.AspNet401不正なエラーと永続的なアクセストークンの秘密の修正
Cloud Ebook ManagerであるQuietThymeを設計するとき、私たちと同じように、誰もが新しいアカウントを作成することを嫌うことを知っていました。ソーシャルログインを可能にするために活用できるOAuthおよびOpenIdライブラリを探し始めました。このDotNetOpenAuth.AspNet
ライブラリは、Microsoft、Twitter、Facebook、LinkedIn、Yahooなど、すぐにサポートされるため、ユーザー認証に使用することになりました。すべてを設定する際にいくつかの問題が発生しましたが、最終的には、ほとんどを機能させるためにいくつかの小さなカスタマイズを行うだけで済みました(以前のcoderwallの投稿で説明されています))。他のすべてとは異なり、LinkedInクライアントは認証されず、DotNetOpenAuthから401UnauthorizedErrorが返されることに気づきました。これは署名の問題が原因であることがすぐに明らかになり、ソースを調べた後、取得したAccessTokenシークレットが認証済みプロファイル情報要求で使用されていないことがわかりました。
OAuthClientクラスに取得したアクセストークンシークレットが含まれていない理由は、ASP.NETOAuthライブラリの主な目的である認証目的には通常必要ないためです。
ユーザーがログインした後、メールアドレスやフルネームなどの標準的なプロファイル情報を取得するために、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資格情報を使用してWebConsumer
を作成する必要があります。HttpWebRequest
これにより、単純な401Unauthorizedの問題が解決されます。
認証プロセス後にAccessToken資格情報を保持したい場合はどうなりますか?これは、たとえば、ファイルをユーザーのDropBoxに非同期的に同期したいDropBoxクライアントに役立つ可能性があります。問題はAspNetライブラリの作成方法に戻り、DotNetOpenAuthはユーザーの認証にのみ使用され、今後のOAuthapi呼び出しの基礎としては使用されないと想定されていました。ありがたいことに、修正はかなり簡単でした。私がしなければならなかった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シークレットを永続化するその他のクライアントを変更するだけです。
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))
{}