Autenticación OAuth

Es posible que muchos de vosotros hayáis oído la noticia de que varias de las grandes empresas ya soportan autenticación en sus APIs a través de OAuth. ¿Qué es OAuth y por qué ha venido para quedarse?

OAuth es un protocolo abierto que permite autenticarse a través de las API de distintas compañías como por ejemplo Twitter, Yahoo, Google, etcétera. Hasta hace bien poco uno de los métodos más usados para la autenticación en las mismas era a través de Autenticación Básica con todo lo que ello suponía: el envío de nuestras credenciales con cada petición fácilmente descifrable. Twitter dejará de dar la posibilidad de este tipo de autenticación en el próximo mes.

¿Qué ofrece OAuth?

El objetivo de este método de autenticación es poder acceder a recursos protegidos sin la necesidad de enviar nuestro usuario y contraseña para solicitar los mismos. ¿Cómo es posible? A través de nuestra aplicación será necesario generar una firma que nos identifique gracias a un conjunto de tokens facilitados por el servicio. En este post voy a centrarme en las dos plataformas con las que he estado trabajando estos días: Twitter y Yahoo.

Workflow de OAuth

En esta imagen podemos ver cuáles son los pasos para poder solicitar los recursos protegidos de un servicio. El objetivo fundamental es conseguir un Access token  y un Access Token secret. Con ellos seremos capaces de generar la firma necesaria para obtener los recursos protegidos de nuestros usuarios sin necesidad de enviar sus credenciales en cada petición. Los pasos que debemos seguir son los siguientes:

  1. Creación de una aplicación y obtención de una Consumer key y Consumer Secret.
  2. Solicitar un token temporal.
  3. Autorización del usuario.
  4. Intercambio del token temporal y la autorización del usuario por un Access Token y Access Secret Token.

Creación de una aplicación y obtención de una Consumer key y Consumer secret

Lo primero que necesitamos para comenzar es una Consumer key y Consumer Secret. Ambos se consiguen cuando creamos “simbólicamente” una aplicación en cualquiera de las plataformas de la cual queremos hacer uso de sus servicios. En el caso de Twitter, para crear la aplicación, podemos acceder a la siguiente dirección. Por otro lado, en Yahoo podemos realizar la misma operación a través de este otro enlace. Obviamente, es necesario tener una cuenta para poder darlas de alta. Una vez creada, en ambos casos podemos ver en los detalles la Consumer Key y Consumer Secret generadas:

Solicitar un token temporal

Llegados a este punto es donde comenzamos con la generación de código. Lo primero que necesitamos antes de pedir la autorización del usuario, para recuperar sus datos privados desde nuestra aplicación, es solicitar un token temporal para poder iniciar un proceso de autorización desde la plataforma en cuestión. Para solicitar un token al servicio serán necesarios los siguientes parámetros:

  • oauth_consumer_key: Se obtuvo cuando dimos de alta la aplicación.
  • oauth_signature_method: Se trata del método que utilizaremos para generar la fima. En este post usaré HMAC-SHA1 ya que Twitter solamente soporta este método hasta la fecha, pero también es posible usar PLAINTEXT y RSA-SHA1. Yahoo soporta tanto HMAC-SHA1 como PLAINTEXT.
  • oauth_signature: La firma generada por nuestra aplicación.
  • oauth_nonce: Se trata de un string aleatorio que acompaña a un timestamp.
  • oauth_timestamp: El timestamp está comprendido entre el 1 de Enero de 1970 y la fecha actual y se utiliza de manera conjunta por el servidor con oauth_nonce para verificar que la petición nunca se ha realizado antes y así prevenir ataques.
  • oauth_version: En la actualidad se está utilizando la versión 1.0 de OAuth. Este parámetro es opcional.
  • oauth_callback: Es importante mencionar que es posible utilizar OAuth tanto con aplicaciones de escritorio como aplicaciones web. Es por ello que este parámetro es importante para que el servicio se redirija a esta dirección después de que se autorice el acceso. En el caso de Twitter se puede omitir si la aplicación es de escritorio pero no ocurre lo mismo para el caso de Yahoo, donde debemos especificar este parámetro con el valor oob (Out of bounds).

Estos parámetros serán parte de la cabecera de la petición dentro del apartado Authorization:

private NameValueCollection GetOAuthParameters(string httpMethod, string url, string requestToken = null, string tokenSecret = null, string verifier = null, string callback = null)
{
        var oAuthParameters = new NameValueCollection
                                    {
                                       {"oauth_timestamp",GetTimeStamp()},
                                       {"oauth_nonce",GetNonce()},
                                       {"oauth_version", "1.0"},
                                       {"oauth_signature_method", "HMAC-SHA1"},
                                       {"oauth_consumer_key", _consumerKey},
                                    };

        if (!String.IsNullOrEmpty(requestToken)) oAuthParameters.Add("oauth_token", requestToken);

        if (!String.IsNullOrEmpty(verifier)) oAuthParameters.Add("oauth_verifier", verifier);

        if (!String.IsNullOrEmpty(callback)) oAuthParameters.Add("oauth_callback", callback);
        var signatureBase = GetSignatureBase(httpMethod, NormalizeUrl(url), oAuthParameters);
        var signature = GetSignature(_consumerSecret, signatureBase, tokenSecret);
        oAuthParameters.Add("oauth_signature", signature);

        Debug.WriteLine("oauth_signature {0}", signature);

        return oAuthParameters;
} 

Generar nonce

Se trata de un valor usado sólo una vez y simplemente se nos pide la generación de un string diferente para cada petición en las especificaciones oficiales de OAuth. Una implementación válida podría ser la siguiente:

private static string GetNonce()
{
    return new Random().Next(Int16.MinValue, Int16.MaxValue).ToString("X");
}

Generar timestamp

Como se explicó en la enumeración de los parámetros necesarios, el timestamp debe estar comprendido entre el 1 de Enero de 1970 y la fecha actual.

private static string GetTimeStamp()
{
    return ((long)(DateTime.UtcNow - (new DateTime(1970, 1, 1))).TotalSeconds).ToString();
}

Normalizar la url

Si la dirección a la cual debemos realizar la petición utiliza un puerto distinto al usado por defecto en su protocolo, deberemos asignar el puerto en cuestión a la dirección:

public static string NormalizeUrl(string url)
{
    var uri = new Uri(url);
    var port = string.Empty;

    if (uri.Scheme == "http" && uri.Port != 80 ||
        uri.Scheme == "https" && uri.Port != 443 ||
        uri.Scheme == "ftp" && uri.Port != 20)
        port = string.Format(":{0}", uri.Port);

    return string.Format("{0}://{1}{2}{3}", uri.Scheme, uri.Host, port, uri.AbsolutePath);
}

Creación de la firma

La firma es quizás la parte más compleja de la aplicación ya que está compuesta de una serie de pasos y si cometemos el error en alguno de ellos, el servidor rechazará la petición sin dar más explicación que un signature_invalid.

Firma base

La firma base, tal y como nos indican en las especificaciones, se trata de la concatenación de el http method utilizado, la url a la que hacemos la petición y el resto de parámetros concatenados y ordenados por nombre y valor.

private static string GetSignatureBase(string httpMethod, string url, NameValueCollection oAuthParameters)
{
    var parameters = new Dictionary<string, string>();
    foreach (var key in oAuthParameters.AllKeys)
        parameters.Add(key, oAuthParameters[key]);

    var normalizedParameters = NormalizeParameters(parameters);

    return string.Format("{0}&{1}&{2}", httpMethod, Uri.EscapeDataString(url), Uri.EscapeDataString(normalizedParameters));
}

Firma encriptada y final

Una vez que tenemos los métodos para concatenar y normalizar los parámetros según lo establecido por OAuth, debemos realizar un último paso para obtener la firma que finalmente pasaremos al servicio. En la misma, debemos concatenar primeramente el token  Consumer Secret  y Token Secret si es que lo hubiera con un ampersand(&) entre ellos. Si utilizáramos PLAINTEXT deberíamos utilizar su código correspondiente en Unicode: %26. Si no existiera el segundo integrante, Token Secret, debemos indicar igualmente el ampersand dentro de la cadena.

Por último es necesario hacer uso del algoritmo de hash HMAC-SHA1, generado a partir del Consumer Secret y Token Secret concatenado anteriormente, para cifrar la firma base creada en el apartado anterior. Un método para llevar a cabo estos pasos podría ser el siguiente:

private static string GetSignature(string consumerSecret, string signatureBase, string tokenSecret = null)
{
    var hmacsha1 = new HMACSHA1(Encoding.UTF8.GetBytes(string.Concat(consumerSecret, "&", tokenSecret)));

    var data = Encoding.ASCII.GetBytes(signatureBase);
    var hashData = hmacsha1.ComputeHash(data);

    return Uri.EscapeDataString(Convert.ToBase64String(hashData));
}

Con este conjunto de métodos para crear un nonce, timestamp y la firma podemos realizar básicamente todas las llamadas que nos son necesarias para establecer ese primer contacto, desde que creamos una aplicación hasta que un usuario accede a establecer conexión con sus datos privados a través de la API usando nuestro cliente.

Llamada al servicio para la obtención del token temporal

Una vez lista la cabecera de nuestra petición, ya podemos realizar la llamada para recuperar el token temporal y un token secret que nos serán útiles para el último paso. Dependiendo del servicio con el que queramos contactar, realizaremos la llamada a una dirección u otra facilitadas por la plataforma:

  • Twitter: http://twitter.com/oauth/request_token
  • Yahoo: https://api.login.yahoo.com/oauth/v2/get_request_token.
public Token GetRequestToken(string url)
{
    var parameters = GetOAuthParameters(HttpMethod.Get, url, callback: "oob");

    var oAuthRequest = new OAuthRequest(parameters, url, HttpMethod.Get, proxy: _proxy);

    return RetrieveToken(oAuthRequest.GetResponse());
}

Para realizar las peticiones podemos utilizar bien WebClient o HttpWebRequest.


using System.Text;
using System.Collections.Specialized;
using System.Net;
using System.IO;
using System.Diagnostics;

namespace OAuthTools
{
    public class OAuthRequest
    {
        private readonly HttpWebRequest _request;

        public string GetResponse()
        {
            try
            {
                Debug.WriteLine("{0}: {1}", _request.Method, _request.RequestUri);

                using (var response = _request.GetResponse())
                {
                    var stream = response.GetResponseStream();
                    using (var reader = new StreamReader(stream))
                    {
                        return reader.ReadToEnd();
                    }
                }
            }
            catch (WebException ex)
            {
                if (ex.Response is HttpWebResponse)
                {
                    using (var reader = new StreamReader(ex.Response.GetResponseStream()))
                    {
                        var result = reader.ReadToEnd();
                        return result;
                    }
                }

                throw;
            }
        }

        public OAuthRequest(NameValueCollection parameters, string url, string method, string realm = null, WebProxy proxy = null)
        {
            ServicePointManager.Expect100Continue = false;
            ServicePointManager.UseNagleAlgorithm = false;

            var header = new StringBuilder();

            header.Append("OAuth ");

            if (!string.IsNullOrEmpty(realm)) header.Append("realm="" + realm + "" ");

            for (var i = 0; i < parameters.Count; i++)
            {
                var key = parameters.GetKey(i);
                header.Append(string.Format("{0}="{1}"", key, parameters[key]));

                if (i < parameters.Count - 1)
                    header.Append(",");
            }

            _request = CreateRequestByProxy(url, method, proxy);

            _request.Headers["Authorization"] = header.ToString();

        }

        private static HttpWebRequest CreateRequestByProxy(string url, string method, WebProxy proxy = null)
        {
            var httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
            httpWebRequest.Method = method.ToUpper();

            if (proxy != null) httpWebRequest.Proxy = proxy;

            return httpWebRequest;
        }
    }
}

Si nos fijamos en el constructor de la clase, lo primero que hacemos es deshabilitar la opción Expect 100 Continue. El propósito de esta propiedad es la posibilidad de que la petición pudiera enviar en primer lugar sólo la cabecera con la autorización, para verificar si efectivamente tenemos permiso de realizar la misma. Si la autenticación fallara devolviendo un 417 nos ahorraríamos el envío de los datos y finalizaría la comunicación. Si por otro lado el servidor devolviera el estado 100 (Continue) el cliente, es decir nuestra aplicación .NET, enviaría el resto del mensaje. Al menos Twitter no soporta este comportamiento y debe ser desactivado para evitar errores imprevistos. La segunda propiedad deshabilitada se utiliza para reducir el tráfico y debe ser anulada también por temas de compatibilidad.
Por otro lado, Realm es el nombre del dominio al que va dirigido aunque no es obligatorio incluirlo.

Una vez realizada la llamada con éxito, recuperamos un conjunto de valores donde podemos encontrar el token temporal necesario para el siguiente paso y el token secret para completar el último. Para recuperarlos, he creado un pequeño método llamado RetrieveToken donde almacenaremos en una clase los valores necesarios de la respuesta:

private static Token RetrieveToken(string result)
{
 var values = HttpUtility.ParseQueryString(result);

 var tokens = new Token { Value = values.Get("oauth_token"), SecretToken = values.Get("oauth_token_secret") };

 return tokens;
}

Autorización del usuario

Una vez que ya hemos obtenido el token temporal, ya podemos solicitar autorización al usuario para poder acceder a sus recursos protegidos. Dependiendo del servicio que estemos utilizando, debemos dirigirnos a su página de autorización con el token temporal como query string. Ejemplos de estas páginas son:

  • Twitter:http://twitter.com/oauth/authorize?oauth_token=
  • Yahoo:https://api.login.yahoo.com/oauth/v2/request_auth?oauth_token=
Process.Start(string.Format(url + "{0}", tokens.Value));

El resultado de esta llamada podría ser parecido a lo siguiente:

Si el usuario acepta, en el caso de Twitter se nos facilitará un número y en Yahoo una cadena de caracteres aleatorios. En cualquiera de los dos casos, este nuevo dato se conoce como oauth_verifier y nos servirá junto con el token temporal y el token secret para finalizar el proceso.

Intercambio del token temporal y la autorización del usuario por un Access Token

 

Ya estamos en la recta final y solamente debemos realizar una última petición. Ya tenemos todos los ingredientes para conseguir el Access Token e incluso todas las recetas (métodos) para llevar a cabo dicha petición, por lo que solamente necesitamos pasar los valores a nuestro código. En el caso de Twitter y Yahoo las direcciones a las cuales debemos realizar la petición son las siguientes:

  • Twitter: http://twitter.com/oauth/access_token
  • Yahoo:https://api.login.yahoo.com/oauth/v2/get_token

Es recomendable realizar la misma a través del método POST. Por otro lado, se adjuntan nuevos parámetros que son: token secret, devuelto en la petición junto con el token temporal, oauth_verifier, el cual se nos facilitó cuando dimos permiso a nuestra aplicación en el paso anterior, y el token temporal en sí que será necesario adjuntarlo en esta ocasión en la cabecera junto con los demás parámetros.

public Token GetAccessToken(string url, string requestToken, string requestTokenSecret, string verifier)
{
 var parameters = GetOAuthParameters(HttpMethod.Post, url, requestToken: requestToken, tokenSecret: requestTokenSecret, verifier: verifier);

 var oAuthRequest = new OAuthRequest(parameters, url, HttpMethod.Post, proxy: _proxy);

 return RetrieveToken(oAuthRequest.GetResponse());
}

Como podemos ver, los métodos utilizados son los mismos que para solicitar un token temporal, añadiendo los valores que hasta ahora eran parámetros opcionales utilizando el Framework 4.0 de .NET. Una vez que se realice la petición obtendremos el access token y access token secret con el que podremos generar nuestras claves.

Para finalizar, me gustaría comentar que, debido al tiempo que he empleado en realizar este estudio y análisis de cada una de las partes y de lo mucho que me tuve que pegar con ello, he subido una pequeña aplicación a Codeplex que servirá como utilidad y aprendizaje sobre el funcionamiento de OAuth con un ejemplo completo de todo este proceso. Aún estoy realizando cambios para intentar mejorarla pero podéis acceder a la misma a través de esta dirección.

Espero que haya sido de utilidad.

¡Saludos!

9 comentarios sobre “Autenticación OAuth

  1. Compartido por Cómo enviar peticiones utilizando OAuth: Firmando peticiones | Return(GiS);

  2. Muy bueno Gisela, me has ahorrado unas buenas horas estidiandome la definición “oficial”.
    Sólo una nota: el símbolo “&” se llama “ampersand”, no “uppersand” ^^
    Saludos u enhorabuena por tu MVP.

    • Hola Fer,

      Ops! Ya lo cambié jajajaja :D Tendría un día raro y puse cualquier cosa ;)

      ¡Me alegro que te haya sido de utilidad!

      ¡Gracias por avisarme! :)

      ¡Saludos!

  3. Hola! estoy haciendo una aplicación en perl para conectarme a un sitio y extraer información de él. Me ha servido mucho el tutorial pero tengo algunas dudas, por ejemplo; ya tengo “access token” y “access token secret” y ahora? como hago que mi para extraer la información del sitio?

    Gracias!!

    Saludos

    Alejandro

  4. disculpa por la pregunta funciona también para C# con silverlight V5?, llevo unos días intentanto hacer que me muestre los tweet pero aun no me ha funcionado, aun que aun me cuesta trabajo la programación, espero que me puedan ayudar y gracias

Deja un comentario