Auth con JWT y Express.js

jwt

La mayoría de las aplicaciones que utilizamos manejan autentificación. Muchas de estas aplicaciones ofrecen acceso por medio de aplicaciones de terceros como Facebook, Google, Twitter, este tipo de autentificación se conoce como OAuth, o el fiel combo correo-contraseña. Porque menciono esto, estas aplicaciones comparten la misma estrategia o método de verificar al usuario con el servidor que desean consultar, este método se conoce como JWT.

JWT (JSON Web Tokens) es un estándar de autentificación, que se encuentra estandarizado por el IETF en RFC7519, que tiene como función identificar al usuario que solicita peticiones a nuestro servidor. Es muy diferente a una session/cookie, el token encriptado del usuario se envía por cada petición que el cliente haga, y el servidor se encarga de verificar el token que recibió.

Los JWT están conformados por un header, un payload, y un signature.

  • Header muestra el algoritmo de hash que se utilizó.
  • Payload contiene información pública y privada del token (fecha de expiración, fecha de creación, información del usuario).
  • Signature es para verificar si el cliente es quien dice ser.

Ejemplo de un JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

La estrategia que nosotros utilizaremos para la verificar las peticiones a nuestro servidor será verificar el JWT que nos envíe nuestro cliente por el HTTP Authorization Header, así el cliente podrá acceder a los recursos que ofrezca nuestro servidor.

Express es un web framework de Node.js que nos ayudará a crear un REST API básico y rápido, con middleware y rutas, para que nuestros clientes la consuman.

Bien! Ahora que tenemos un concepto claro de que hace cada herramienta, tenemos que planear y crear los flujos de nuestra API.

Primero crearemos una carpeta donde contenga nuestra aplicación, crearemos un archivo de extensión js e instalaremos nuestras dependencias:

mkdir jwt-pass && cd jwt-pass
touch index.js usuarios.json .env
npm init -y
npm i -S dotenv uuid express cors bcryptjs jsonwebtoken body-parser

 

Después modificaremos el .env con variables de entorno para nuestra aplicación:

HOST=0.0.0.0

PORT=3000

JWT_SECRET=cruelcruelworldmustIgooncruelcruelworldImmovingon

Después tenemos que modificar el archivo base de la aplicación, el index.js. Iniciaremos con crear el servidor:

require('dotenv').config();
const fs = require('fs');
const http = require('http');
const cors = require('cors');
const uuidv4 = require('uuid/v4');
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const salt = 10;
const host = process.env.HOST || '127.0.0.1';
const port = process.env.PORT || 3000;
const secret = process.env.JWT_SECRET || 'NeverTellYourSecrets';
const app = express();
const server = http.createServer(app);

/**
* Utils
*/

/** ************ */

/**
* Middlewares
*/
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: false,
}));
/** ************ */

/**
* Routes
*/
app.get('/info', (req, res) => res.json('Hiya buddy!'));
/** ************ */

server.listen(port, host, () => {
console.log(`jwt-pass app listening on ${host}:${port}`);
});

Para “encender” el servidor basta con escribir el siguiente comando:

node index.js

Para consumir nuestro servidor necesitaremos un REST client para hacer pruebas, yo utilizaré Insomnia, pero también pueden usar Postman o cualquier otro con el que se sientan cómodos.

Basta con solo ingresar el host y el puerto que está sirviendo, y la ruta que queremos consultar y listo!

Ya tenemos un servidor web en Express funcionando, ahora solo nos falta crear registrar usuarios, e iniciar sesión.

Nosotros estaremos registrando a los usuarios en nuestro archivo usuarios.json, solamente para este ejercicio, ya en práctica es recomendable tener esos datos persistentes en una base de datos.

Dentro del bloque Utils tenemos que declarar una función que lea el contenido del archivo usuarios.json y lo parsee a objetos, otra función que modifique el contenido del archivo usuarios.json, y otra función que haga un hash en la contraseña del cliente (por motivos de seguridad).

/**
 * Utils
 */
let listaUsuarios = (() => {
   try {
       return JSON.parse(fs.readFileSync('./usuarios.json', 'utf8'));
   } catch (error) {
       return [];
   }
})();
 
const updateUsuarios = obj => fs.writeFileSync('./usuarios.json', JSON.stringify(obj), 'utf8');
 
const hashPassword = async (pwd) => {
    try {
        return await bcrypt.hash(pwd, salt);
    } catch (error) {
        throw error;
    }
};
/** ************ */

Después crearemos una nueva ruta de verbo POST en el bloque Routes, esta ruta la nombraremos “/signup”, y tendrá como función registrar usuarios:

app.post('/signup', async (req, res) => {
    try {
        const {
            nombre,
            apellido,
            email = undefined,
        } = req.body;
        let { password = undefined } = req.body;
 
        if (!email || !password) {
            return res.status(400).json({
                message: 'debe de ingresar un email y password para registrarse',
            });
        }
 
        const emailUsed = listaUsuarios.filter(=> u.email === email);
        if (emailUsed.length > 0) {
            return res.status(400).json({
                message: 'email ya está siendo utilizado',
            });
        }
 
        password = await hashPassword(password);
 
        listaUsuarios = [
            ...listaUsuarios,
            {
                id: uuidv4(),
                nombre,
                apellido,
                email,
                password,
                activo: true,
                createdAt: new Date(),
            }
        ];
        updateUsuarios(listaUsuarios);
 
        res.status(201).json('Registro exitoso!');
    } catch (error) {
        console.log(error);
        res.status(500).json(error);
    }
});

En esta ruta estamos verificando si el email que se ingresó ya está siendo utilizado por alguien más, y si el usuario nos envío un email y password como mínimo para registrarlo.

Después crearemos otra una nueva ruta de verbo POST en el bloque Routes, esta ruta la nombraremos “/login”, y tendrá como función entregar un JWT válido al usuario que lo solicite:

app.post('/login', async (req, res) => {
    try {
        const { email, password } = req.body;
        const [usuario] = listaUsuarios.filter(=> u.email === email);
 
        if (!usuario) {
            return res.status(404).json({
                message: 'email no fue encontrado',
            });
        }
        if (usuario.activo == false) {
            return res.status(400).json({
                message: 'usuario se encuentra desactivado',
            });
        }
 
        const match = await comparePassword(password, usuario.password);
        if (!match) {
            return res.status(400).json({
                message: 'password incorrecto',
            });
        }
        const tokenTTL = `${1000 * 60 * 60 * 1}ms`; // 1 hora
        const token = generateJWT({
            id: usuario.id,
            nombre: usuario.nombre,
            apellido: usuario.apellido,
            email,
        }, tokenTTL);
        res.json({ token });
    } catch (error) {
        console.log(error);
        res.status(500).json(error);
    }
});

Antes de consultar esta ruta, debemos de agregar 2 nuevas funciones dentro del bloque Utils:

const comparePassword = async (pwd, hash) => {
    try {
        return await bcrypt.compare(pwd, hash);
    } catch (error) {
        throw error;
    }
};
 
const generateJWT = (payload, expiresIn) => jwt.sign(payload, secret, { expiresIn });

 

En esta ruta estamos verificando si existe un usuario registrado con ese email, si ese usuario aún sigue activo, y si la contraseña que se ingresó hace match con el hash de la contraseña que se guardó. Si cumple con las demandas, se le genera y entrega un JWT (con tiempo de expiración de 1 hora) al usuario.

Nuesto web server con JWT ya esta funcionando, pero como podemos probarlo? Primero tenemos que agregar 2 funciones mas, 1 en el bloque de Utils:

const verifyJWT = token => jwt.verify(token, secret);

 

y la otra en el bloque de Middlewares:

const checkJWT = (req, res, next) => {
    try {
        const { authorization } = req.headers;
        const decoded = verifyJWT(authorization.split(' ')[1]);
        const [usuario] = listaUsuarios.filter(=> u.id === decoded.id);
        if (!usuario || !usuario.activo) {
            throw Error();
        }
        req.usuario = usuario;
        next();
    } catch (error) {
        return res.status(401).send({ message: 'Unauthorized' });
    }
};

 

Ahora solo nos queda modificar la ruta GET “/info” de nuestro web server:

app.get('/info', checkJWT, (req, res) => res.json(req.usuario));

 

Lo que cambio en esta es que solo se puede consultar si el cliente envia un JWT valido en el Header Authorization.

Authorization: Bearer <jwt que entrega el servidor>

Para probar esta ruta intentemos consultarla sin cargar el header, nos debe de regresar un HTTP code 401. Ahora si elegimos el método de auth Bearer Token en Insomnia, y cargamos el token que nos entregó el server (y que aún no haya expirado), al hacer la petición nos debe de entregar nuestra información registrada en usuarios.json.

Si gustan ver que header, payload y signature contiene su token, pueden debbugearlo en la pagina oficial de JWT

Cualquier duda o sugerencia, me la pueden hacer saber, aquí les dejo un gist del proyecto. Hasta la próxima, vaqueros!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *