API Rest de login con Express, MongoDB y JWT

El proceso de autenticación o login es uno de los más importantes y más usados en el desarrollo web, ya que muchas aplicaciones web lo necesitan para su operación. Dada su importancia y relativa complejidad, existen múltiples servicios que realizan este proceso, facilitándonos mucho el desarrollo. De entre estos servicios tenemos a Firebase Authentication, AWS Cognito y otros que los veremos en próximos tutoriales.

En este tutorial aprenderemos a crear desde cero un servicio Api REST de autenticación de usuario utilizando Express, MongoDB y JWT.

Índice

  1. Configuración del proyecto
  2. Probando Express
  3. Creando nuestras primera ruta
  4. Configurando Mongodb
  5. Creando el esquema y obteniendo los datos
  6. Validaciones
  7. Hash y la contraseña
  8. Ruta para el Login
  9. Utilizando JWT
  10. Validando el token
  11. Configurando los cors
  12. Tu turno

1. Configuración del proyecto

Comenzaremos creando un proyecto Nodejs usando npm, para lo cual usamos el comando npm init -y. Esto nos crea el package.json donde podremos registrar las dependencias que utilizaremos en nuestro proyecto. Las dependencias que utilizaremos en este proyecto son las siguientes.

# Framework nodejs para el backend
npm i express
# Librería para poder conectarnos a MongoDb
npm i mongoose
# Librería para encriptar texto, lo utilizaremos junto con JWT
npm i bcrypt
# Librería para usar variables de entorno
npm i dotenv
# Librería para usar JWT
npm i jsonwebtoken
# Librería para validar los strings que nos envíen
npm i @hapi/joi
# Middleware de Express para configurar los Cors
npm i cors

Además de esa lista de dependencias que utilizaremos en el proyecto hay una herramienta que nos será de gran utilidad, ya que nos permitirá reiniciar nuestro backend cada vez que hagamos cambios el proyecto y guardemos. Esta herramienta es Nodemon y lo debemos instalar de forma global, si es que no lo tenemos aún, para eso usamos el siguiente comando.

npm i -g nodemon

Luego de instalar todo esto, configuraremos algunos scripts para ejecutar nuestro proyecto, para esto agregamos el siguiente código en el archivo package.json de nuestro proyecto.

"scripts": {
    "dev": "nodemon index",
    "start": "node index"
},

Algo muy importante y que siempre recomiendo en un proyecto, por más que este sea de practica es utilizar Git, ya que es una herramienta imprescindible para todo desarrollador. Para esto solo usamos el comando git init. Además debemos agregar un archivo llamado .gitignore donde colocaremos las carpetas o archivos que no se deberían subir a un repositorio. Dentro colocaremos lo siguiente.

node_modules
.env

El primero contiene los archivos de instalación de nuestro proyecto, esos que cada vez que descarguemos nuestro proyecto podemos instalar fácilmente usando npm install o yarn. Mientras que el segundo contiene las variables de entorno, esas que nunca se deben compartir ni subir a un repositorio remoto, y deben quedar únicamente en el servidor.

Este último archivo, nos recuerda que aún no creamos nuestro archivo de variables de entorno, así que debemos crear un archivo llamado .env donde más adelante agregaremos nuestras variables de entorno.

Por último dentro de nuestro proyecto necesitaremos dos carpetas, la primera llamada routes donde definiremos las rutas de nuestro API, y el segundo llamado models, donde modelaremos las entidades fundamentales que almacenará nuestra base de datos.

2. Probando Express

Ahora que ya tenemos un proyecto configurado, comenzaremos creando un endpoint para comprobar que Nodejs con Express funcionen correctamente y sin problemas. Para esto creamos un archivo index.js donde utilizaremos el siguiente código.

const express = require("express");

const app = express();

app.get("/", (req, res) => {
  res.json({
    estado: true,
    mensaje: "Ya funciona!!",
  });
});

app.listen(8002, () => {
  console.log(`Tu servidor está corriendo en el puerto: 8002`);
});

Si obtenemos el siguiente resultado en Postman, la aplicación excelencia para probar APIs, es que nuestro primer endpoint ya funciona.

Sin embargo, el código que tenemos es muy ineficiente por lo que podemos simplificarlo utilizando las variables de entorno que están dentro de .env. Así que cambiamos el código al siguiente.

const express = require("express");
require("dotenv").config();

const app = express();

app.get("/", (req, res) => {
  res.json({
    estado: true,
    mensaje: "Ya funciona!!",
  });
});
const PORT = process.env.PORT || 8002;
app.listen(PORT, () => {
  console.log(`Tu servidor está corriendo en el puerto: ${PORT}`);
});

Y dentro de nuestro archivo .env debemos colocar el número del puerto de la siguiente manera.

PORT=8005

Entonces de esta manera probamos que Express esta corriendo correctamente y ya usamos las variables de entorno.

3. Creando nuestras primera ruta

La primera ruta que crearemos es la de registro, esta nos servirá para enviar el nombre de usuario así como la contraseña para poder luego almacenarla en MongoDB.

Para esta ruta crearemos un archivo auth.js dentro de nuestra carpeta routes. Dentro de ese archivo colocaremos el siguiente código.

const router = require("express").Router();

router.post("/register", async (req, res) => {
  res.json({
    error: null,
    data: "Acabas de enviarme las credenciales de un usuario",
  });
});

module.exports = router;

Expliquemos un poco este código.

  • Lo primero que hacemos es traernos el módulo Router() que nos permite crear rutas. Este lo almacenamos en una constante para utilizarla luego.
  • Lo siguiente que hacemos es crear una ruta de tipo POST donde pasamos como parámetro el nombre de la ruta seguido de la función que este ejecutará. Recuerda que la función que le pasemos debe tener dos parámetros req y res.
  • En este caso para probar nuestra ruta POST tomará el response res utilizará .json para crear un json y enviarlo de vuelta al usuario con el mensaje 'Acabas de enviarme las credenciales de un usuario'
  • Por último, exportamos el módulo que acabamos de crear utilizando el nombre router.

Dentro de nuestro archivo index.js también haremos algunos cambios y ahora el código nos quedará asi.

const express = require("express");
const authRoutes = require("./routes/auth.js");
require("dotenv").config();

const app = express();

app.use("/api/user", authRoutes);

app.get("/", (req, res) => {
  res.json({ mensaje: "My Auth Api Rest" });
});

const PORT = process.env.PORT || 8002;
app.listen(PORT, () => {
  console.log(`Tu servidor está corriendo en el puerto: ${PORT}`);
});

Como puedes ver a parte que redujimos el mensaje de respuesta a la ruta principal de nuestro Api Rest, también nos traemos el módulo que creamos en routes/auth.js. Luego hacemos uso de él utilizando app.use() que nos permite utilizarlo en Express con el proyecto.

Podrás notar que por ahora no enviamos datos por el POST, esto es porque primero quiero configurar MongoDB para que tome los datos que le envíe y los almacene.

4. Configurando Mongodb

Para este proyecto utilizaremos Mongo Atlas, que es la plataforma desde donde podemos administrar múltiples bases de datos de MongoDB.

Lo primero que debemos hacer es conectar nuestro proyecto y a algún cluster de MongoDB donde se almacenará la información de los usuarios. Para esto le damos click en Connect en el cluster que utilizaremos.

Lo siguiente es obtener el siguiente string de conexión, es este string que utilizaremos en nuestro proyecto para conectarnos a MongoDB.

Esta conexión la haremos desde nuestro archivo index.js donde agregaremos el siguiente código.

const mongoose = require("mongoose");

const uri = `mongodb+srv://${process.env.USER}:${process.env.PASSWORD}@free-cluster.saw1r.mongodb.net/${process.env.DBNAME}?retryWrites=true&w=majority`;
mongoose
  .connect(uri, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => {
    console.log("Conectado a la base de datos");
  })
  .catch((e) => {
    console.log("Database error", e);
  });

Expliquemos este código.

  • Este código primero se trae la dependencia de MongoDB llamada mongoose.
  • Lo siguiente que hacemos es crear una constante llamada uri donde almacenaremos el string de conexión. Ese que obtenemos en MongoAtlas. Además, aquí hacemos uso de nuestras variables de entorno para colocar las credenciales y el nombre de la base de datos.
  • Si deseas saber qué significa cada una de los parámetros que le pasamos a connect te sugiero revisar la documentación oficial de Mongoose.
  • Por último utilizamos la función connect de mongoose para conectarnos a la base de datos utilizando el uri. Este nos devuelve una promesa, por lo que debemos utilizar el then y catch.

