Introducción
Para comprender mejor este artículo, es necesario que conozcas previamente qué son las cookies y su relación con las server-side sessions.Si tienes en claro la diferencia entre cookies y sesiones, y cómo operan en conjunto, entonces podemos continuar hacia la primera pregunta.
¿Cookies o Tokens? ¿Qué es mejor?
Pregunta
He estado usando cookies y sesiones para gestionar la autenticación de usuarios en mis aplicaciones web, y estoy contento por lo simple que resulta su uso.Sin embargo, un desarrollador iOS me comentó que lo nuevo y más adecuado es usar JWT (JSON Web Tokens).
Me dijo que JWT es el camino a seguir para implementar una autenticación en las aplicaciones móviles nativas. Sin dar ejemplos, simplemente comentó que las aplicaciones Android y iOS tienen problemas con las cookies.
He buscado información, pero no he encontrado algo que demuestre que los JSON Web Tokens sean superiores a las Cookies. Es más, me resultan tan similares que no he podido encontrar una diferencia importante, ni por qué se recomienda su uso con las aplicaciones móviles nativas.
Por lo menos a mi parecer, sí es posible usar Cookies en el desarrollo iOS.
Entonces mi pregunta es, para una aplicación móvil, ¿qué ventajas presenta una API que hace uso de JWT en vez de Cookies para la autenticación de usuarios?
Respuesta
Nosotros, como desarrolladores de software, tenemos una tendencia por aplicar todo aquello nuevo que encontramos. Por dar un ejemplo: si de pronto nos encontramos con un martillo que no hemos visto antes (a pesar de ser sólo una variante de las muchas que conocemos), comenzamos a ver todo como un "clavo". Sentimos la necesidad de aplicar todo aquello nuevo que vamos aprendiendo (en la gran mayoría de ocasiones).Ahora bien, retomando la pregunta original:
- Es importante mencionar que, ni los JWT ni las Cookies constituyen en sí mismos un mecanismo de autenticación.
- El primero sólo define un formato de token, y el segundo es un mecanismo de gestión de estado para las peticiones HTTP.
- Sólo con esto determinamos que es incorrecto decir que uno es superior a otro.
Tradicionalmente las aplicaciones web han usado cookies (en conjunto con sesiones) para hacer un seguimiento de los usuarios que han iniciado sesión. De esta forma, no necesitan enviar sus credenciales en cada petición.
Generalmente, el contenido de una cookie está determinado por un identificador único (generado aleatoriamente). Este ID le permite al servidor encontrar los datos de sesión correspondientes para cada usuario.
Sin embargo, en el desarrollo de APIs es más común aceptar tokens (en formato JWT principalmente) para que el servidor decida si otorgar acceso o no a quien está realizado la petición.
Esto se debe a que:
- Tradicionalmente, el principal tipo de cliente (para visitar aplicaciones web) ha sido el
web browser
(navegador web), que presenta un soporte completo para cookies. - Pero las APIs hoy en día son usadas también por clientes HTTP mucho más simples, que no soportan cookies de forma nativa.
Es decir, si una API admite tokens entonces aumentará el rango de clientes que puede atender, por lo que resulta más conveniente si la API se debe usar más allá de los navegadores web.
Hoy en día es posible el uso de cookies en las aplicaciones móviles nativas, pero en resumen: los JWT tienen una ventaja sobre las cookies sólo por el hecho de que su uso es más común. Siguiendo este enfoque, podemos tener más recursos de aprendizaje, SDKs, información sobre las vulnerabilidades más conocidas, etcétera.
Autenticación basada en tokens VS autenticación basada en cookies y sesiones
A continuación vamos a repasar cómo funcionan las cookies (en conjunto con sesiones) y los tokens, para que luego nos resulte más fácil resaltar las diferencias.Autenticación basada en cookies
La autenticación basada en cookies ha sido el método predeterminado (y comprobado) para manejar la autenticación de usuarios durante mucho tiempo.La autenticación basada en cookies presenta un estado (es
stateful
).Al iniciar sesión, luego que un usuario envía sus credenciales (y estas se validan), el servidor registra datos (con el fin de recordar que el usuario se ha identificado correctamente). Estos datos que se registran en el backend, en correspondencia con el identificador de sesión, es lo que se conoce como estado.
En el lado del cliente una cookie es creada para almacenar el identificador de sesión, mientras que los datos se almacenan en el servidor (y son llamados variables de sesión).
El flujo que sigue este sistema de autenticación tradicional es el siguiente:
- Un usuario ingresa sus credenciales (datos que le permiten iniciar sesión)
- El servidor verifica que las credenciales sean correctas, y crea una sesión (esto puede corresponderse con la creación de un archivo, un registro nuevo en una base de datos, o alguna otra solución server-side)
- Una cookie con el
session ID
es puesta en el navegador web del usuario - En las peticiones siguientes, el
session ID
es comparado con las sesiones creadas por el servidor - Una vez que el usuario se desconecta, la sesión es destruida en ambos lados (tanto en el cliente como en el servidor)
Autenticación basada en tokens
La autenticación basada en tokens ha ganado prevalencia en los últimos años debido al aumento de lasSingle Page Applications
, web APIs y la Internet de las cosas (Internet of Things
en inglés).Cuando hablamos de autenticación con tokens, generalmente hablamos de autenticación con JSON Web Tokens (JWT).
Si bien existen diferentes implementaciones, los JWT se han convertido en el estándar de facto. Con ello en mente, en el resto del artículo, tokens y JWT se usarán indistintamente.
La autenticación basada en tokens carece de estado (es
stateless
). El servidor ya no guarda información de qué usuarios están conectados o qué tokens se han emitido. Esto es así porque cada solicitud realizada al servidor va acompañada de un token, y el servidor verifica la autenticidad de la solicitud basándose únicamente en el token.
Como ya comentamos antes, JWT define un formato para los tokens. Pero JWT no nos ata a ningún mecanismo de persistencia de datos en el lado del cliente y tampoco a ninguna regla de cómo se debe transportar el token.
Los tokens se envían generalmente como un
Authorization header
, con el valor Bearer {JWT}
; pero pueden enviarse también en el cuerpo de una petición POST o incluso como un query parameter
. Veamos cómo funciona:
- Un usuario ingresa sus credenciales (datos que le permiten iniciar sesión)
- El servidor verifica que las credenciales sean correctas, y devuelve un token firmado
- El token es guardado en el lado del cliente, comúnmente en el
local storage
(pero puede guardarse también en elsession storage
o incluso como una cookie) - Las peticiones siguientes al servidor incluyen este token (a través de un
Authorization header
o alguno de los otros métodos antes mencionados) - El servidor decodifica el JWT y si el token es válido procesa la solicitud
- Una vez que el usuario se desconecta, el token es destruido en el lado del cliente (no es necesaria la interacción con el servidor)
Ventajas de la autenticación basada en tokens
Luego de comprender cómo funcionan ambos enfoques, vamos a ver las ventajas que presenta la autenticación basada en tokens sobre el enfoque tradicional basado en cookies.Sin estado, escalable y desacoplado
Probablemente la mayor ventaja de usar tokens y no cookies es el hecho de que ofrecen una autenticación sin estado (stateless).Desde backend no se necesita tener un registro de los tokens. Cada token es autónomo: contienen en sí mismos toda la data necesaria para confirmar su validez (así como también información puntual del usuario que ha iniciado sesión).
De esta forma, el único trabajo del servidor es: firmar tokens ante un inicio de sesión exitoso, y verificar que los tokens entrantes sean válidos.
Cross Domain y CORS
Las cookies funcionan bien con un dominio (o subdominio) en específico, pero cuando se trata de administrar cookies en diferentes dominios, el manejo se torna complicado.En contraste, un enfoque basado en tokens con CORS habilitado hace que sea trivial exponer las APIs a diferentes servicios y dominios.
Dado que se requiere y se verifica un token en cada una de las llamadas al backend, siempre que haya un token válido, las solicitudes se pueden procesar. Sobre esto, hay algunos detalles que debemos tener en cuenta, y los abordaremos en la sección de Preguntas comunes).
Guardar datos en los JWT
Con un enfoque basado en cookies, simplemente guardamos el identificador de sesión.Los tokens por otro lado nos permiten guardar cualquier tipo de metadata, siempre que se trate de un JSON válido.
La especificación de JWT indica que podemos incluir diferentes tipos de datos (llamados
claims
), y que se pueden guardar como datos reservados, públicos y privados. Dependiendo del contexto, podemos optar por usar una cantidad mínima de claims, y guardar sólo la identificación de usuario y el vencimiento del token, o bien podemos incluir claims adicionales, como el email del usuario, quién emitió el token, los alcances y/o permisos de los que dispone el usuario, etcétera.
Performance
Al utilizar una autenticación basada en cookies, desde backend se debe realizar una búsqueda de la sesión (correspondiente al identificador enviado por el cliente; ya sea en archivos, en una base de datos SQL tradicional o una alternativa NoSQL). En ese caso es muy probable que la ida y vuelta tome más tiempo si lo comparamos con la decodificación de un token. Además, como se pueden almacenar datos adicionales en los tokens (como el nivel de permisos), podemos disminuir la cantidad de búsquedas requeridas para obtener y procesar los datos solicitados.Por ejemplo, supongamos que tenemos un recurso
/api/orders
en nuestra API que devuelve las últimas órdenes registradas en nuestra aplicación, pero sólo los usuarios con rol administrador tienen acceso para ver esta data. En un enfoque basado en cookies, una vez que se realiza la petición, desde backend es necesario hacer una consulta para verificar que la sesión es válida, otra búsqueda para acceder a los datos del usuario y verificar que tenga el rol de administrador, y finalmente una tercera consulta para obtener los datos.
Por otro lado, usando JWT, podemos guardar el rol del usuario en el token. Así, una vez que la petición se realiza y el token se valida, necesitamos realizar una sola consulta a la base de datos (para acceder a la información de las órdenes).
Listo para móviles
- Las APIs modernas no solo interactúan con el navegador.
- Escribir correctamente una API implica que pueda ser usada tanto por navegadores como desde plataformas móviles nativas (como iOS y Android).
- Las plataformas móviles nativas y las cookies no operan muy bien en conjunto, ya que se debe tener en cuenta toda una serie de consideraciones para su correcto funcionamiento.
- Los tokens, por otro lado, son mucho más fáciles de implementar (tanto en iOS como en Android). También son más fáciles de implementar para aplicaciones y servicios de Internet of Things (que no incorporan el concepto de gestión de cookies).
Preguntas comunes e inquietudes
En esta sección, veremos algunas preguntas y preocupaciones comunes que surgen con frecuencia cuando se trata el tema de autenticación basada en tokens.El tema principal es la seguridad, pero examinaremos también el tamaño que pueden tener los tokens, el almacenamiento y la encriptación.
Tamaño de los JWT
La mayor desventaja de la autenticación basada tokens es el tamaño de los JWT.
Una cookie de sesión es relativamente pequeña en comparación (incluso) con el token más pequeño.
Dependiendo del caso, el tamaño de un token puede resultar problemático si lo cargamos con muchos claims.
Recuerda que cada solicitud al servidor debe incluir el correspondiente JWT.
¿Dónde almacenar los tokens?
Con una autenticación basada en tokens, tenemos la opción de escoger dónde guardar los JWT.
Comúnmente, los JWT son almacenados en el
local storage
de los navegadores, y esto funciona bien para la mayoría de los casos. Existen algunos inconvenientes a tener en cuenta si almacenamos los JWT en el
local storage
(los mencionamos luego). Podemos almacenar un token en una cookie, pero el tamaño máximo de una cookie es de 4kb, por lo que puede ser problemático si el token presenta varios claims. También podemos almacenar un token en el
session storage
, que es similar al local storage
, pero se borra en el instante en que el usuario cierra el navegador.Protección XSS y XSRF
Proteger a nuestros usuarios y servidores es siempre una prioridad.
Las preocupaciones más comunes que tienen los desarrolladores para decidir si usar o no la autenticación basada en tokens son acerca de la seguridad.
Dos de los vectores de ataque más comunes que enfrentan los sitios web son:
- Cross Site Scripting (XSS), y
- Cross Site Request Forgery (XSRF o CSRF).
El vector de ataque más común aquí es si un sitio web presenta entradas (inputs) que no están debidamente validadas.
Si un atacante puede ejecutar código Javascript sobre tu dominio, tus JSON Web Tokens son vulnerables.
Muchos frameworks, automáticamente validan (desinfectan) las entradas de datos y evitan la ejecución de código arbitrario.
Si no estás utilizando un framework (que realice esta validación), puedes usar también plugins (como
caja
, un plugin desarrollado por Google para ayudar con esta tarea). Se recomienda usar un framework o plugin para tener este problema resuelto, versus la alternativa de crear una solución propia.
Los ataques de Cross Site Request Forgery no son un problema si estás utilizando JWT con el
local storage
. Por otro lado, si almacenas el JWT en una cookie, deberás protegerte contra XSRF.Si este concepto no te resulta conocido, puedes ver este video que explica con mayor detalle cómo funcionan los ataques XSRF.
Afortunadamente, prevenir los ataques XSRF no es muy complicado. En resumen: para protegernos en contra de ataques XSRF, nuestro servidor, al establecer una sesión con un cliente debe generar un token único (es importante tener en claro que no es un JWT). Luego, cada vez que se envíen datos al servidor, un campo de entrada oculto (
hidden input field
) contendrá este token y el servidor lo validará para asegurarse de que los tokens coincidan. Otra buena forma de proteger a nuestros usuarios y servidores consiste tener un tiempo de expiración corto para los tokens. De esta forma, incluso si un token se ve comprometido, rápidamente se volverá inútil. Además, podemos mantener una lista negra (
blacklist
) de tokens comprometidos y así evitar que estos tokens puedan usarse. Finalmente, un enfoque definitivo sería cambiar el algoritmo de firma, lo que invalidaría todos los tokens activos y requeriría que todos los usuarios inicien sesión de nuevo. Este enfoque no es recomendable, pero está disponible en caso de una infracción grave.Los Tokens son firmados, mas no encriptados
Un JSON Web Token se compone de 3 partes: header, payload, y signature.
El formato de los JWT consiste en unir estas partes usando un punto entre ellas:
header.payload.signature
. Por ejemplo, si tuviéramos que firmar un JWT con el algoritmo HMACSHA256, la clave secreta 'shhhh' y el siguiente contenido (payload):
{
"sub": "1234567890",
"name": "Ado Kukic",
"admin": true
}
El JWT generado sería:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkbyBLdWtpYyIsImFkbWluIjp0cnVlLCJpYXQiOjE0NjQyOTc4ODV9.Y47kJvnHzU9qeJIN48_bVna6O0EDFiMiQ9LpNVDFymM
Lo más importante a tener en cuenta aquí, es que este token está firmado usando el algoritmo HMACSHA256, pero el encabezado (header) y los datos (payload) están codificados en Base64URL (no están encriptados).Si vamos a jwt.io, pegamos el token y seleccionamos el algoritmo HMACSHA256 (abreviado como HS256), podemos decodificar el token y leer su contenido. Por lo tanto, no hace falta decir que datos confidenciales, como contraseñas, nunca deben almacenarse en el payload.
Si necesitas almacenar datos confidenciales en el payload, puedes usar JSON Web Encryption (JWE). JWE permite cifrar el contenido de un JWT para que no sea legible por nadie más que por el servidor. Aunque es posible encriptar el contenido de un token, no es realmente necesario para un sistema de autenticación (lo que sí es importante es que usemos el protocolo HTTPS para que los mensajes que intercambiamos con el servidor viajen cifrados).
Autenticación: Uso de JWT vs sesiones
Los JWT proveen un mecanismo para mantener el estado de una sesión en el lado del cliente, en vez de hacerlo en el servidor.Por lo tanto, una pregunta más adecuada sería, "¿Cuáles son los beneficios de usar JWT sobre usar sesiones del lado del servidor?" (server-side sessions).
Con las server-side sessions es necesario guardar las sesiones activas en una base de datos o en memoria; y por otro lado asegurar que cada cliente siempre sea atendido por el mismo servidor. Ambos presentan inconvenientes.
- Si se usa una base de datos (u otro almacenamiento centralizado), esto puede convertirse en un cuello de botella (una preocupación más), ya que se requiere realizar una consulta al atender cada request (petición).
- Con una solución en memoria limitamos nuestro escalamiento horizontal, y las sesiones son afectadas por problemas de red (como el reinicio de servidores).
- Guardar los tokens de forma segura
- Transportarlos de forma segura
- Los JWT (que representan a las sesiones) pueden ser difíciles de invalidar
- Asegurar confiabilidad sobre los datos enviados por el cliente
JWT de forma particular ya soluciona el último de los puntos mencionados.
¿Pero qué es un JSON Web Token?:
Es una cadena, un conjunto de caracteres, que contiene un poco de información. Para las sesiones de usuario puede incluir el username y el tiempo de expiración (fecha y hora). Pero en realidad puede representar lo que sea, incluso un identificador de sesión o el perfil completo del usuario que ha iniciado sesión. Aunque, por favor, esto debe evitarse.
Tiene una firma segura que evita que agentes externos malintencionados generen tokens falsos. Se necesita acceder a la clave privada del servidor para firmarlos; y gracias a esta firma se puede verificar que no se hayan modificado (desde que el servidor los firmó).
Se envían en cada petición (tal como sucede con las cookies). Comúnmente se envían a través del
Authorization header
de las peticiones HTTP, pero curiosamente también se pueden usar cookies para transportarlos.Cada token es firmado, y así el servidor puede verificar su validez. El servidor confía en su habilidad para firmar los tokens de forma segura. Para esto existen bibliotecas estándares, que se recomiendan sobre las implementaciones que uno mismo pueda realizar.
A fin de transportar de manera segura el token, lo adecuado es enviarlo a través de un canal encriptado (generalmente
httpS
).Con respecto al almacenamiento seguro del token en el cliente, debemos asegurar que los delincuentes no puedan acceder a ellos. Esto (principalmente) significa evitar que se cargue JS de sitios ajenos sobre nuestra página, porque con ello es posible leer el token y por tanto capturarlo. Esto se mitiga utilizando las mismas estrategias utilizadas para mitigar ataques XSS.
Si tienes la necesidad de invalidar los JWT, definitivamente hay formas de lograrlo.
Almacenar datos en una tabla temporal solo para usuarios que han solicitado que "se cierren sus otras sesiones" es una muy buena alternativa.
Si una aplicación necesita invalidar sesiones de forma específica, de la misma manera se puede guardar el ID de cada sesión y tener una tabla de "tokens desactivados". Esta tabla solo necesita conservar registros en base a la máxima duración permitida para los tokens.
Como habrás notado, la capacidad de invalidar tokens niega parcialmente el beneficio de las sesiones del lado del cliente, en el sentido de que se debe mantener un estado en el servidor (de las sesiones desactivadas). Pero, esta tabla ha de ser mucho más pequeña que la tabla de sesiones original (server-side sessions), por lo que las búsquedas todavía siguen siendo más eficientes.
Otra ventaja del uso de JWT es que resulta fácil de implementar utilizando las bibliotecas disponibles en (probablemente) todos los lenguajes que puedan existir. También está completamente divorciado de su esquema de autenticación: si se pasa a usar un sistema basado en huellas dactilares, no es necesario realizar ningún cambio en el esquema de administración de la sesión.
En resumen, JWT resuelve algunas de las deficiencias de otras técnicas de sesión:
- Autenticación "más barata", porque puede eliminar un consulta a la base de datos (¡o al menos tener una tabla mucho más pequeña para consultar!), lo que a su vez habilita la escalabilidad horizontal.
- Datos del lado del cliente "a prueba de manipulaciones".
Existe mucha crítica negativa sobre los JWT, pero si se implementan con el mismo cuidado que otros tipos de autenticación, al final es lo mismo.
Una nota final: no es correcto comparar Cookies vs Tokens. Las cookies son un mecanismo para almacenar y transportar datos, y por tanto también se pueden usar para almacenar y transportar JSON Web Tokens.
0 Comentarios