Autenticación y autorización con JWT en Express.js

A

Introducción

En este artículo, hablaremos sobre cómo funciona JSON Web Tokens, cuáles son las ventajas de ellos, su estructura y cómo usarlos para manejar la autenticación básica y la autorización en Express.

No es necesario que tenga experiencia previa con JSON Web Tokens, ya que lo hablaremos desde cero.

Para la sección de implementación, sería preferible si tiene experiencia previa con Rápido, Javascript ES6 y clientes REST.

¿Qué son los tokens web JSON?

Los tokens web JSON (JWT) se han introducido como un método de comunicación entre dos partes de forma segura. Fue introducido con el RFC 7519 especificación del Grupo de trabajo de ingeniería de Internet (IETF).

Aunque podemos usar JWT con cualquier tipo de método de comunicación, hoy JWT es muy popular para manejar la autenticación y autorización a través de HTTP.

Primero, necesitará conocer algunas características de HTTP.

HTTP es un protocolo sin estado, lo que significa que una solicitud HTTP no mantiene el estado. El servidor no conoce ninguna solicitud anterior enviada por el mismo cliente.

Las solicitudes HTTP deben ser independientes. Deben incluir la información sobre solicitudes anteriores que el usuario realizó en la propia solicitud.

Hay algunas formas de hacer esto, sin embargo, la forma más popular es establecer una ID de sesión, que es una referencia a la información del usuario.

El servidor almacenará este ID de sesión en la memoria o en una base de datos. El cliente enviará cada solicitud con este ID de sesión. A continuación, el servidor puede obtener información sobre el cliente mediante esta referencia.

Este es el diagrama de cómo funciona la autenticación basada en sesión:

Normalmente, esta ID de sesión se envía al usuario como una cookie. Ya discutimos esto en detalle en nuestro artículo anterior Manejo de la autenticación en Express.js.

Por otro lado, con JWT, cuando el cliente envía una solicitud de autenticación al servidor, enviará un token JSON de regreso al cliente, que incluye toda la información sobre el usuario con la respuesta.

El cliente enviará este token junto con todas las solicitudes posteriores. Entonces, el servidor no tendrá que almacenar ninguna información sobre la sesión. Pero hay un problema con ese enfoque. Cualquiera puede enviar una solicitud falsa con un token JSON falso y pretender ser alguien que no es.

Por ejemplo, digamos que después de la autenticación, el servidor devuelve un objeto JSON con el nombre de usuario y la fecha de vencimiento al cliente. Entonces, dado que el objeto JSON es legible, cualquiera puede editar esa información y enviar una solicitud. El problema es que no hay forma de validar dicha solicitud.

Aquí es donde entra la firma del token. Entonces, en lugar de simplemente enviar un token JSON simple, el servidor enviará un token firmado, que puede verificar que la información no ha cambiado.

Entraremos en eso con más detalle más adelante en este artículo.

Aquí está el diagrama de cómo funciona JWT:

Estructura de un JWT

Hablemos de la estructura de un JWT a través de un token de muestra:

Como puede ver en la imagen, hay tres secciones de este JWT, cada una separada por un punto.

Barra lateral: la codificación Base64 es una forma de asegurarse de que los datos no estén corruptos, ya que no los comprime ni los cifra, sino que simplemente los codifica de una manera que la mayoría de los sistemas pueden entender. Puede leer cualquier texto codificado en Base64 simplemente decodificándolos.

La primera sección del JWT es el encabezado, que es una cadena codificada en Base64. Si decodificas el encabezado, se vería algo similar a esto:

{
  "alg": "HS256",
  "typ": "JWT"
}

La sección de encabezado contiene el algoritmo hash, que se utilizó para generar el signo y el tipo de token.

La segunda sección es la carga útil que contiene el objeto JSON que se envió de vuelta al usuario. Dado que solo está codificado en Base64, cualquiera puede decodificarlo fácilmente.

Se recomienda no incluir ningún dato sensible en los JWT, como contraseñas o información de identificación personal.

Por lo general, el cuerpo de JWT se verá así, aunque no necesariamente se aplica:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

La mayor parte del tiempo, sub La propiedad contendrá el ID del usuario, la propiedad iat, que es la abreviatura de emitido en, es la marca de tiempo de cuando se emite el token.

También puede ver algunas propiedades comunes como eat o exp, que es la hora de vencimiento del token.

La sección final es la firma del token. Esto se genera mediante el hash de la cadena base64UrlEncode(header) + "." + base64UrlEncode(payload) + secret utilizando el algoritmo que se menciona en la sección del encabezado.

los secret es una cadena aleatoria que solo el servidor debe conocer. Ningún hash puede volver a convertirse al texto original e incluso un pequeño cambio en la cadena original dará como resultado un hash diferente. Entonces el secret no puede someterse a ingeniería inversa.

Cuando esta firma se envía de vuelta al servidor, puede verificar que el cliente no haya cambiado ningún detalle en el objeto.

Según los estándares, el cliente debe enviar este token al servidor a través de la solicitud HTTP en un encabezado llamado Authorization con la forma Bearer [JWT_TOKEN]. Entonces el valor de la Authorization El encabezado se verá algo como:

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

Si desea leer más sobre la estructura de un token JWT, puede consultar nuestro artículo detallado, Comprender los tokens web JSON. También puedes visitar jwt.io y jugar con su depurador:

Ventaja de usar JWT sobre métodos tradicionales

Como hemos comentado anteriormente, JWT puede contener toda la información sobre el propio usuario, a diferencia de la autenticación basada en sesión.

Esto es muy útil para escalar aplicaciones web, como una aplicación web con microservicios. Hoy en día, la arquitectura de una aplicación web moderna se parece a esto:

Todos estos servicios podrían ser el mismo servicio, que será redirigido por el balanceador de carga según el uso de recursos (CPU o uso de memoria) de cada servidor, o algunos servicios diferentes como autenticación, etc.

Si usamos métodos de autorización tradicionales, como las cookies, tendremos que compartir una base de datos, como Redis, para compartir la información compleja entre servidores o servicios internos. Pero si compartimos el secreto a través de los microservicios, podemos usar JWT y luego no se necesitan otros recursos externos para autorizar a los usuarios.

Usando JWT con Express

En este tutorial, crearemos una aplicación web simple basada en microservicios para administrar libros en una biblioteca con dos servicios. Un servicio será responsable de la autenticación de usuarios y el otro será responsable de la gestión de libros.

Habrá dos tipos de usuarios: administradores y miembros. Los administradores podrán ver y agregar libros nuevos, mientras que los miembros solo podrán verlos. Idealmente, también podrían editar o eliminar libros. Pero para mantener este artículo lo más simple posible, no entraremos en muchos detalles.

Para comenzar, en tu terminal inicializa un proyecto Node.js vacío con la configuración predeterminada:

$ npm init -y

Luego, instalemos el marco Express:

$ npm install --save express

Servicio de autenticación

Entonces, creemos un archivo llamado auth.js, que será nuestro servicio de autenticación:

const express = require('express');
const app = express();

app.listen(3000, () => {
    console.log('Authentication service started on port 3000');
});

Idealmente, deberíamos utilizar una base de datos para almacenar la información del usuario. Pero para mantenerlo simple, creemos una matriz de usuarios, que usaremos para autenticarlos.

Para cada usuario, habrá un rol: admin o member adjunto a su objeto de usuario. Además, recuerde aplicar hash a la contraseña si se encuentra en un entorno de producción:

const users = [
    {
        username: 'john',
        password: 'password123admin',
        role: 'admin'
    }, {
        username: 'anna',
        password: 'password123member',
        role: 'member'
    }
];

Ahora podemos crear un controlador de solicitudes para el inicio de sesión del usuario. Instalemos el jsonwebtoken módulo, que se utiliza para generar y verificar tokens JWT.

Además, instalemos el body-parser middleware para analizar el cuerpo JSON de la solicitud HTTP:

$ npm i --save body-parser jsonwebtoken

Ahora, vamos a estos módulos y configurémoslos en la aplicación Express:

const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

app.use(bodyParser.json());

Ahora podemos crear un controlador de solicitudes para manejar la solicitud de inicio de sesión del usuario:

const accessTokenSecret="youraccesstokensecret";

Este es tu secreto para firmar el token JWT. Nunca debe compartir este secreto, de lo contrario, un mal actor podría usarlo para falsificar tokens JWT para obtener acceso no autorizado a su servicio. Cuanto más complejo sea este token de acceso, más segura será su aplicación. Así que intente usar una cadena aleatoria compleja para este token:

app.post('/login', (req, res) => {
    // Read username and password from request body
    const { username, password } = req.body;

    // Filter user from the users array by username and password
    const user = users.find(u => { return u.username === username && u.password === password });

    if (user) {
        // Generate an access token
        const accessToken = jwt.sign({ username: user.username,  role: user.role }, accessTokenSecret);

        res.json({
            accessToken
        });
    } else {
        res.send('Username or password incorrect');
    }
});

En este controlador, hemos buscado un usuario que coincida con el nombre de usuario y la contraseña en el cuerpo de la solicitud. Luego hemos generado un token de acceso con un objeto JSON con el nombre de usuario y el rol del usuario.

Nuestro servicio de autenticación está listo. Arranquemos ejecutando:

$ node auth.js

Una vez que el servicio de autenticación esté funcionando, enviemos una solicitud POST y veamos si funciona.

Usaré el resto-cliente Insomnio para hacer esto. Siéntase libre de usar cualquier cliente de descanso que prefiera o algo como Postman para hacer esto.

Enviemos una solicitud de publicación al http://localhost:3000/login punto final con el siguiente JSON:

{
    "username": "john",
    "password": "password123admin"
}

Debería obtener el token de acceso como respuesta:

{
  "accessToken": "eyJhbGciOiJIUz..."
}

Servicio de libros

Una vez hecho esto, creemos un books.js Solicite nuestro servicio de libros.

Comenzaremos con el archivo importando las bibliotecas necesarias y configurando la aplicación Express:

const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');

const app = express();

app.use(bodyParser.json());

app.listen(4000, () => {
    console.log('Books service started on port 4000');
});

Después de la configuración, para simular una base de datos, creemos una matriz de libros:

const books = [
    {
        "author": "Chinua Achebe",
        "country": "Nigeria",
        "language": "English",
        "pages": 209,
        "title": "Things Fall Apart",
        "year": 1958
    },
    {
        "author": "Hans Christian Andersen",
        "country": "Denmark",
        "language": "Danish",
        "pages": 784,
        "title": "Fairy tales",
        "year": 1836
    },
    {
        "author": "Dante Alighieri",
        "country": "Italy",
        "language": "Italian",
        "pages": 928,
        "title": "The Divine Comedy",
        "year": 1315
    },
];

Ahora, podemos crear un controlador de solicitudes muy simple para recuperar todos los libros de la base de datos:

app.get('/books', (req, res) => {
    res.json(books);
});

Porque nuestros libros solo deberían ser visibles para usuarios autenticados. Tenemos que crear un middleware para la autenticación.

Antes de eso, cree el secreto del token de acceso para la firma de JWT, como antes:

const accessTokenSecret="youraccesstokensecret";

Este token debe ser el mismo que se usa en el servicio de autenticación. Debido al hecho de que el secreto es compartido entre ellos, podemos autenticarnos usando el servicio de autenticación y luego autorizar a los usuarios en el servicio de libros.

En este punto, creemos el middleware Express que maneja el proceso de autenticación:

const authenticateJWT = (req, res, next) => {
    const authHeader = req.headers.authorization;

    if (authHeader) {
        const token = authHeader.split(' ')[1];

        jwt.verify(token, accessTokenSecret, (err, user) => {
            if (err) {
                return res.sendStatus(403);
            }

            req.user = user;
            next();
        });
    } else {
        res.sendStatus(401);
    }
};

En este middleware, leemos el valor del encabezado de autorización. Desde el authorization encabezado tiene un valor en el formato de Bearer [JWT_TOKEN], hemos dividido el valor por el espacio y separado el token.

Entonces hemos verificado el token con JWT. Una vez verificado, adjuntamos el user objetar en la solicitud y continuar. De lo contrario, enviaremos un error al cliente.

Podemos configurar este middleware en nuestro controlador de solicitudes GET, así:

app.get('/books', authenticateJWT, (req, res) => {
    res.json(books);
});

Arranquemos el servidor y probemos si todo funciona correctamente:

$ node books.js

Ahora podemos enviar una solicitud al http://localhost:4000/books endpoint para recuperar todos los libros de la base de datos.

Asegúrese de cambiar el encabezado “Autorización” para que contenga el valor “Portador [JWT_TOKEN]”, como se muestra en la siguiente imagen:

