Express, parte 4: Token de autenticación

En esta publicación vamos a continuar con nuestra api, agregando la seguridad tanto en las contraseñas, como en los accesos de los usuarios a los endpoints.

bcrypt

La dependencia bcrypt nos servirá para trabajar con el cifrado de contraseñas.

Debemos incluirla:

//Encriptador.
const bcrypt = require('bcrypt');

Podemos encriptar una contraseña. Es decir la cadena de texto original a un hash para proteger la misma:

const contrasena_crypt = await bcrypt.hash(contrasena, 10);

Y también verificar si una contraseña coincide con un hash. Verificar si la contraseña original del usuario coincide con el hash:

await bcrypt.compare(contrasena, contrasena_crypt);

Access Token

  • Autentica y autoriza solicitudes protegidas por un API
  • Tienen una duración limitada (Nosotros debemos indicarla)
  • Es información encriptada que será desencriptada por el servidor, el cual contiene la clave secreta para hacer esto
  • Los token deben enviarse en la cabecera de la petición (Authorization: Bearer + hash )

JST (Json web token)

Trabajo con los tokens de autenticación.

Playload

Contiene los datos que se van a encriptar. Esto se realiza en el servidor y está oculto en el cliente, quien sólo tendrá acceso al hash.

Secret key

Es el algoritmo que se utiliza para realizar lo anterior.

En todo caso, vamos a tener que agregar una variable nueva al archivo .env, que actualmente se ve de esta forma:

DB_HOST=host
DB_USER=user
DB_PASSWORD=password
DB_NAME=nombre_de_la_base_de_datos

Y agregar una variable nueva:

DB_HOST=host
DB_USER=user
DB_PASSWORD=password
DB_NAME=nombre_de_la_base_de_datos
JWT_ACCESS_TOKEN_SECRET=tu_access_token_secret

(Recordar cambiar los valores por tus valores de configuración. Si no sabés qué poner JWT_ACCESS_TOKEN_SECRET, podés ingresar uno de forma aleatorio: https://it-tools.tech/token-generator)

Encriptar información:

//Información que se va a hashear.
const playload = {
    "name": "Cornelio del Rancho",
    "email": "cornelio.delrancho@test.com",
    "is_admin": true
};
//Access token.
const accessToken = jwt.sign(playload, process.env.JWT_ACCESS_TOKEN_SECRET, { expiresIn: '15m' });

Desencriptar información:

jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET);

Ejemplo

Dentro de src/models vamos a crear nuestro modelo usuarioModel.js:

//Conexión a la base de datos.
const connection = require('../../db');
//Helper para fechas
const {formatToday} = require('../helpers/dateHelper');
//Encriptador.
const bcrypt = require('bcrypt');

exports.create = async( {nombre, email, contrasena, is_admin} ) => {
    const contrasena_crypt = await bcrypt.hash(contrasena, 10);
    const query = `
        INSERT INTO usuarios(nombre, email, contrasena, is_admin, fecha_creacion, fecha_modificacion)
        VALUES(?, ?, ?, ?, ?, ?)
    `;
    try{
        await connection.query(query, [nombre, email, contrasena_crypt, (is_admin ? 1 : 0), formatToday(), formatToday()]);
    }catch(error){
        throw error;
    }
}

exports.login = async( {email, contrasena} ) => {
    //Buscamos el usuario por su correo.
    const query = `
        SELECT id, nombre, email, contrasena, is_admin
        FROM usuarios
        WHERE email = ?
    `;
    try{
        [results] = await connection.query(query, [email]);
        //Verificamos si encontró el usuario.
        if(results.length == 1){
            const usuario = results[0];
            //Verificamos que la contraseña ingresada es correcta.
            const is_contrasena = await bcrypt.compare(contrasena, usuario.contrasena);
            return (is_contrasena) ? usuario : null;
        }else{
            return null;
        }
    }catch(error){
        throw error;
    }
}

(create() sirve para crear un usuario nuevo con la contraseña encriptada. login() por su parte comprueba que la combinación de email y contraseña coincidan con algún usuario)

En src/controllers creamos el controlador usuarioController.js:

const usuarioModel = require("../models/usuarioModel");
//Jason Web Token
const jwt = require('jsonwebtoken');

exports.register = async(req, res) => {
    const {nombre, email, contrasena} = req.body;
    const is_admin = false;
    try{
        await usuarioModel.create( {nombre, email, contrasena, is_admin} );
        res.json({ success: true, message: 'Usuario registrado correctamente'});
    }catch(error){
        console.log(error);
        res.status(500).json({ success: false, message: 'Error al intentar registrar al usuario' });
    }
}

exports.login = async(req, res) => {
    const {email, contrasena} = req.body;
    try{
        const usuario = await usuarioModel.login( {email, contrasena} );
        if(usuario == null){
            res.json({ success: false, message: 'Credenciales incorrectas' });
        }else{
            //Información que se va a hashear.
            const playload = { ID: usuario.id, nombre: usuario.nombre, is_admin: (usuario.is_admin == '1') };
            //Access token.
            const accessToken = jwt.sign(playload, process.env.JWT_ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
            res.json({
                success: true,
                message: 'Inicio de sesión exitoso',
                nombre: usuario.nombre,
                accessToken
            });
        }
    }catch(error){
        console.log(error);
        res.status(500).json({ success: false, message: 'Error al intentar iniciar sesión' });
    }
}

//Ruta protegida
exports.welcome = (req, res) => {
    res.json({ success: true, message: 'Bienvenida/o ' + req.user.nombre });
}

Middleware de autenticación

Un middleware Permite inspeccionar y filtrar peticiones HTTP. Ya veremos más abajo cómo.

Por ejemplo para verificar si el visitante tiene permiso de acceso a un determinado endpoint.

Podemos crear un directorio nuevo dentro de src, con el nombre middlewares, y dentro de este último auth.js:

const jwt = require('jsonwebtoken');
exports.requireAuth = (req, res, next) => {
    const authHeader = req.headers.authorization;
    if (!authHeader) {
        return res.status(401).json({ success: false, message: 'Token de autenticación no proporcionado' });
    }
    // El valor del encabezado de autorización debe tener el formato "Bearer tu_token_jwt_aqui"
    const [bearer, token] = authHeader.split(' ');
    if (bearer !== 'Bearer' || !token) {
        return res.status(401).json({ success: false, message: 'Formato de token no válido' });
    }
    try{
        const decodedToken = jwt.verify(token, process.env.JWT_ACCESS_TOKEN_SECRET);
        req.user = decodedToken; // Agregar información del usuario a la solicitud
        next();
    }catch(error){
        return res.status(401).json({ success: false, message: 'Token de autenticación inválido' });
    }
};

Por último vamos a crear dentro de src/routes usuarioRoutes.js:

const express = require('express');
const router = express.Router();
const { requireAuth } = require("../middlewares/auth");

//Controlador
const usuarioController = require("../controllers/usuarioController");

router.post("/register", usuarioController.register);
router.post("/login", usuarioController.login);
//Ruta protegida por el middleware de autenticación
router.get("/welcome", requireAuth, usuarioController.welcome);

module.exports = router;

Notar que la siguiente línea:

router.get("/welcome", requireAuth, usuarioController.welcome);

Recibe un argumento adicional, justamente el middleware que va a verificar si quien accede tiene permiso.

Finalmente agregamos el ruteador a nuestro archivo app.js:

app.use(require('./src/routes/usuarioRoutes'));

Probando desde Postman

A continuación vamos a probar los tres endpoints que acabamos de crear.

Crear usuario

Ingresamos el nombre del usuario, su email y contraseña:

Login

Ingresamos el email y la contraseña del usuario que acabamos de crear:

Si los datos son incorrectos debería devolvernos un objeto con un formato similar:

{
    "success": true,
    "message": "Inicio de sesión exitoso",
    "nombre": "Miko",
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJJRCI6MSwibm9tYnJlIjoiTWlrbyIsImlzX2FkbWluIjpmYWxzZSwiaWF0IjoxNzM2NTE5OTg1LCJleHAiOjE3MzY1MjA4ODV9.VSy1WbTSi7P9rigXs2ILBQhtbIEdOXMVE2xwsVNfGFs"
}

Notar que la propiedad accessToken nos ha devuelto un hash. Entonces vamos a guardar ese hash en alguna parte, porque vamos a tener que usarlo en el siguiente endpoint.

Validación de usuario autenticado

Finalmente vamos a probar el endpoint welcome, el cual sólo puede ser accedido con un token de autorización:

El access token se envía en las cabeceras de la petición. Como Authorization y con el valor concatenado de «Bearer token» (modificar token, por el que te haya devuelto anteriormente)

Algo importante a resaltar es que este token sólo tiene una duración de 15 minutos. Que es la duración que le dimos en el código:

//Access token.
const accessToken = jwt.sign(playload, process.env.JWT_ACCESS_TOKEN_SECRET, { expiresIn: '15m' });

Podemos aumentar el tiempo, aunque no mucho, ya que esto no es recomendable, teniendo en cuenta que si alguien se hiciera con las credenciales del usuario podría hacerse pasar por este. Cuanto menos tiempo tenga de duración, más seguro es el access token.

Una alternativa a esto es algo llamado refresh token, algo que no veremos en esta publicación.