2012-09-18 6 views
5

Ich arbeite gerade an der Implementierung eines Dropbox-OAuth-Clients für meine Anwendung. Es war ein ziemlich schmerzloser Prozess, bis ich das Ende erreichte. Sobald ich autorisiert habe, wenn ich versuche, auf Benutzerdaten zuzugreifen, bekomme ich eine 401 von Dropbox zurück, über das Token ungültig ist. Ich fragte in den Dropbox-Foren und es sieht so aus, als ob meine Anfrage den access_token_secret fehlt, den Dropbox zurückgibt. Ich konnte Fiddler verwenden, um das Geheimnis auszugraben und es meiner Anfrage-URL hinzuzufügen, und es funktionierte gut, also ist das definitiv das Problem. Warum gibt DotNetOpenAuth das Zugriffs-Token-Geheimnis nicht zurück, wenn es das Zugriffs-Token zurückgibt?Benutzerdefinierter OAuth-Client in MVC4/DotNetOpenAuth - fehlendes Zugriffstokengeheimnis

Als Referenz mein Code:

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

Ich weiß, es gibt eine schönere Möglichkeit, den Code zu formatieren, aber ich kann nicht für das Leben von mir finden. Das Klicken auf die Code-Schaltfläche in der Frage schien nicht zu funktionieren. Wenn jemand Ratschläge geben möchte, wie das zu beheben ist, sehr geschätzt. –

+2

Code-Formatierung basiert jetzt auf Tags und Sie hatten keine sprachspezifischen Tags in Ihrem Post, so dass es nichts getan hat. Ich habe über Ihrem Code hinzugefügt, um die Hervorhebung zu erzwingen. Siehe http://meta.stackexchange.com/a/128910/190311 –

Antwort

5

Ich habe Ihre Frage gefunden, als ich nach einer Lösung für ein ähnliches Problem suchte. Ich habe es gelöst, indem ich 2 neue Klassen gemacht habe, über die man in dieser coderwall post lesen kann.

Ich werde auch hier den vollständigen Beitrag kopieren und einfügen:


DotNetOpenAuth.AspNet 401 Unauthorized Fehler und Persistent Zugriffstoken Geheimnis Fix

Wenn QuietThyme Gestaltung unserer Cloud-Ebook-Manager, wussten wir, dass jeder es hasst, neue Konten genauso zu erstellen wie wir. Wir haben angefangen, nach OAuth- und OpenId-Bibliotheken zu suchen, die wir für den sozialen Login verwenden können. Wir haben am Ende die DotNetOpenAuth.AspNet Bibliothek für die Benutzerauthentifizierung verwendet, weil sie Microsoft, Twitter, Facebook, LinkedIn und Yahoo und viele andere direkt aus dem Bug unterstützt. Während wir einige Probleme hatten, die alles aufstellten, mussten wir am Ende nur ein paar kleine Anpassungen vornehmen, um das meiste davon zum Laufen zu bringen (beschrieben in einer previous coderwall post).Uns ist aufgefallen, dass sich der LinkedIn-Client im Gegensatz zu allen anderen nicht authentifizieren und einen 401-nicht autorisierten Fehler von DotNetOpenAuth zurückgeben kann. Es wurde schnell klar, dass dies auf ein Signaturproblem zurückzuführen war, und nachdem wir uns die Quelle angesehen hatten, konnten wir feststellen, dass das abgerufene AccessToken-Geheimnis nicht mit der authentifizierten Profil-Informationsanforderung verwendet wurde.

Es ist sinnvoll, da die OAuthClient-Klasse das abgerufene Zugriffstokengeheimnis nicht enthält, weil es normalerweise nicht für Authentifizierungszwecke benötigt wird. Dies ist der Hauptzweck der OAuth-Bibliothek von ASP.NET.

Wir mussten authentifizierte Anfragen gegen die API machen, nachdem der Benutzer sich angemeldet hat, um einige Standardprofilinformationen, einschließlich E-Mail-Adresse und vollständiger Name, abzurufen. Wir konnten dieses Problem lösen, indem wir vorübergehend einen InMemoryOAuthTokenManager verwenden.

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

Hier ist der Abschnitt, der von dem von Microsoft geschriebenen Basis-LinkedIn-Client geändert wurde.

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); 

Leider ist die IOAuthTOkenManger.ReplaceRequestTokenWithAccessToken(..) Methode erhält erst nach der VerifyAuthentication() Methode zurückzugeführt, so dass wir, anstatt einen neuen TokenManager erstellen müssen und und ein WebConsumer und HttpWebRequest die AccessToken Anmeldeinformationen erstellen verwenden wir nur abgerufen werden.

Dies löst unser einfaches 401 Unauthorized-Problem.

Was passiert nun, wenn Sie die AccessToken-Anmeldeinformationen nach dem Authentifizierungsprozess beibehalten möchten? Dies könnte beispielsweise für einen DropBox-Client nützlich sein, bei dem Sie Dateien asynchron mit der DropBox eines Benutzers synchronisieren möchten. Das Problem geht zurück auf die Art und Weise, wie die AspNet-Bibliothek geschrieben wurde. Es wurde angenommen, dass DotNetOpenAuth nur für die Benutzerauthentifizierung und nicht als Grundlage für weitere OAuth-API-Aufrufe verwendet wird. Zum Glück war die Lösung ziemlich einfach, ich musste lediglich die Basis ändern AuthetnicationOnlyCookieOAuthTokenManger, so dass die Methode ReplaceRequestTokenWithAccessToken(..) den neuen AccessToken-Schlüssel und die geheimen Schlüssel speicherte.

/// <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); 

    } 

} 

Dann ist dieses PersistentCookieOAuthTokenManager alles, was Sie tun müssen, verwenden, um sich Ihre DropboxClient Konstruktor oder andere Client ändern, wo Sie die AccessToken Geheimnis

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

Ich löste dies, indem ich nicht das in ASP.NET eingebaute Zeug benutzte und auf DNOA zurückging, aber ich mag diesen Ansatz auch. –

0

Der Grund, dass OAuthClient Klasse keinen Zugriff Token-Geheimnis nicht enthalten ist, dass es normalerweise nicht für die Authentifizierung Zweck benötigt werden, die der primäre Zweck des ASP.NET OAuth ist Bibliothek.

Wenn Sie das geheime Zugriffstoken in Ihrem Fall abrufen möchten, können Sie die VerifyAuthentication() - Methode anstelle von VerifyAuthenticationCore() wie oben beschrieben außer Kraft setzen. Innerhalb von VerifyAuthentication() können Sie WebWorker.ProcessUserAuthorization() aufrufen, um die Anmeldung zu validieren, und von dem zurückgegebenen AuthorizedTokenResponse-Objekt haben Sie Zugriff auf den Token-Secret.

+0

Die VerifyAuthenticationCore-Methode verfügt jedoch über einen AuthorizedTokenResponse-Parameter, der dieselben Daten enthalten sollte. –

+0

Entschuldigung, ich war abgelenkt und habe meinen Kommentar nicht bearbeitet. Bei der Ableitung von OAuthClient ist VerifyAuthenticationCore eine abstrakte Methode, daher muss ich sie implementieren. Zugegeben, ich kann einfach VerifyAuthentication aufrufen und den HttpContext übergeben, aber das scheint Redundanz zu sein. VerifyAuthenticationCore nimmt auch eine AuthorizedTokenResponse, also sollte das nicht haben, was ich brauche? In der Tat habe ich festgestellt, dass das Geheimnis auf der AuthorizedTokenResponse ist, aber es ist intern geschützt.Gibt es einen anderen Weg, auf den ich zugreifen soll? –

0

Nachdem einige graben tun, konnte ich diese meine Konstruktor Logik durch Änderung wie folgt lösen:

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

Graben durch die Quelle zeigt DNOA wird, dass, wenn Sie ein OAuthClient konstruieren (Meine Basisklasse) nur mit dem Consumer Key und Secret, es verwendet den InMemoryOAuthTokenManager anstelle des SimpleConsumerTokenManager. Ich weiß nicht warum, aber jetzt ist mein Zugriffs-Token-Geheimnis ordnungsgemäß an meine Signatur in der autorisierten Anfrage angehängt und alles funktioniert. Hoffentlich hilft das jemand anderem. In der Zwischenzeit werde ich das wahrscheinlich für einen Blogpost aufräumen, da es null Anleitung im Netz gibt (die ich finden kann), um dies zu tun.

EDIT: Ich werde meine Antwort rückgängig zu machen, da, wie ein Kollege darauf hingewiesen, diese Versorgung eines Antrags, aber jetzt, dass ich die In-Memory-Manager bin mit, das bündig einmal werde ich Reise abrunden vollständig zurück zum Browser (ich nehme an). Also denke ich, dass das Grundproblem hier ist, dass ich das Zugriffstoken geheim halten muss, was ich noch nicht gesehen habe.

0

Was Ihre ursprüngliche Frage beharren möchte, dass die "secret" wird nicht zur Verfügung gestellt - das Geheimnis liegt genau dann vor, wenn Sie die Antwort in der Funktion verifyAuthenticationCore erhalten. Sie erhalten beide wie folgt: