2012-09-18 50 views
5

我目前正在爲我的應用程序實現Dropbox OAuth客戶端。直到我結束,這是一個相當無痛的過程。一旦我獲得授權,當我嘗試訪問用戶數據時,我會從Dropbox得到一個關於該令牌無效的401回執。我問Dropbox論壇,看起來我的請求缺少Dropbox返回的access_token_secret。我能夠使用Fiddler挖掘祕密,並將其添加到我的請求網址,它運行良好,所以這絕對是問題。那麼爲什麼DotNetOpenAuth在返回訪問令牌時不會返回訪問令牌密鑰呢?MVC4/DotNetOpenAuth中的自定義OAuth客戶端 - 缺少訪問令牌密碼

僅供參考,我的代碼:

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)); 
       } 
      } 
     } 
    } 
} 
+0

我知道有一個更好的方式來格式化代碼,但我不能爲我的生活找到它。點擊問題中的代碼按鈕似乎不起作用。如果有人想告訴如何解決這個問題,非常感謝。 –

+2

代碼格式現在基於標籤,並且您的帖子中沒有任何語言特定的標籤,因此它沒有任何操作。我在代碼上面添加了<! - language:lang-cs - >強制它突出顯示它。請參閱http://meta.stackexchange.com/a/128910/190311 –

回答

5

我在尋找類似問題的解決方案時發現了您的問題。我通過製作2個新課程來解決這個問題,你可以在這個coderwall post中看到。

我也將在這裏複製並粘貼完整的信息:


DotNetOpenAuth.AspNet 401未授權錯誤和持久訪問令牌密鑰固定

在設計QuietThyme,我們的雲電子書經理,我們知道每個人都喜歡創造新的賬戶,就像我們一樣。我們開始尋找OAuth和OpenId庫,我們可以利用這些庫來進行社交登錄。我們最終使用DotNetOpenAuth.AspNet庫進行用戶身份驗證,因爲它支持微軟,Twitter,Facebook,LinkedIn和雅虎等許多其他用戶。儘管我們遇到了一些問題,但最終我們只需要進行一些小的自定義工作即可實現其中的大部分工作(如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); 
     } 
    } 
} 

以下是微軟編寫的基礎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。問題可以追溯到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祕密

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

我最終解決這個問題的方式是不使用內置到ASP.NET中的內容並返回到直接的DNOA,但我也喜歡這種方法。 –

0

是OAuthClient類不包括訪問令牌祕密的原因是,它通常不需要進行身份驗證的目的,這是ASP.NET的OAuth的主要目的圖書館。這就是說,如果你想在你的情況下檢索訪問令牌的祕密,你可以重寫VerifyAuthentication()方法,而不是VerifyAuthenticationCore(),就像你在上面做的那樣。在VerifyAuthentication()內部,您可以調用WebWorker.ProcessUserAuthorization()來驗證登錄,並從返回的AuthorizedTokenResponse對象中訪問令牌密鑰。

+0

但VerifyAuthenticationCore方法具有應包含相同數據的AuthorizedTokenResponse參數。 –

+0

對不起,分心,沒有完成我的評論編輯。從OAuthClient派生時,VerifyAuthenticationCore是一個抽象方法,所以我必須實現它。當然,我可以調用VerifyAuthentication並將它傳遞給HttpContext,但這似乎是冗餘。此外,VerifyAuthenticationCore需要一個AuthorizedTokenResponse,所以不應該有我需要的?事實上,我注意到這個祕密在AuthorizedTokenResponse上,但它是內部保護的。我還有其他方法可以訪問嗎? –

0

做一些挖後,我能夠改變我的構造函數邏輯來解決這個問題如下:

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。我不知道爲什麼,但現在我的訪問令牌密碼正確地附加到我的簽名中的授權請求,一切正常。希望這可以幫助別人。與此同時,我可能會爲博客文章進行清理,因爲在網上有指導(我可以找到)。

編輯:我要撤消我的回答,因爲,正如一位同事指出的那樣,這將處理一個請求,但現在我正在使用內存管理器,一旦我往返完全回到瀏覽器(我假設)。所以我認爲這裏的根本問題是我需要將訪問令牌保密,但我仍然沒有看到如何去做。

0

至於你原來的問題是,祕密不作爲響應提供 - 當您在verifyAuthenticationCore函數中獲得響應時,祕密就在那裏。你得到他們這樣:

string token = response.AccessToken; ; 
    string secret = (response as ITokenSecretContainingMessage).TokenSecret; 
相關問題