2012-09-18 31 views
5

Actualmente estoy trabajando en la implementación de un cliente Dropbox OAuth para mi aplicación. Ha sido un proceso bastante sencillo hasta que toco el final. Una vez que he autorizado, cuando intento acceder a los datos del usuario obtengo un 401 de Dropbox sobre el token que no es válido. Pedí en los foros de Dropbox y parece que mi solicitud no incluye el access_token_secret que devuelve Dropbox. Pude usar Fiddler para desenterrar el secreto y agregarlo a mi URL de solicitud y funcionó bien, así que ese es definitivamente el problema. Entonces, ¿por qué DotNetOpenAuth no devuelve el secreto del token de acceso cuando devuelve el token de acceso?Cliente OAuth personalizado en MVC4/DotNetOpenAuth - falta token secreto de acceso

Como referencia, mi código:

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

Sé que hay una manera más agradable para dar formato al código, pero no puedo por la vida de mí encontrar. Al hacer clic en el botón de código en la pregunta no pareció funcionar. Si alguien quiere aconsejar sobre cómo solucionarlo, muy apreciado. –

+2

El formato de código ahora está basado en etiquetas, y no tenía ninguna etiqueta específica de idioma en su publicación, por lo que no hizo nada. Añadí arriba de tu código para forzarlo a resaltarlo. Consulte http://meta.stackexchange.com/a/128910/190311 –

Respuesta

5

Encontré su pregunta cuando estaba buscando una solución a un problema similar. Lo resolví haciendo 2 nuevas clases, que puedes leer en este coderwall post.

También voy a copiar y pegar el post completo aquí:


DotNetOpenAuth.AspNet 401 Error no autorizado y persistente Token de acceso secreto fijo las

Al diseñar QuietThyme, nuestro Gerente de la nube de libros electrónicos, sabíamos que todo el mundo odia la creación de nuevas cuentas tanto como nosotros. Comenzamos a buscar las bibliotecas OAuth y OpenId que pudiéramos aprovechar para permitir el inicio de sesión social. Terminamos usando la biblioteca DotNetOpenAuth.AspNet para la autenticación de usuarios, porque es compatible con Microsoft, Twitter, Facebook, LinkedIn y Yahoo, y muchos otros desde el principio. Si bien tuvimos algunos problemas al configurarlo todo, al final solo necesitábamos hacer algunas personalizaciones pequeñas para que funcionase en su mayor parte (descrito en un previous coderwall post).Notamos que, a diferencia de todos los demás, el cliente de LinkedIn no se autenticaba y devolvía un error 401 no autorizado de DotNetOpenAuth. Rápidamente se hizo evidente que esto se debía a un problema de firma y, después de consultar la fuente, pudimos determinar que el secreto de AccessToken recuperado no se está utilizando con la solicitud de información de perfil autenticada.

Realmente tiene sentido, la razón por la cual la clase OAuthClient no incluye el secreto del token de acceso recuperado es que normalmente no es necesario para fines de autenticación, que es el propósito principal de la biblioteca ASP.NET OAuth.

Necesitamos realizar solicitudes autenticadas contra la API, después de que el usuario haya iniciado sesión, para recuperar cierta información de perfil estándar, incluida la dirección de correo electrónico y el nombre completo. Pudimos resolver este problema haciendo uso de un InMemoryOAuthTokenManager de forma temporal.

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

Aquí está la sección que ha cambiado desde el cliente base de LinkedIn escrito por 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); 

Por desgracia, el método IOAuthTOkenManger.ReplaceRequestTokenWithAccessToken(..) no se ejecuta hasta después de las VerifyAuthentication() devuelve el método, así que en vez tenga que crear una nueva y TokenManager y crear un WebConsumer y HttpWebRequest utilizando las credenciales accessToken que acabamos de recuperar.

Esto resuelve nuestro problema simple 401 no autorizado.

Ahora, ¿qué ocurre si desea conservar las credenciales de AccessToken después del proceso de autenticación? Esto podría ser útil para un cliente DropBox, por ejemplo, donde le gustaría sincronizar archivos a DropBox de un usuario de forma asíncrona. El problema se remonta a la forma en que se escribió la biblioteca AspNet, se asumió que DotNetOpenAuth solo se usaría para la autenticación automática del usuario, no como base para otras llamadas api de OAuth. Afortunadamente la solución fue bastante simple, todo lo que tuve que hacer fue modificar la base AuthetnicationOnlyCookieOAuthTokenManger para que el método ReplaceRequestTokenWithAccessToken(..) almacenara la nueva clave y secretos de 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); 

    } 

} 

Luego de usar este PersistentCookieOAuthTokenManager todo lo que tiene que hacer es modificar el constructor DropboxClient, o cualquier otro cliente en la que desea que persista la accessToken Secreto

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

Terminé resolviendo esto al no usar las cosas incorporadas en ASP.NET y volver a DNOA, pero también me gusta este enfoque. –

0

La razón de que la clase OAuthClient no incluye el acceso secreta del token es que está normalmente no requiera la finalidad de autenticación, que es el propósito principal de la ASP.NET OAuth biblioteca.

Dicho esto, si desea recuperar el token secreto de acceso en su caso, puede anular el método VerifyAuthentication(), en lugar de VerifyAuthenticationCore() como lo hace anteriormente. Dentro de VerifyAuthentication(), puede llamar a WebWorker.ProcessUserAuthorization() para validar el inicio de sesión y, desde el objeto AuthorizedTokenResponse devuelto, tiene acceso al token secret.

+0

Pero el método VerifyAuthenticationCore tiene un parámetro AuthorizedTokenResponse que debe contener los mismos datos. –

+0

Lo siento, me he distraído y no terminé mi comentario. Cuando se deriva de OAuthClient, VerifyAuthenticationCore es un método abstracto, así que tengo que implementarlo. De acuerdo, puedo llamar a VerifyAuthentication y pasarle el HttpContext, pero eso parece una redundancia. Además, VerifyAuthenticationCore toma una AuthorizedTokenResponse, ¿no debería tener lo que necesito? De hecho, noté que el secreto está en AuthorizedTokenResponse, pero está protegido interno.¿Hay alguna otra forma en que se supone que debo acceder a ella? –

0

Después de hacer algo de investigación, yo era capaz de resolver esto cambiando mi lógica constructor de la siguiente manera:

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

convierte

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

de excavación a través de la fuente de DNOA muestra que si se construye un OAuthClient (mi clase base) con solo la clave del consumidor y el secreto, usa InMemoryOAuthTokenManager en lugar de SimpleConsumerTokenManager. No sé por qué, pero ahora mi secreto de token de acceso se adjunta correctamente a mi firma en la solicitud autorizada y todo funciona. Espero que esto ayude a alguien más. Mientras tanto, es probable que lo limpie para una publicación de blog ya que hay cero orientación en la red (que puedo encontrar) para hacer esto.

EDIT: Voy a deshacer mi respuesta ya que, como señaló un colega, esto se encargará de una solicitud, pero ahora que estoy usando el administrador en memoria, se vaciará una vez que viaje de ida y vuelta completamente de vuelta al navegador (estoy asumiendo). Así que creo que el problema principal aquí es que necesito obtener el token de acceso secreto, que todavía no he visto cómo hacerlo.

0

cuanto a su pregunta original que el el secreto no se proporciona en respuesta: el secreto está ahí cuando obtienes la respuesta en la función verifyAuthenticationCore. Se obtiene tanto de ellos como esto:

string token = response.AccessToken; ; 
    string secret = (response as ITokenSecretContainingMessage).TokenSecret; 
Cuestiones relacionadas