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
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
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
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í:
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
.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.
Si la validación de la contraseña fue bien registramos en nuestra tabla
SESIONES
, usando el métodocreateSession
, la sesión de nuestro usuario.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 elREFRESH_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.
En la anterior imagen observamos todo el proceso de autorización completo pero vamos a revisarlo linea por linea
En este ejemplo estamos enviando los tokens por medio de el header
Authorization
y un header personalizado que llamamosx-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 tipoForbbiden
.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
.-
dentro del
try...catch
verificamos elACCESS_TOKEN
y accedemos a los valorespayload
yexpired
. Si el payload existe, creamos un valor nuevo dentro del objetoreq
que llamamosuser
y lo cargamos con lo que viene en el payload. Por último, ejecutamos la funciónnext
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í -
Si ocurrió algún problema con la verificación de nuestro
ACCESS_TOKEN
nuestro método verifyJWT nos devolverá un objeto con el payloadnull
y el expiredjwt expired
. Luego de esto validamos de que si nuestra propiedad
expired
tiene un valortruthy
y nuestroREFRESH_TOKEN
fue validado con éxito validaremos nuestroREFRESH_TOKEN
usando el métodoverifyWT
. Caso contrario devolveremos un objeto con una propiedad payload ennull
.Si nuestro
REFRESH_TOKEN
no fue validado con éxito devolveremos un error deUnauthorized
.Caso contrario, avanzaremos con nuestro flujo y consultaremos en nuestra base de datos el
sessionId
que venia en el payload de nuestroREFRESH_TOKEN
y si esta consulta no encuentra ningún resultado volveremos a arrojar un error de tipoUnauthorized
Por el contrario, si nuestra consulta encuentra un sessionId activo firmaremos un nuevo
ACCESS_TOKEN
.El payload de este nuevo token lo ingresaremos en la propiedad user de nuestro objeto
Request
Por último, agregaremos este nuevo
ACCESS_TOKEN
dentro de la cabeceraAuthorization
concatenando la palabraBearer
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)