Como estamos haciendo uso e otras variables de entorno entonces debemos agregar estas variables dentro de nuestro archivo .env. Obviamente nosotros debemos colocar las credenciales aquí.

USER=
PASSWORD=
DBNAME=

Para rellenar estas variables de entorno debemos crear un usuario nuevo dentro de Mongo Atlas. Para esto nos dirigimos a la sección de Database Access en la consola de Mongo Atlas y le damos click en Add new database user.

Y en la siguiente ventana debemos crear un user_name y un password, además que le podemos dar privilegios para permitirle acceder a una base de datos especifica. Esas credenciales serán las que colocaremos dentro de nuestro archivo .env.

Dentro de la variable de entorno DBNAME podemos colocar cualquier nombre ya que, si este no existe en nuestro cluster, este se creará automáticamente.

Si hiciste bien todo, dentro de nuestros logs de la consola obtendremos el mensaje de Conectado a la base de datos, que nos indica que ya se estableció conexión con la base de datos.

5. Creando el esquema y obteniendo los datos

Lo que haremos ahora es crear el esquema de datos que utilizaremos para almacenar la información de nuestros usuarios. Para esto nos vamos a la carpeta models y dentro creamos el archivo llamado user.js donde usaremos el siguiente código.

const mongoose = require("mongoose");

const userSchema = mongoose.Schema({
  name: {
    type: String,
    require: true,
    min: 6,
    max: 255,
  },
  email: {
    type: String,
    require: true,
    min: 6,
    max: 255,
  },
  password: {
    type: String,
    require: true,
    minlength: 6,
  },
  date: {
    type: Date,
    default: Date.now,
  },
});

module.exports = mongoose.model("User", userSchema);

Lo que hacemos aquí es diseñar cómo estarán estructurados los datos de los usuarios dentro de nuestra aplicación usando mongoose.Schema que sirve para crear esquemas para MongoDB. Luego los exportamos utilizando el método mongoose.model().

Lo siguiente que haremos será usar este modelo que creamos para enviar los datos por el método POST que creamos anteriormente y almacenarlos en mongoDB. Entonces dentro de nuestro código routes/auth.js debemos agregar lo siguiente.

const router = require("express").Router();
const User = require("../models/user");

router.post("/register", async (req, res) => {
  // Creamos el objeto usando el model que creaos anteriormente
  const user = new User({
    name: req.body.name,
    email: req.body.email,
    password: req.body.password,
  });
  // Usamos .save() del model para almacenar los datos en Mongo
  try {
    const savedUser = await user.save();
    res.json({
      error: null,
      data: savedUser,
    });
  } catch (error) {
    res.status(400).json({ error });
  }
});

module.exports = router;

En el código agregamos el modelo de datos que creamos dentro de la carpeta schema y lo utilizamos para crear un objeto con el require de nuestra petición POST, ya que este trae los datos que le enviamos al endpoint. luego de crear el objeto User utilizaremos save() para almacenar la información en MongoDB. Por esta razón usamos await y el try catch para capturar si es que sucede algún error al momento de guardar la información.

Lo siguiente es usar el response res para devolver una respuesta a nuestro usuario con los datos que MongoDB nos devuelve al guardar el usuario.

Dentro del archivo index.js también es necesario agregarle un par de líneas de código de que nos permiten leer el request tipo json que enviaremos por el POST.

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

Estos son Middleware, y funcionan como una capa en el endpoint para hacer alguna lógica en dicho endpoint. En este caso leer correctamente los objetos json que nos enviarán.

Y así ya sabemos cómo almacenar datos dentro de MongoDB, y ya podemos probar nuestro endpoint usando Postman enviando el siguiente json.

{
  "name": "prueba3",
  "email": "prueba3@prueba.com",
  "password": "123123"
}

6. Validaciones

Ahora utilizaremos la dependencia @hapi/joi para realizar las validaciones del email y password. Para esto solo debemos agregar el siguiente código a nuestro archivo routes/auth.js.

...
// Traemos la dependencia que vamos a utilizar
const Joi = require('@hapi/joi');

// Creamos un esquema de registro usando las validaciones de Joi
const schemaRegister = Joi.object({
    name: Joi.string().min(6).max(255).required(),
    email: Joi.string().min(6).max(255).required().email(),
    password: Joi.string().min(6).max(1024).required()
})

router.post('/register', async (req, res) => {

    // Dentro del método que invoca POST
    // Usaremos la propiedad error del objeto que nos entrega schemaRegister.validate()
    const { error } = schemaRegister.validate(req.body)

    // Si este error existe, aqui se termina la ejecución devolviedonos el error
    if (error) {
        return res.status(400).json(
            { error: error.details[0].message }
        )
    }

    // Otras validaciones

    const user = new User({ ...

Lo siguiente que tenemos que hacer es probar si el email que enviamos es único, es decir no existe otro dentro de nuestra base de datos. Esto lo hacemos debajo de la validación anterior.

const isEmailExist = await User.findOne({ email: req.body.email });
if (isEmailExist) {
  return res.status(400).json({ error: "Email ya registrado" });
}

El código no es difícil de entender, lo que hace es usar el modelo User creado con Mongoose para pedir a la base de datos un registro con el email que nos entregaron, asi si el email existe, devolveremos un status(400).

Con eso ya tenemos nuestra validación de datos de usuario y ya lo podemos probar usando POSTMAN.

7. Hash y la contraseña

En este punto es importante recordarles que jamás se almacena la contraseña en formato de texto plano dentro de la base de datos. La contraseña debe ser cifrada, de tal forma que muy pocas personas puedan acceder a ella y ver la contraseña de los usuarios. Para hacer esto utilizaremos un algoritmo de Hash mediante la dependencia que instalamos anteriormente llamada bcrypt dentro de nuestro archivo de registro de usuario auth.js.

Pero usar solo un algoritmo de hash no es suficiente, ya que un input igual nos devolverá un output igual, así que lo que necesitamos es realizar el hash utilizando un número aleatorio, de esta manera si un atacante tiene acceso a la base de datos de las contraseñas, no podrá descifrar las contraseñas. Por otro lado, el número aleatorio va incluido en el hash por lo que no solo bastará el hash para descifrar la contraseña. Este número aleatorio es entregado por la dependencia bcrypt y es conocido como salt.

Entonces utilizaremos esta dependencia dentro del archivo auth.js de la siguiente forma.

const salt = await bcrypt.genSalt(10);
const password = await bcrypt.hash(req.body.password, salt);

const user = new User({
  name: req.body.name,
  email: req.body.email,
  password: password,
});

Como ya explicamos, primero creamos el salt y luego el password usando el password enviado del usuario y el salt. Todo este código lo colocamos en la creación del objeto User y antes de enviarlo a la base de datos.

si probamos con POSTMAN veremos que ahora la contraseña de usuario se encripta y es enviada de ese modo a la base de datos.

7. Ruta para el Login

Ahora crearemos la ruta para el login de usuario. Esto lo podríamos hacer en una archivo diferente, pero por cuestiones de tiempo y sabiendo que esta es una demostración, lo haremos en el mismo archivo del registro. Así que crearemos un nuevo endpoint POST llamado /login.

// Esquema del login
const schemaLogin = Joi.object({
  email: Joi.string().min(6).max(255).required().email(),
  password: Joi.string().min(6).max(1024).required(),
});

// LOGIN
router.post("/login", async (req, res) => {
  // Validaciones de login
  const { error } = schemaLogin.validate(req.body);
  if (error) return res.status(400).json({ error: error.details[0].message });

  // Validaciond e existencia
  const user = await User.findOne({ email: req.body.email });
  if (!user) return res.status(400).json({ error: "Usuario no encontrado" });

  // Validacion de password en la base de datos
  const validPassword = await bcrypt.compare(req.body.password, user.password);
  if (!validPassword)
    return res.status(400).json({ error: "Constraseña invalida" });

  res.json({
    error: null,
    data: "bienvenido",
  });
});

El código es similar al de registro de usuario, pero además de las validaciones que utilizamos aquí validamos la contraseña usando el método bcrypt.compare al cual le entregamos la contraseña almacenada en la base de datos que traemos en la constante user y la contraseña que entregamos mediante el req de la petición.

Entonces ya podríamos probar este nuevo endpoint de login usando http://localhost:3001/api/user/login en Postman.

9. Utilizando JWT

Aquí utilizaremos JWT para enviar la información sobre el usuario, ojo que por este medio no debe ir la contraseña del usuario solo la información que identifique al usuario, tales como el Id o el Email. Este token lo utilizaremos para identificar al usuario y mantener la sesión activa de este dentro del sistema.

Entonces ahora procederemos a utilizar esto dentro de nuestro endpoint de Login, donde en lugar de solo enviarle un mensaje de bienvenida, también le enviaremos el token que generamos del usuario con JWT.

// Creando token
const token = jwt.sign(
  {
    name: user.name,
    id: user._id,
  },
  process.env.TOKEN_SECRET
);

// Colocando el token en el header y el cuerpo de la respuesta
res.header("auth-token", token).json({
  error: null,
  data: { token },
  message: "Bienvenido",
});

Es importante mencionar que el token necesita de una clave secreta, la que colocaremos dentro de nuestras variables de entorno con nombre de TOKEN_SECRET. Esa es la magia de un token, solo si el token trae la misma clave secreta podrá ser validada por el back. Así, si alguien malintencionado suplanta un token, no podrá hacerlo sin la clave secreta. Entonces si probamos nuestro endpoint con Postman obtendremos el siguiente resultado.

{
  "error": null,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiMTIzNDU2Nzg5IiwiaWQiOiI2MGFiMTEyODBjMzk2MTQwYjAxMjNjYTUiLCJpYXQiOjE2MjE4MjQxMTB9.q6bqF016E4EJjqOVgCoN4MSv_ros7IRac8ZTobkG_1o"
  },
  "message": "Bienvenido"
}

10. Validando el token

Dentro de un backend hay endpoints protegidas, es decir, rutas a las que solo se puede acceder con el token que obtenemos del inicio de sesión. Así que para estas rutas primero se debe verificar el token del request de la petición. Para esto crearemos un middleware que nos permitirá hacer esta verificación cuando accedamos a una determinada ruta. Esto lo haremos dentro de un archivo nuevo llamado validate-token.js dentro de /routes.

const jwt = require("jsonwebtoken");

const verifyToken = (req, res, next) => {
  // Obtenemos el token del header del request
  const token = req.header("auth-token");
  // Validamos si no hay token
  if (!token) return res.status(401).json({ error: "Acceso denegado" });
  try {
    // Verificamos el token usando la dependencia de jwt y el método .verify
    const verified = jwt.verify(token, process.env.TOKEN_SECRET);
    // si el token es correcto nos devolvera los datos que pusimos en el token
    req.user = verified;
    // next() indica que el req paso la prueba y continue su camino
    next();
  } catch (error) {
    res.status(400).json({ error: "Token no valido, acceso denegado" });
  }
};

module.exports = verifyToken;

Ahora para probar esto primero debemos crear una ruta protegida, para lo cual creamos un archivo dentro de routes llamado dashboard.js donde colocaremos el siguiente código.

const router = require("express").Router();

router.get("/", (req, res) => {
  res.json({
    error: null,
    data: {
      title: "Esta es una ruta protegida",
      user: req.user,
    },
  });
});

module.exports = router;

Como puedes ver tiene la estructura de una ruta cualquier. La magia o el código para verificar si se puede o no ingresar a esta ruta está dentro del archivo index.js.

// traemos los archivos necesarios
const dashboardRoutes = require('./routes/dashboard')
const verifyToken = require('./routes/validate-token')

...
app.use('/api/dashboard', verifyToken, dashboardRoutes)

Básicamente lo que hacemos es utilizar el verifytoken como un middleware dentro de la ruta protegida.

Entonces ya podemos probar esta ruta usando la http://localhost:8005/api/dashboard/ en Postman. Recuerda que para acceder a esta ruta debemos enviar el token por el header de la petición.

11. Configurando los cors

Esta configuración es necesaria ya que evitará que el navegador cierre la conexión entre cliente y servidor si es que estos dos se encuentran en diferentes lugares con Ips diferentes. Esto lo debemos configurar en index.js de la siguiente manera.

// Dependencia
const cors = require('cors');
...
// Creamos la variable de configuración
var corsOptions = {
    origin: '*', // Aqui debemos reemplazar el * por el dominio de nuestro front
    optionsSuccessStatus: 200 // Es necesario para navegadores antiguos o algunos SmartTVs
}
app.use(cors(corsOptions));

12. Tu turno

Listo!!, ya tenemos un backend de autenticación de usuario creado y listo para ser probado por algún front. Este es el repositorio del https://github.com/andygeek/backend-auth. Te animo a crear tu propia versión de este back.