Una API REST es una forma de exponer los datos de la aplicación a una amplia variedad de clientes. Cualquier cosa que pueda hablar HTTP puede comunicarse con una API REST, y en estos días eso significa una variedad emocionante de dispositivos. Desde navegadores web antiguos hasta dispositivos móviles y una amplia gama de aplicaciones de IoT, hay muchas buenas razones para usar una API REST. Y si alguna vez has creado uno, estás familiarizado con los medios predominantes para restringir los puntos finales a los usuarios autenticados: la autenticación basada en JSON Web Token (JWT).
En este tutorial de autenticación de JWT, aprenderás cuándo usar JWT, por qué no debes usar JWT para las sesiones y cómo almacenar JWT en cookies para evitar problemas de seguridad. También repasaremos algunas de las mejores prácticas generales de JWT.
Indice
¿Qué es JWT?
Un JWT es un mecanismo para verificar el propietario de algunos datos JSON. Es una cadena codificada y segura para URL que puede contener una cantidad ilimitada de datos (a diferencia de una cookie) y está firmada criptográficamente.
Cuando un servidor recibe un JWT, puede garantizar que los datos que contiene sean confiables porque están firmados por la fuente. Ningún intermediario puede modificar un JWT una vez enviado.
Es importante tener en cuenta que un JWT garantiza la propiedad de los datos pero no el cifrado. Cualquier persona que intercepte el token puede ver los datos JSON que almacena en un JWT porque solo está serializado, no cifrado.
Por esta razón, se recomienda encarecidamente utilizar HTTPS con JWT (y HTTPS en general, por cierto).
La razón principal para usar JWT es intercambiar datos JSON de una manera que se pueda verificar criptográficamente. Hay dos tipos de JWT:
- JSON Web Signature (JWS)
- JSON Web Encryption (JWE)
Los datos en un JWS son públicos, lo que significa que cualquier persona con el token puede leer los datos, mientras que un JWE está encriptado y es privado. Para leer los datos contenidos en un JWE, necesitas tanto el token como una clave secreta.
Cuando usas un JWT, generalmente es un JWS. La ‘S’ (la firma) es la parte importante y permite validar el token.
Cuándo usar la autenticación JWT
JWT es una tecnología particularmente útil para la autenticación API y la autorización de servidor a servidor.
Supongamos que estamos desarrollando un cliente para la API de nómina de nuestra empresa. Esta API está destinada a emitir pagos a los empleados de la empresa, recuperar información histórica sobre ellos y, finalmente, editar la información de los empleados.
Además (para evitar errores humanos) los desarrolladores de la API decidieron que algunas de estas acciones requieren privilegios de administrador. Por lo tanto, tendremos usuarios con acceso normal que solo pueden revisar información y usuarios con súper acceso (administradores) que también pueden emitir pagos y editar datos.
La autenticación basada en token es probablemente el método más común para autenticar solicitudes a los puntos finales de la API REST. Funciona así:
- Un usuario inicia sesión en una aplicación con un nombre de usuario y contraseña, o prueba su identidad.
- El servidor confirma su identidad y devuelve un token de acceso que contiene una referencia a su identidad (por ejemplo, una clave privada que apunta a una instancia de usuario única).
- Luego, el cliente incluye este token de acceso con cada solicitud al servidor.
- Para las rutas protegidas, el middleware de autenticación de la API REST afirma la presencia de un token de acceso válido. El servidor puede usar además la identidad afirmada por el token validado para implementar permisos más granulares, como actuar sobre los recursos que pertenecen a ese usuario en particular.
Las características de un JWT lo convierten en una excelente opción para la autenticación basada en tokens. Queremos un paquete ligero, ya que se incluirá en cada solicitud a nuestra API REST. También debe ser a prueba de manipulaciones, de modo que el reclamo de identidad no se pueda alterar en tránsito o falsificar por completo.
Una de las mayores ventajas de este enfoque es que es apátrida. No requiere que el cliente o el servidor REST mantengan sesiones. De hecho, no se requiere ninguna búsqueda en la base de datos para verificar la identidad del usuario solicitante. Debido a que el JWT está firmado con un secreto guardado por la API REST, podemos implícitamente confiar en que este usuario es quien dice ser.
Usar JWT para la autenticación de API
Un uso muy común de JWT, y quizás el único bueno, es como mecanismo de autenticación de API.
La tecnología JWT es tan popular y ampliamente utilizada que Google la utiliza para permitirle autenticarse en sus API.
La idea es simple: obtienes un token secreto del servicio cuando configuras la API.
En el lado del cliente, creas el token (hay muchas bibliotecas para esto) usando el token secreto para firmarlo.
Cuando lo pasas como parte de la solicitud de API, el servidor sabrá que es ese cliente específico porque la solicitud está firmada con su identificador único.
Uso de JWT para la autenticación de SPA
Los JWT se pueden utilizar como un mecanismo de autenticación que no requiere una base de datos. El servidor puede evitar el uso de una base de datos porque el almacén de datos en el JWT enviado al cliente es seguro.
Uso de JWT para autorizar operaciones en servidores
Supongamos que tienes un servidor en el que has iniciado sesión, SERVER1, que te redirige a otro servidor SERVER2 para realizar algún tipo de operación.
SERVER1 puede emitirte un JWT que te autoriza a SERVER2. Esos dos servidores no necesitan compartir una sesión ni nada para autenticarte. El token es perfecto para este caso de uso.
Por qué no deberías usar JWT como tokens de sesión
No debes usar JWT como tokens de sesión de forma predeterminada. Por un lado, JWT tiene una amplia gama de características y un gran alcance, lo que aumenta la posibilidad de errores, ya sea por parte de los autores de la biblioteca o de los usuarios.
Otro problema es que no puedes eliminar un JWT al final de una sesión porque es autónomo y no hay una autoridad central para invalidarlos.
Finalmente, en pocas palabras, los JWT son relativamente grandes. Cuando se usa con cookies, esto se suma a una tonelada de gastos generales por solicitud.
El uso de JWT para tokens de sesión puede parecer una buena idea al principio porque:
- Puede almacenar cualquier tipo de datos de usuario en el cliente.
- El servidor puede confiar en el cliente porque el JWT está firmado y no es necesario llamar a la base de datos para recuperar la información que ya almacenó en el JWT.
- No necesita coordinar sesiones en una base de datos centralizada cuando llega al eventual problema de escalamiento horizontal
En última instancia, si ya tienes una base de datos para tu aplicación, simplemente usa una tabla de sesiones y usa sesiones regulares según lo provisto por el marco del lado del servidor de tu elección.
¿Por qué? Hay un coste involucrado en el uso de JWT: se envían para cada solicitud al servidor y siempre es un coste alto en comparación con las sesiones del lado del servidor.
Además, si bien se minimizan los riesgos de seguridad al enviar JWT mediante HTTPS, siempre existe la posibilidad de que sea interceptado y descifrado los datos, exponiendo los datos de su usuario.
Cómo invalidar un solo token JWT
Una solución sin esfuerzo es cambiar la clave secreta del servidor, lo que invalida todos los tokens. Sin embargo, esto no es ideal para los usuarios, cuyos tokens pueden caducar sin ningún motivo.
Una forma de hacerlo es agregar una propiedad a su objeto de usuario en la base de datos del servidor para hacer referencia a la fecha y hora en que se creó el token.
Un token almacena automáticamente este valor en la iatpropiedad. Cada vez que verificas el token, puedes comparar su iatvalor con la userpropiedad del lado del servidor.
Para invalidar el token, simplemente actualiza el valor del lado del servidor. Si iat es más antiguo que este, puede rechazar el token.
Otra forma de lograr esto es estableciendo una lista negra en tu base de datos almacenada en la memoria caché (o, mejor aún, una lista blanca).
Cómo almacenar de forma segura JWT en una cookie
Un JWT debe almacenarse en un lugar seguro dentro del navegador del usuario. Si lo almacenas dentro de localStorage, cualquier script dentro de su página podrá acceder a él. Esto es tan malo como parece; un ataque XSS podría dar acceso al token a un atacante externo.
Para reiterar, hagas lo que hagas, no guardes un JWT en el almacenamiento local (o en el almacenamiento de sesiones). Si alguno de los scripts de terceros que incluyes en tu página se ve comprometido, puede acceder a todos los tokens de sus usuarios.
Para mantenerlos seguros, siempre debes almacenar los JWT dentro de una cookie httpOnly. Este es un tipo especial de cookie que solo se envía en solicitudes HTTP al servidor. Nunca es accesible (tanto para leer como para escribir) desde JavaScript que se ejecuta en el navegador.
¿Por qué utilizar JWT?
En resumen, los JWT se utilizan como una forma segura de autenticar usuarios y compartir información.
Normalmente, el emisor utiliza una clave privada o secreta para firmar el JWT. El receptor del JWT verificará la firma para asegurarse de que el token no haya sido alterado después de que fue firmado por el emisor. Es difícil para las fuentes no autenticadas adivinar la clave de firma e intentar cambiar las afirmaciones dentro del JWT.
Sin embargo, no todos los algoritmos de firma son iguales. Por ejemplo, algunos algoritmos de firma utilizan un valor secreto que se comparte entre el emisor y la parte que verifica el JWT. Otros algoritmos utilizan una clave pública y privada. La clave privada es conocida solo por el emisor, mientras que la clave pública puede distribuirse ampliamente. La clave pública se puede usar para verificar la firma, pero solo la clave privada se puede usar para crear la firma. Esto es más seguro que un secreto compartido porque la clave privada solo necesita existir en un lugar.
Debido a esto, el servidor no necesita mantener una base de datos con la información necesaria para identificar al usuario. Para los desarrolladores, esta es una gran noticia: el servidor que emite el JWT y el servidor que lo valida no tienen que ser el mismo.
Cómo elegir la mejor biblioteca JWT
¿Cómo decides qué biblioteca JWT usar en tu proyecto? Un buen lugar para comenzar es esta lista de bibliotecas JWT para la firma y verificación de tokens.
El sitio contiene una lista de las bibliotecas más populares que implementan JWT, incluidas las bibliotecas para Node.js , Python, Rust , Go, JavaScript y muchas más.
Selecciona el idioma que prefieras y elige la biblioteca que prefieras, idealmente, la que tenga el mayor número de checks verdes.
Firmas JWT: ¿cómo se utilizan para la autenticación?
La última parte de un JWT es la firma, que es un código de autenticación de mensaje. La firma de un JWT solo puede ser producida por alguien en posesión tanto de la carga útil (más el encabezado) como de una clave secreta determinada.
Así es como se utiliza la firma para garantizar la autenticación:
- El usuario envía el nombre de usuario y la contraseña a un servidor de autenticación, que puede ser nuestro servidor de aplicaciones, pero normalmente es un servidor independiente.
- El servidor de autenticación valida la combinación de nombre de usuario y contraseña y crea un token JWT con una carga útil que contiene el identificador técnico del usuario y una marca de tiempo de vencimiento.
- El servidor de autenticación luego toma una clave secreta y la usa para firmar el encabezado más la carga útil y lo envía de vuelta al navegador del usuario.
- El navegador toma el JWT firmado y comienza a enviarlo con cada solicitud HTTP a nuestro servidor de aplicaciones.
- El JWT firmado actúa efectivamente como una credencial de usuario temporal, que reemplaza la credencial permanente que es la combinación de nombre de usuario y contraseña.
Y a partir de ahí, esto es lo que hace nuestro servidor de aplicaciones con el token JWT:
- Nuestro servidor de aplicaciones verifica la firma JWT y confirma que, de hecho, alguien en posesión de la clave secreta firmó esta carga útil en particular.
- La carga útil identifica a un usuario en particular a través de un identificador técnico.
- Solo el servidor de autenticación está en posesión de la clave privada, y el servidor de autenticación solo entrega tokens a los usuarios que envían la contraseña correcta.
- Por lo tanto, nuestro servidor de aplicaciones puede estar seguro de que este token fue entregado a este usuario en particular por el servidor de autenticación, lo que significa que es el usuario, ya que tenía la contraseña correcta.
- El servidor procede a procesar la solicitud HTTP asumiendo que realmente pertenece a ese usuario.
La única forma de que un atacante se haga pasar por un usuario sería robar su nombre de usuario y contraseña de inicio de sesión personal, o robar la clave de firma secreta del servidor de autenticación.
Como podemos ver, ¡la firma es realmente la parte clave del JWT!
La firma es lo que permite a un servidor completamente sin estado estar seguro de que una solicitud HTTP determinada pertenece a un usuario determinado, con solo mirar un token JWT presente en la solicitud en sí, y sin forzar que la contraseña se envíe cada vez con la solicitud.
Firma digital HS256 JWT: ¿cómo funciona?
Como la mayoría de las firmas, la firma digital HS256 se basa en un tipo especial de función: una función hash criptográfica.
Esto suena intimidante, pero es un concepto que vale la pena aprender: este conocimiento fue útil hace 20 años y será útil durante mucho tiempo. Gran parte de la implementación de seguridad práctica gira en torno a los hash, están en todas partes en la seguridad de las aplicaciones web.
La buena noticia es que es posible explicar todo lo que necesitamos saber sobre Hashing en unos pocos párrafos.
¿Qué es una función hash?
Una función hash es un tipo especial de función con algunas propiedades únicas: tiene muchos casos de uso prácticos y útiles, como firmas digitales.
Vamos a hablar de 4 propiedades interesantes de estas funciones, y luego veremos por qué estas propiedades nos permiten producir una firma verificable.
Propiedades
Las propiedades de las funciones hash son:
Irreversibilidad
Esto significa que si tomamos nuestro encabezado y carga útil y lo ejecutamos a través de esta función, nadie podrá recuperar los datos con solo mirar la salida.
El hash no es encriptación: la encriptación por definición es una acción reversible; necesitamos recuperar la entrada original de la salida encriptada.
Reproducible
Otra cosa importante que debes saber sobre el hash es que es reproducible, lo que significa que si aplicamos el mismo hash al mismo lector de entrada y carga útil varias veces, siempre obtendremos exactamente el mismo resultado.
Esto significa que dados un par de entradas y una salida hash, siempre podemos validar si una salida dada (por ejemplo, una firma) es correcta porque podemos reproducir fácilmente el cálculo, pero solo si tenemos todas las entradas.
Sin colisiones
Otra propiedad interesante de las funciones de hash es que si le enviamos varios valores, siempre obtenemos un resultado único por valor de entrada.
Efectivamente, no hay situaciones en las que dos entradas diferentes produzcan la misma salida: una entrada única produce una salida única.
Esto significa que si aplicamos hash a la Carga útil más el Encabezado, siempre obtenemos exactamente el mismo resultado, y ningún otro dato de entrada podría haber producido la misma salida de hash; la salida de hash es efectivamente una representación única de los datos de entrada.
Impredecibilidad
La última propiedad que cubriremos sobre las funciones hash es que dada una salida conocida, no es posible adivinar la entrada usando un método de aproximación incremental sucesivo.
Digamos que teníamos la salida hash justo arriba y estábamos tratando de averiguar la carga útil que la generó, adivinando una entrada e inspeccionando la salida para ver si se acerca al resultado esperado.
Luego, simplemente ajustamos un carácter en la entrada y luego revisamos la salida nuevamente para ver si nos acercamos, y si es así, repetimos el proceso hasta que logremos adivinar la entrada.
Con funciones hash, ¡esta estrategia no funcionará!
Esto se debe a que en una función hash, si cambiamos incluso un solo carácter en la entrada (en realidad, incluso un solo bit), ¡en promedio, el 50% de los bits de salida cambiarán!
Entonces, incluso las diferencias mínimas en la entrada crean una salida completamente diferente.
Desventajas de las firmas HS256
Las firmas HS256 se pueden forzar brutalmente si la clave secreta de entrada es débil, pero eso podría decirse de muchas otras tecnologías basadas en claves.
Sin embargo, las firmas basadas en hash son particularmente más sencillas de usar por fuerza bruta en comparación con otras alternativas, para tamaños de clave de producción típicos.
Pero más que eso, una desventaja práctica de HS256 es que requiere la existencia de una contraseña secreta previamente acordada entre el servidor que emite los JWT y cualquier otra máquina servidor que consuma los JWT para la validación y la identificación del usuario.
Rotación de teclas poco práctica
Esto significa que si queremos cambiar la contraseña necesitamos distribuirla e instalarla en todos los nodos de red que la necesiten, lo cual no es conveniente, es propenso a errores e implicaría un tiempo de inactividad coordinado del servidor.
Es posible que esto ni siquiera sea factible, digamos que un servidor es administrado por un equipo completamente diferente o incluso por una organización de terceros.
Sin separación entre creación y validación de tokens
Todo se reduce al hecho de que no hay distinción entre la capacidad de crear JWT y la capacidad de simplemente validarlos: con HS256, todos en la red pueden crear y validar tokens porque todos tienen la contraseña secreta.
Esto significa que hay muchos más lugares donde un atacante puede perder o robar la contraseña, ya que la contraseña debe instalarse en todas partes y no todas las aplicaciones tienen el mismo nivel de seguridad operativa.
Una forma de mitigar esto es crear una contraseña compartida por aplicación, pero en su lugar, vamos a aprender sobre un nuevo método de firma que resuelve todos estos problemas y que las soluciones modernas basadas en JWT ahora usan de forma predeterminada: RS256.
La firma RS256 JWT
Con RS256 seguiremos produciendo un código de autenticación de mensajes como antes, el objetivo sigue siendo crear una firma digital que demuestre que un JWT determinado es válido.
Pero en el caso de esta firma, vamos a separar la capacidad de crear tokens válidos, que solo debería tener el servidor de autenticación, de la capacidad de validar tokens JWT, que solo nuestro servidor de aplicaciones se beneficiaría de hacer.
La forma en que vamos a hacerlo es que vamos a crear dos claves en lugar de una:
- Seguirá habiendo una clave privada, pero esta vez será propiedad únicamente del servidor de autenticación, que se utilizará únicamente para firmar los JWT.
- La clave privada se puede usar para firmar JWT, pero no se puede usar para validarlos.
- Hay una segunda clave llamada clave pública, que es utilizada por el servidor de aplicaciones solo para validar los JWT.
- La clave pública se puede usar para validar firmas JWT, pero no se puede usar para firmar nuevos JWT.
- No es necesario que la clave pública se mantenga privada y, a menudo, no lo es, porque si el atacante la obtiene, no hay forma de usarla para falsificar firmas.
JWT en la empresa
Los JWT también son aplicables a la empresa, como una gran alternativa a la configuración típica de autenticación previa, que es una responsabilidad de seguridad conocida.
En la configuración de autenticación previa que utilizan muchas empresas, ejecutamos nuestros servidores de aplicaciones detrás de un proxy en una red privada y simplemente recuperamos el usuario actual de un encabezado HTTP.
El encabezado HTTP que identifica al usuario generalmente se completa con un elemento centralizado de la red, generalmente una página de inicio de sesión centralizada instalada en un servidor proxy que se encargará de la sesión del usuario.
Ese servidor bloqueará el acceso a la aplicación si la sesión expira y autenticará a los usuarios después de iniciar sesión.
Después de eso, reenviará todas las solicitudes al servidor de aplicaciones y simplemente agregará un encabezado HTTP para identificar al usuario.
El problema es que con esa configuración en la práctica, cualquier persona dentro de la red puede hacerse pasar por un usuario fácilmente configurando el mismo encabezado HTTP.
Hay soluciones para eso, como incluir en la lista blanca la IP del servidor proxy al nivel del servidor de aplicaciones o usar un certificado de cliente, pero en la práctica, la mayoría de las empresas no tienen estas medidas.
Una mejor versión de la configuración de autenticación previa
Sin embargo, la idea de la autenticación previa es buena porque esta configuración significa que el desarrollador de aplicaciones no tiene que implementar las funciones de autenticación en cada aplicación, ahorrando tiempo y evitando posibles errores de seguridad.
¿No sería genial tener la conveniencia de la configuración de autenticación previa, sin tener que comprometer la seguridad y hacer que la autenticación de nuestra aplicación se pueda omitir fácilmente, incluso si solo está dentro de una red privada?
Esto es simple de hacer si ponemos JWT en la imagen: en lugar de simplemente poner el nombre de usuario en el encabezado como solemos hacer en Pre-Autenticación, hagamos que el encabezado HTTP sea un JWT.
Luego pongamos el nombre de usuario dentro de la carga útil de ese JWT, firmado por el servidor de autenticación.
El servidor de aplicaciones, en lugar de simplemente tomar el nombre de usuario del encabezado, primero validará el JWT:
- si la firma es correcta, entonces el usuario está correctamente autenticado y la solicitud pasa,
- si no, el servidor de aplicaciones puede simplemente rechazar la solicitud.
El resultado es que ahora tenemos la autenticación funcionando correctamente, ¡incluso dentro de la red privada!
Ya no tenemos que confiar ciegamente en el encabezado HTTP que contiene el nombre de usuario. Podemos asegurarnos de que ese encabezado HTTP sea válido y emitido por el proxy, y que no sea un atacante que intente iniciar sesión como otro usuario.
Ataques contra JWT
Por último, destacamos los siguientes ataques contra JWT:
Manipulación del algoritmo de firma ‘none’
JWT admite el uso del algoritmo ‘none’ para casos de uso en los que la integridad del reclamo dentro de JWT ya se verifica por otros medios. Este algoritmo permite al servidor emitir un JWT sin firma.
Vale la pena señalar que el algoritmo ‘none’ junto con ‘HMAC SHA-256 (HS256)’ son los únicos dos algoritmos que son obligatorios para implementarse de acuerdo con el estándar JWT. Los atacantes pueden usar esta función para configurar el algoritmo en su token a ‘none’ y proporcionar una firma vacía para engañar al servidor para que lo acepte como un token válido. Sin embargo, la mayoría de las implementaciones modernas ahora tienen un control de seguridad adicional que rechaza los tokens configurados con el algoritmo ‘none’ cuando se usó una clave secreta para emitirlos.
RS256 a HS256
JWT admite el uso de algoritmos de firma asimétricos como RS256, que utiliza una clave privada para firmar el token y una clave pública para verificar la firma. La clave privada solo es conocida por el servidor y la clave pública es accesible para todos. El uso de algoritmos de firma asimétrica es útil en situaciones en las que los clientes lo necesitan para verificar la validez de una JWT no emitidos por las mismas. Un servidor de la firma JWTs con un algoritmo simétrico como HS256 tendrá que compartir la clave secreta con todos los clientes fabricantes que desean verificar el token. Esto aumenta el riesgo de que se revele la clave secreta.
En implementaciones inseguras donde el servidor confía en los datos dentro del encabezado de un JWT y no valida el algoritmo que usó para emitir un token, los atacantes pueden cambiar el algoritmo de ‘RS256’ a ‘HS256’ y usar la clave ‘pública’ para generar una firma HMAC para el token. El servidor ahora tratará este token como uno generado con el algoritmo ‘HS256’ y usará su clave pública para decodificarlo y verificarlo.
HS256 de fuerza bruta
Los JWT firmados con el algoritmo HS256 podrían ser vulnerables a la divulgación de claves secretas que suele suceder a través de ataques de fuerza bruta, especialmente para claves débiles. Dado que un cliente no necesita interactuar con el servidor para verificar la validez de la clave secreta después de que el servidor emite un token, los atacantes pueden realizar ataques de fuerza bruta fuera de línea contra el token utilizando listas de palabras de posibles claves secretas. Se recomienda utilizar claves suficientemente largas (256 bits) para protegerse contra estos ataques.
Divulgación de información confidencial
Toda la información dentro de la carga útil se almacena en texto sin formato. Es importante no filtrar información confidencial, como direcciones IP internas, a través de los tokens.
Los ataques contra JWT surgen de malas implementaciones y el uso de bibliotecas obsoletas. Para beneficiarse de las funciones de seguridad que ofrece JWT, sigue las mejores prácticas para implementarlas, usa solo bibliotecas actualizadas y seguras y elige el algoritmo adecuado para tu caso de uso.