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

    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 鈥嬧媏n 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.

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *