DEV Community

Cover image for JWT & Refresh Tokens 🔒
Jean Carlo Vittory Laguna
Jean Carlo Vittory Laguna

Posted on

JWT & Refresh Tokens 🔒

JWT o Json Web Token es un estándar para la autenticación y transferencia segura de datos entre dos partes. Usa la encriptación en base64 y una firma por medio de una clave privada o publico/privada para poder verificar la validez del token.

Estos tokens cuentan con una estructura en particular

Image description

El header o encabezado contiene la información del tipo de token utilizado y el tipo de algoritmo usado para la encriptación. Por otro lado, el payload o carga útil hace referencia a los datos suministrados por el desarrollador y cargados dentro del token en formato JSON. Por ultimo tenemos el signature o la firma que consiste en una clave secreta para ser usada como método de verificación de la validez del token.

Para mayor información referirse a: https://jwt.io/

Ahora bien, en el contexto de una aplicación que requiera un proceso de autenticación y autorización de usuarios por lo general hacemos uso de JWT para generar tokens los cuales cargamos, dentro del payload del token, con información que nos permita verificar en nuestra base de datos que esa persona sea quien dice ser o cuente con los roles y permisos que se requieren para acceder a un recurso en especifico.

Es importante resaltar que el payload utilizado no debe contener información susceptible del usuario como contraseñas, cuentas bancarias entre otras.

Para lograr esto por lo general tenemos un flujo similar a este

Image description

En la anterior imagen observamos como un cliente inicia sesión dentro de nuestra aplicación, enviando mediante una solicitud HTTP, un email y una contraseña la cual verificamos en nuestra base de datos y si dichos datos coinciden devolvemos un ACCESS_TOKEN el cual el cliente almacenara, ya sea en el local storage o en el session storage, y deberá enviar en cada solicitud que realice dentro de nuestra aplicación para autorizar el consumo de los recursos que requiera.

La forma en como enviar el ACCESS_TOKEN desde el servidor dependerá de tu aplicación. Este token puede ser enviado mediante cookies o mediante una respuesta HTTP ordinaria. Cada una tiene ventajas y desventajas que deben ser consideradas.

Este ACCESS_TOKEN contara con un tiempo de expiración el cual configuraremos de acuerdo a las necesidades tanto de seguridad como de experiencia de usuario que nuestra aplicación requiera.

Debemos considerar que entre mas largo sea el tiempo de expiración de nuestros tokens mas vulnerable será nuestra aplicación en términos de seguridad. Por otro lado, entre mas corto sea dicho tiempo, nuestros usuarios deberán hacer login nuevamente para obtener un ACCESS_TOKEN nuevo lo cual entra en detrimento de la experiencia de usuario.

REFRESH TOKENS

Hasta este punto no se ha dicho nada diferente a lo que es normalmente un sistema de autenticación y autorización común que implemente JWT.

Los refresh tokens nos permiten lograr que nuestro usuario no tenga que realizar un login nuevamente cada que el ACCESS_TOKEN expira y lo logra mediante el envío de un token extra o REFRESH_TOKEN cuando el usuario hace login.

La diferencia a este punto es que ambos tokens tienen un tiempo de expiración diferente. Mientras el ACCESS_TOKEN puede llegar a tener 20s el REFRESH_TOKEN puede llegar a tener 6 meses o 1 año de expiración y al vencerse el primer token el usuario utilizara el REFRESH_TOKEN como token de acceso para generar un nuevo ACCESS_TOKEN y de esta forma mantener activa la sesión dentro de nuestra aplicación.

Es posible llegar a pensar que esta solución no es realmente útil ya que simplemente aumentando el tiempo de expiración del ACCESS_TOKEN nos ahorraría tener que enviar un segundo token o podríamos pensar ¿Qué sucede si el REFRESH_TOKEN es hurtado? ¿No pondríamos en riesgo la seguridad de nuestro sistema al tener un tiempo de expiración tan largo?

¿Por qué enviar dos tokens de acceso?

Es importante resaltar que el REFRESH_TOKEN nos permite manejar un concepto llamado SESIONES dentro de nuestra aplicación la cual por lo general es una tabla en nuestra base de datos.

La idea de generar un segundo token es poder mediante este token generar un ACCESS_TOKEN nuevo para el usuario en caso de que dicho ACCESS_TOKEN haya sido expirado y solo generaremos un nuevo ACCESS_TOKEN si el usuario nos envía el ACCESS_TOKEN expirado y el REFRESH_TOKEN existe dentro de nuestro registro de sesiones y es valido como JWT.

Dicho lo anterior, nuestro proceso de autorización ahora no solo consultara la validez del REFRESH_TOKEN y del ACCESS_TOKEN o si el usuario que esta accediendo a los recursos de nuestra aplicación existe, sino que ahora consultara una tabla llamada SESIONES que almacenara los REFRESH_TOKENS activos y, si dicho token existe y esta activo, generara un nuevo ACCESS_TOKEN para acceder a los recursos de nuestra aplicación.

De esta forma estaremos generando ACCESS_TOKENS continuamente solo y solo si nuestro usuario cuente con un ACCESS_TOKEN valido y un REFRESH_TOKEN valido.

Debemos resaltar que no es lo mismo validez del token que tiempo de expiración del token. Nuestro token pudo haber expirado pero sigue siendo valido. Los JWT no pueden ser invalidados y solo podrán ser manejados mediante un tiempo de expiración lo cual no necesariamente quiere decir invalidez

¿Qué sucede si el REFRESH_TOKEN es hurtado? ¿No pondríamos en riesgo la seguridad de nuestro sistema al tener un tiempo de expiración tan largo?

Recordemos que la forma de acceder a nuestra aplicación es mediante el ACCESS_TOKEN y si este ha expirado generaremos otro nuevo mediante la validación del REFRESH_TOKEN. Por lo tanto solo podemos generar un nuevo ACCESS_TOKEN si contamos tanto con el ACCESS_TOKEN y el REFRESH_TOKEN valido.

Si un atacante logra hurtar nuestro ACCESS_TOKEN solo tardara 5s, o menos si así lo configuramos, para ser expulsado de la aplicación ya que no podrá generar uno nuevo al no tener el REFRESH_TOKEN.

Por otro lado, si un atacante logra hurtar nuestro REFRESH_TOKEN no podrá generar un ACCESS_TOKEN ya que, para generar uno, necesita el primer ACCESS_TOKEN generado al momento de hacer login.

Podemos observar aquí la importancia de tener un buen manejo de seguridad en nuestros tokens como por ejemplo:

  • Configurar tiempos cortos en el ACCESS_TOKEN.
  • Transferir nuestros tokens mediante las cabeceras HTTP.
  • Si usamos cookies asegurarnos de usar la bandera HttpOnly y secure para evitar ataques XSS.
  • Limitar el tiempo de expiración de nuestras cookies.

Llegados a este punto encontramos un flujo similar a este

Image description

Ahora veremos de que forma, desde el código, podemos actualizar nuestro ACCESS_TOKEN haciendo uso de nuestro REFRESH_TOKEN.

IMPLEMENTACIÓN

En este ejemplo utilizaremos Node y Express para ejemplificar este proceso y haremos toda nuestra lógica dentro del middleware de autorización. Debemos recordar que nuestro proceso de autenticación, donde estamos generando nuestros dos tokens, ya esta configurado y nos centraremos en como actualizar nuestro ACCESS_TOKEN utilizando nuestro REFRESH_TOKEN.

Sin embargo, debemos realizar un pequeño ajuste a nuestro proceso de autenticación ya que es aquí donde haremos el registro de nuestro REFRESH_TOKEN en la tabla que anteriormente llamamos SESIONES.

El código luce algo así:

Image description

  1. Podemos observar como nuestro servicio de autenticación, que es llamado desde nuestro controlador de login, primero valida la existencia del usuario a través del método getUserByEmail.

  2. Utilizando la librería bcrypt validamos la contraseña y, si este proceso arroja un error, devolvemos a través del manejador de errores el código correspondiente.

  3. Si la validación de la contraseña fue bien registramos en nuestra tabla SESIONES, usando el método createSession, la sesión de nuestro usuario.

  4. Como ultimo paso, firmamos ambos tokens y configuramos el tiempo de expiración para cada uno. Recordemos que el ACCESS_TOKEN por lo general cuenta con un tiempo muy corto de expiración y el REFRESH_TOKEN, por el contrario, se configura con un tiempo largo de expiración.

De esta forma tenemos nuestro proceso de autenticación ahora configurado para que nos devuelva ambos tokens y nos registre una sesión por cada usuario con un login activo.

Proceso de autorización

Por lo general tendemos a realizar el proceso de autorización, para cada uno de los recursos de nuestra aplicación, mediante un middleware. Es en este en donde en este ejemplo realizaremos todo el proceso de validación y generación ACCESS_TOKEN.

Una vez nuestro usuario haya accedido a nuestra aplicación y este haya guardado el ACCESS_TOKEN y el REFRESH_TOKEN utilizaremos nuestro middleware de autorización para validar si nuestro usuario cuenta con los permisos suficientes para consultar el recurso que esta solicitando.

Image description

En la anterior imagen observamos todo el proceso de autorización completo pero vamos a revisarlo linea por linea

  1. En este ejemplo estamos enviando los tokens por medio de el header Authorization y un header personalizado que llamamos x-refresh-token por lo tanto lo primero que realizamos es la validación acerca de la existencia o no de ambos tokens. Si estos no existen, enviaremos un error de tipo Forbbiden.

  2. En las siguientes lineas tenemos una segunda capa de validación en donde, usando la librería JOI, validamos si los datos dentro de estas cabeceras son de tipo string.

  3. dentro del try...catch verificamos el ACCESS_TOKEN y accedemos a los valores payload y expired. Si el payload existe, creamos un valor nuevo dentro del objeto req que llamamos user y lo cargamos con lo que viene en el payload. Por último, ejecutamos la función next la cual nos permite avanzar al controlador.

    Observemos que existe un @ts-ignore ya que para crear valores dentro del objeto Request de Express debemos realizar una configuración extra la cual no entra en el contexto de este blog. Si deseas saber más al respecto puedes leer como hacerlo aquí

  4. Si ocurrió algún problema con la verificación de nuestro ACCESS_TOKEN nuestro método verifyJWT nos devolverá un objeto con el payload null y el expired jwt expired.

    Image description

  5. Luego de esto validamos de que si nuestra propiedad expired tiene un valor truthy y nuestro REFRESH_TOKEN fue validado con éxito validaremos nuestro REFRESH_TOKEN usando el método verifyWT. Caso contrario devolveremos un objeto con una propiedad payload en null.

  6. Si nuestro REFRESH_TOKEN no fue validado con éxito devolveremos un error de Unauthorized.

  7. Caso contrario, avanzaremos con nuestro flujo y consultaremos en nuestra base de datos el sessionId que venia en el payload de nuestro REFRESH_TOKEN y si esta consulta no encuentra ningún resultado volveremos a arrojar un error de tipo Unauthorized

  8. Por el contrario, si nuestra consulta encuentra un sessionId activo firmaremos un nuevo ACCESS_TOKEN.

  9. El payload de este nuevo token lo ingresaremos en la propiedad user de nuestro objeto Request

  10. Por último, agregaremos este nuevo ACCESS_TOKEN dentro de la cabecera Authorization concatenando la palabra Bearer al inicio. Finalizaremos ejecutando la función next para avanzar a nuestro controlador.

De esta forma podemos crear un sistema de tokens el cual hace uso de los REFRESH_TOKEN. Si bien este sistema no es infalible podemos mejorar ciertos aspectos de nuestro sistema como lo hemos visto a través de esta lectura.

Gracias por leer 😊

Top comments (0)