Finalmente, podemos crear nuestro controlador de solicitudes para crear un libro. Porque solo un admin puede agregar un nuevo libro, en este controlador también tenemos que verificar el rol del usuario.

También podemos usar el middleware de autenticación que hemos usado anteriormente en esto:

app.post('/books', authenticateJWT, (req, res) => {
    const { role } = req.user;

    if (role !== 'admin') {
        return res.sendStatus(403);
    }


    const book = req.body;
    books.push(book);

    res.send('Book added successfully');
});

Dado que el middleware de autenticación vincula al usuario con la solicitud, podemos obtener el role desde el req.user objeto y simplemente verifique si el usuario es un admin. Si es así, se agrega el libro; de lo contrario, se arroja un error.

Probemos esto con nuestro cliente REST. Inicie sesión como admin usuario (utilizando el mismo método que el anterior) y luego copie el accessToken y enviarlo con el Authorization encabezado como hemos hecho en el ejemplo anterior.

Entonces podemos enviar una solicitud POST al http://localhost:4000/books punto final:

{
    "author": "Jane Austen",
    "country": "United Kingdom",
    "language": "English",
    "pages": 226,
    "title": "Pride and Prejudice",
    "year": 1813
}

Actualización de token

En este punto, nuestra aplicación maneja tanto la autenticación como la autorización para el servicio de libros, aunque hay una falla importante en el diseño: el token JWT nunca caduca.

Si roban este token, tendrán acceso a la cuenta para siempre y el usuario real no podrá revocar el acceso.

Para eliminar esta posibilidad, actualice nuestro controlador de solicitud de inicio de sesión para que el token caduque después de un período específico. Podemos hacer esto pasando el expiresIn propiedad como una opción para firmar el JWT.

Cuando vencemos un token, también deberíamos tener una estrategia para generar uno nuevo, en caso de vencimiento. Para hacer eso, crearemos un token JWT separado, llamado token de actualización, que se puede usar para generar uno nuevo.

Primero, cree un secreto de token de actualización y una matriz vacía para almacenar tokens de actualización:

const refreshTokenSecret="yourrefreshtokensecrethere";
const refreshTokens = [];

Cuando un usuario inicia sesión, en lugar de generar un solo token, genera tokens de actualización y autenticación:

app.post('/login', (req, res) => {
    // read username and password from request body
    const { username, password } = req.body;

    // filter user from the users array by username and password
    const user = users.find(u => { return u.username === username && u.password === password });

    if (user) {
        // generate an access token
        const accessToken = jwt.sign({ username: user.username, role: user.role }, accessTokenSecret, { expiresIn: '20m' });
        const refreshToken = jwt.sign({ username: user.username, role: user.role }, refreshTokenSecret);

        refreshTokens.push(refreshToken);

        res.json({
            accessToken,
            refreshToken
        });
    } else {
        res.send('Username or password incorrect');
    }
});

Y ahora, creemos un controlador de solicitudes que generó nuevos tokens basados ​​en los tokens de actualización:

app.post('/token', (req, res) => {
    const { token } = req.body;

    if (!token) {
        return res.sendStatus(401);
    }

    if (!refreshTokens.includes(token)) {
        return res.sendStatus(403);
    }

    jwt.verify(token, refreshTokenSecret, (err, user) => {
        if (err) {
            return res.sendStatus(403);
        }

        const accessToken = jwt.sign({ username: user.username, role: user.role }, accessTokenSecret, { expiresIn: '20m' });

        res.json({
            accessToken
        });
    });
});

Pero también hay un problema con esto. Si le roban el token de actualización al usuario, alguien puede usarlo para generar tantos tokens nuevos como desee.

Para evitar esto, implementemos un sencillo logout función:

app.post('/logout', (req, res) => {
    const { token } = req.body;
    refreshTokens = refreshTokens.filter(token => t !== token);

    res.send("Logout successful");
});

Cuando el usuario solicite cerrar la sesión, eliminaremos el token de actualización de nuestra matriz. Se asegura de que cuando el usuario cierre la sesión, nadie podrá usar el token de actualización para generar un nuevo token de autenticación.

Conclusión

En este artículo, le presentamos JWT y cómo implementar JWT con Express. Espero que ahora tenga un buen conocimiento sobre cómo funciona JWT y cómo implementarlo en su proyecto.

Como siempre, el código fuente está disponible en GitHub.

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad