asp.net mvc 4 - example - Cliente personalizado de OAuth en MVC4/DotNetOpenAuth: secreto de token de acceso que falta
mvc 5 login example in c# (4)
Después de hacer algunas excavaciones, pude resolver esto cambiando mi lógica de 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))
{
}
se convierte
public DropboxClient(string consumerKey, string consumerSecret) :
base("dropbox", DropboxServiceDescription, consumerKey, consumerSecret)
{
}
Explorar a través de la fuente DNOA muestra que si construye un OAuthClient (mi clase base) solo con 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, probablemente limpiaré esto para una publicación de blog ya que no hay orientación en la red (que pueda encontrar) para hacer esto.
EDITAR: 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 descargará una vez que viaje por completo de vuelta a la 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.
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?
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));
}
}
}
}
}
El motivo por el que la clase OAuthClient no incluye el token de acceso secreto es que normalmente no es necesario para fines de autenticación, que es el objetivo principal de la biblioteca ASP.NET OAuth.
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.
En cuanto a su pregunta original, el secreto no se proporciona en respuesta: el secreto está ahí cuando obtiene la respuesta en la función verifyAuthenticationCore. Los tienes a ambos de esta manera:
string token = response.AccessToken; ;
string secret = (response as ITokenSecretContainingMessage).TokenSecret;
Encontré tu pregunta cuando estaba buscando una solución a un problema similar. Lo resolví haciendo 2 nuevas clases, sobre las cuales puedes leer en esta publicación .
También copiaré y pegaré la publicación completa aquí:
DotNetOpenAuth.AspNet 401 Error no autorizado y corrección secreta de tokens de acceso persistente
Al diseñar QuietThyme, nuestro Cloud Ebook Manager, sabíamos que todos odiamos la creación de nuevas cuentas tanto como lo hacemos. Comenzamos a buscar las bibliotecas OAuth y OpenId que pudiéramos aprovechar para permitir el inicio de sesión social. Terminamos utilizando 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 la mayor parte funcionara (descrito en una publicación previa de coderwall ). 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.
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 necesaria para propósitos de autenticación, que es el propósito principal de la biblioteca ASP.NET OAuth.
Necesitábamos 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);
Desafortunadamente, el IOAuthTOkenManger.ReplaceRequestTokenWithAccessToken(..)
no se ejecuta hasta después de que el método VerifyAuthentication()
retorna, por lo que tenemos que crear un nuevo TokenManager y crear un WebConsumer
y HttpWebRequest
usando las credenciales de AccessToken que acabamos de recuperar.
Esto resuelve nuestro simple problema 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 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);
}
}
Entonces, para usar este PersistentCookieOAuthTokenManager
todo lo que necesita hacer es modificar su constructor DropboxClient, o cualquier otro cliente donde desee mantener el AccessToken Secret
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))
{}