
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
- Configuración del proyecto
- Probando Express
- Creando nuestras primera ruta
- Configurando Mongodb
- Creando el esquema y obteniendo los datos
- Validaciones
- Hash y la contraseña
- Ruta para el Login
- Utilizando JWT
- Validando el token
- Configurando los cors
- 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
yres
. - 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
demongoose
para conectarnos a la base de datos utilizando eluri
. Este nos devuelve una promesa, por lo que debemos utilizar elthen
ycatch
.
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.