2012-09-18 15 views
5

Attualmente sto lavorando all'implementazione di un client OAuth Dropbox per la mia applicazione. È stato un processo abbastanza indolore fino a quando non ho raggiunto la fine. Una volta autorizzato, quando tento di accedere ai dati dell'utente ottengo un 401 indietro da Dropbox sul fatto che il token non è valido. Ho chiesto sui forum Dropbox e sembra che alla mia richiesta manchi l'access_token_secret restituito da Dropbox. Sono stato in grado di usare Fiddler per scavare il segreto e aggiungerlo all'URL della mia richiesta e ha funzionato bene, quindi questo è sicuramente il problema. Quindi perché DotNetOpenAuth non restituisce il token secret di accesso quando restituisce il token di accesso?Client OAuth personalizzato in MVC4/DotNetOpenAuth - segreto di accesso ai token mancanti

Per riferimento, il mio codice:

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

So che c'è un modo più bello per formattare il codice, ma non riesco a trovarlo per tutta la vita. Fare clic sul pulsante del codice nella domanda non sembra funzionare. Se qualcuno vuole consigliarti su come sistemarlo, è molto apprezzato. –

+2

La formattazione del codice è ora basata su tag e non hai tag specifici della lingua nel tuo post, quindi non ha fatto nulla. Ho aggiunto sopra il tuo codice per forzarlo a evidenziarlo. Vedi http://meta.stackexchange.com/a/128910/190311 –

risposta

5

Ho trovato la tua domanda quando cercavo la soluzione per un problema simile. Ho risolto il problema creando 2 nuove classi, che puoi leggere in questo coderwall post.

sarò anche copiare e incollare l'articolo completo qui:


DotNetOpenAuth.AspNet 401 errore non autorizzato e persistente token di accesso segreto Fix

Nel progettare QuietThyme, il nostro manager nuvola Ebook, sapevamo che tutti odiano creare nuovi account tanto quanto noi. Abbiamo iniziato a cercare le librerie OAuth e OpenId che potevamo sfruttare per consentire l'accesso social. Abbiamo finito per utilizzare la libreria DotNetOpenAuth.AspNet per l'autenticazione utente, perché supporta Microsoft, Twitter, Facebook, LinkedIn e Yahoo e molti altri proprio fuori dal campo. Mentre abbiamo riscontrato alcuni problemi nell'impostare tutto, alla fine abbiamo solo dovuto fare alcune piccole personalizzazioni per far funzionare la maggior parte di esso (descritto in un previous coderwall post).Abbiamo notato che, a differenza di tutti gli altri, il client di LinkedIn non si autenticava, restituendo un errore non autorizzato 401 da DotNetOpenAuth. È diventato subito evidente che ciò era dovuto a un problema di firma e, dopo aver esaminato la fonte, siamo stati in grado di determinare che il segreto AccessToken recuperato non viene utilizzato con la richiesta di informazioni sul profilo autenticato.

Rende acutally senso, la ragione che la classe OAuthClient non include l'accesso recuperata token segreto è che è normalmente non necessaria per l'autenticazione, che è lo scopo principale della biblioteca ASP.NET OAuth.

Avevamo bisogno di fare richieste autenticate contro l'API, dopo che l'utente ha effettuato l'accesso, per recuperare alcune informazioni del profilo standard, inclusi indirizzo e-mail e nome completo. Siamo stati in grado di risolvere questo problema utilizzando temporaneamente 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); 
     } 
    } 
} 

Ecco la sezione che è stata modificata dal client di base di LinkedIn scritto da Microsoft.

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

Purtroppo, il metodo IOAuthTOkenManger.ReplaceRequestTokenWithAccessToken(..) non viene eseguito fino a quando il metodo VerifyAuthentication() ritorni, così abbiamo invece necessario creare una nuova TokenManager ed e creare un WebConsumer e HttpWebRequest utilizzando le credenziali access token che abbiamo appena recuperati.

Questo risolve il nostro semplice numero 401 non autorizzato.

Ora cosa succede se si desidera mantenere le credenziali di AccessToken dopo il processo di autenticazione? Ciò potrebbe essere utile per un client DropBox, ad esempio, in cui si desidera sincronizzare i file con DropBox di un utente in modo asincrono. Il problema risale al modo in cui è stata scritta la libreria AspNet, si è ipotizzato che DotNetOpenAuth sarebbe stato utilizzato solo per l'autenticazione dell'utente, non come base per le chiamate api OAuth successive. Per fortuna la correzione era abbastanza semplice, tutto quello che dovevo fare era modificare la base AuthetnicationOnlyCookieOAuthTokenManger in modo che il metodo ReplaceRequestTokenWithAccessToken(..) memorizzasse la nuova chiave e i segreti di 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); 

    } 

} 

quindi di utilizzare questo PersistentCookieOAuthTokenManager tutto quello che dovete fare è modificare il vostro costruttore DropboxClient, o qualsiasi altro client in cui si desidera a persistere l'access token segreto

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

Alla fine ho risolto questo problema non utilizzando le cose incorporate in ASP.NET e tornando al DNOA diretto, ma mi piace anche questo approccio. –

0

La ragione per cui classe OAuthClient non include token di accesso segreto è che è normalmente non serve a scopo di autenticazione, che è lo scopo primario del ASP.NET OAuth biblioteca.

Detto questo, se si desidera recuperare il segreto del token di accesso nel proprio caso, è possibile sovrascrivere il metodo VerifyAuthentication(), anziché VerifyAuthenticationCore() come si fa sopra. All'interno di VerifyAuthentication(), è possibile chiamare WebWorker.ProcessUserAuthorization() per convalidare il login e dall'oggetto AuthorizedTokenResponse restituito, si ha accesso al segreto del token.

+0

Ma il metodo VerifyAuthenticationCore ha un parametro AuthorizedTokenResponse che dovrebbe contenere gli stessi dati. –

+0

Siamo spiacenti, mi sono distratto e non ho terminato la modifica del mio commento. Quando deriva da OAuthClient, VerifyAuthenticationCore è un metodo astratto, quindi devo implementarlo. Certo, posso solo chiamare VerifyAuthentication e passarlo HttpContext, ma sembra una ridondanza. Inoltre, VerifyAuthenticationCore accetta un AuthorizedTokenResponse, quindi non dovrebbe avere quello che mi serve? In effetti, ho notato che il segreto è su AuthorizedTokenResponse, ma è protetto internamente.C'è un altro modo in cui dovrei accedervi? –

0

Dopo aver fatto qualche scavo, sono stato in grado di risolvere questo cambiando la mia logica costruttore come segue:

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

diventa

public DropboxClient(string consumerKey, string consumerSecret) : 
     base("dropbox", DropboxServiceDescription, consumerKey, consumerSecret) 
    { 
    } 

Scavando attraverso la sorgente DNOA dimostra che se si costruisce un OAuthClient (la mia classe base) con solo la chiave del consumatore e il segreto, utilizza InMemoryOAuthTokenManager invece di SimpleConsumerTokenManager. Non so perché, ma ora il mio accesso token secret è correttamente aggiunto alla mia firma nella richiesta autorizzata e tutto funziona. Spero che questo aiuti qualcun altro. Nel frattempo, probabilmente lo ripulirò per un post sul blog, dato che ci sono zero indicazioni sulla rete (che riesco a trovare) per farlo.

EDIT: Ho intenzione di annullare la mia risposta poiché, come ha sottolineato un collega, questo si occuperà di una richiesta, ma ora che sto usando il gestore in memoria, verrà eseguito lo spurgo una volta il round trip completamente indietro al browser (sto assumendo). Quindi penso che il problema principale qui è che ho bisogno di ottenere il segreto del token di accesso, che non ho ancora visto come fare.

0

Per quanto riguarda la tua domanda originale che il il segreto non viene fornito in risposta - il segreto è proprio lì quando ottieni la risposta nella funzione verifyAuthenticationCore. Otterrai entrambi in questo modo:

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