Parte 1: Autenticación por medio de API con JWT, ExpressJS, ReactJS y React Native

En esta primera parte, desarrollaremos nuestra API Rest con ExpressJS, Mongoose y JWT

Edgar Talledos
Edgar Talledos
10 min readMar 17, 2020

--

Para aquellos que son nuevos leyendo alguno de mis artículos, primero déjenme aclararles algunos puntos.

  1. Prefiero ir directo a la práctica que tardarme en explicaciones teorícas.
  2. Pongo todo el código necesario para que lo haga por si mismo.
  3. En lugar de copiar y pegar te recomiendo que escribas el código y lo entiendas, eso te enseñará más que sólo clonar el proyecto e instalar dependencias.
  4. Te recomiendo instalar POSTMAN o INSOMNIA para realizar probar nuestra API.

Aclarados estos puntos, podemos comenzar.

JWT, ExpressJS y MongoDB

Introducción y división del proyecto

Ya tiene mucho tiempo que no escribo ningún artículo, espero este sea de mucha ayuda, porque será un artículo que lo dividiré en tres partes.

Parte 1. Desarrollaremos un API (Backend), añadiendo una base de datos en MongoDB (no se detallará a fondo la instalación de MongoDB, para eso les pido que consulten la documentación oficial, de acuerdo a su sistema operativo) para llevar el registro de nuestros usuarios.

Parte 2. Desarrollaremos un cliente en ReactJS (con create-react-app).

Parte 3. Desarrollaremos un cliente en React Native (con expo.io).

Desarrollo de API

Vamos a utilizar un proyecto de partida, un starter kit que he preparado especialmente para APIs REST con una arquitectura tipo MVC (pero sin las vistas, espero no sea muy cuestionable eso).

El starter kit ya tiene lo necesario para comenzar a trabajar, así es que no ahondaremos en detalles.

Probamos que todo funcione bien, ejecutando los siguientes comandos:

Antes ponía las instrucciones para utilizar yarn, sin embargo, ya no es tan necesario desde que npm alcanzó un nivel muy bueno de madurez.

$ npm install

Antes de iniciar nuestro servidor, necesitamos poner algunas variables de entorno, en la raíz de nuestro proyecto, se encuentra el archivo dotenv que nos indica que variables de entorno necesitamos.

Para el caso, este archivo tiene lo siguiente:

# .env MONGO_DB_URI_DEV='mongodb://localhost:27017/jwt-express-db'
PORT="8000"

Ahora ejecutamos el servidor con npm start pero si queremos que nuestro servidor se recargue cada que hagamos cambios, ejecutamos el siguiente comando:

$ npm run start:dev

Y nos tiene que salir la siguiente ventana:

Para probar que nuestra ruta está bien, vamos a abrir http://localhost:800/api/v1 en nuestro navegador favorito, y tenemos que tener los siguientes resultados:

Del lado izquierdo: Nuestro hola mundo, del lado derecho: nuestra terminal mostrándonos cómo se ejecuta la petición.

Perfecto, eso quiere decir que todo marcha de maravilla. Vamos a continuar con el siguiente paso.

Estructura de carpetas y desarrollo de API

Vamos a tener la siguiente estructura de carpetas.

Aquí podemos ver todas nuestras carpetas.

Como notas en la imagen, nuestra carpeta principal es src que contiene a su vez api , ahí es donde pondremos todo nuestro código.

Vamos a empezar creando nuestro modelo User y después vamos a crear nuestros controladores para users y nuestras rutas, todo esto utilizando la siguiente convención:

  1. Los modelos se nombran en singular, dependiendo del resource .
  2. Dentro de la carpeta controladores (controllers) se crear una nueva carpeta en plural (dependiendo del nombre de nuestro modelo), en este caso, users y dentro de esta carpeta tendremos cada una de nuestras actions , estas son, métodos que pertenecerán a un único endpoint de nuestra API.
  3. Las rutas (routes) se nombran en plural (dependiendo del nombre de nuestro modelo), en este caso también se nombra users y se guardan en la carpeta routes .
  4. Las pólizas (polices) son reglas de validación, que permitirán controlar el acceso a ciertos endpoints de nuestra API, es importante nombrarlas lo más explícito posible, por ejemplo, para verificar la identidad de un usuario, vamos a llamar a una póliza: is-logged-with-jwt que comprobará si un usuario ha iniciado sesión con jsonwebtoken .

Éstas convenciones nos facilitarán el entendimiento entre más desarrolladores y que el proyecto se desarrolle satisfactoriamente, facilitando el escalado.

Registro e inicio de sesión de usuarios

Creación del modelo User

Primero, vamos a instalar un paquete importante, este es el paquete validator , para validar nuestro modelo correctamente.

$ npm i -S validator

Vamos a crear nuestra archivo en src/api/models/user.js utilizando mongoose, que contendrá las siguiente:

./src/api/models/user.js

Nuestro usuario, podrá iniciar sesión por medio de un nombre de usuario, el email solo lo pedimos para tener un registro del usuario (por el momento no verificaremos a este usuario, en otro tutorial se hará la verificación por medio de un correo electrónico).

Creación de controlador para registro de usuarios

Ahora vamos a crear nuestro controlador en la siguiente ruta ./src/api/controllers/users/.. , primero creando un archivo index.js , que tendrá lo siguiente:

const signup = require('./signup');
const login = require('./login');
module.exports = { signup, login };

Esas rutas no existen, así es que ahora vamos a crear, esos archivos.

Necesitamos unas cuantas dependencias y explicar qué es lo que sucede con JWT.

Si en alguna ocasión has creado una aplicación con login, sabrás que no podemos exponer el password así como así, para eso existe un módulo de nodejs que se llama bcrypt , este nos servirá para encriptar nuestra contraseña y no exponerla al mundo exterior. Y aprovechamos para instalar jsonwebtoken que es lo que nos permitirá manejar nuestra sesión.

$ npm i -S jsonwebtoken bcrypt

Ahora cargamos nuestros módulos en ./src/api/controllers/users/signup.js y también jalamos nuestro modelo.

// Cargamos nuestros módulos
const Bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
// Añadimos dotenv para utilizar las variables de entorno
const dotenv = require('dotenv');
// Cargamos nuestro modelo
const User = require('../../models/user');
// Cargamos nuestras variables de entorno
dotenv.config();
module.exports = async (req, res) => {
// Aquí irá nuestra action
};

Para utilizar dotenv correctamente, vamos a necesitar un api-key y un tiempo de expiración para jwt, te sugiero que generes una en el siguiente sitio web.

Y la guardas como API_KEY y como TOKEN_EXPIRES_IN en tu archivo .env

# .env
...
API_KEY="tu-api-key-generada"
TOKEN_EXPIRES_IN="30d" # para que nos dure lo suficiente

Es importante que la guardes, porque la vamos a utilizar mucho.

Ahora si, vamos al código:

Vamos a pasar todos nuestros datos a través del body de la petición, para eso vamos a utilizar destructuring en los argumentos de nuestra función:

module.exports = async ({ body }, res) => {
// Aquí irá nuestra action
const { password, passwordConfirmation, email, username } = body;
...
};

Validamos que las contraseñas sean iguales, en caso contrario regresará un error 400, así como se muestra en el código, si las contraseñas coinciden, entonces intentaremos guardar nuestro usuario, con el password (utilizando bcrypt) encriptado, el correo electrónico (email) y el nombre de usuario (username).

if (password === passwordConfirmation) {
// Creamos una instancia para guardar el usuario
const newUser = User({
// Encriptamos el password, y ese password lo pasamos a la base de datos
password: Bcrypt.hashSync(password, 10),
email,
username,
});
...
}
// En caso de que las contraseñas no sean iguales
// retornamos un error 400 en la petición
return res.status(400).json({
status: 400,
message: '¡Las contraseñas no coinciden, intenta nuevamente!',
});

Si el todo salió bien, vamos a intentar guardar en la base de datos y ahí firmamos nuestro usuario utilizando jwt, y el API_KEY que ya hemos generado anteriormente.

Utilizando try, catch (debido a que son promesas async/await) en caso de haber un error al guardar, regresaremos un error 400, y en el cuerpo de la respuesta, el error en la propiedad message , si la petición se realizó de forma satisfactoria, entonces regresamos un 201, con el token como propiedad en un json.

// Si la instancia del model se ejecutó con éxito
// intentamos guardarlo en nuestra base de datos
// utilizando una promesa (async/await)
try {
// Guardamos el usuario
const savedUser = await newUser.save();
// Si el usuario se guardó con éxito, entonces
// regresamos el email, el id y el username, para firmarlo
// con jsonwebtoken
const token = jwt.sign(
{ email, id: savedUser.id, username },
process.env.API_KEY,
{ expiresIn: process.env.TOKEN_EXPIRES_IN },
);
// Cuando el usuario se firma, regresamos solamente el token
// ya que este contiene la información necesaria para en un
// futuro obtener todos los datos del usuario
return res.status(201).json({ token });
} catch (error) {
// En caso que no se haya realizado la petición con éxito al guardar
// regresamos un error 400 con el error en el "message" de la respuesta
return res.status(400).json({
status: 400,
message: error,
});
}

Perfecto, hemos terminado de crear nuestra action signup, les comparto como tiene que quedar todo el código en signup.

./src/api/controllers/users/signup.js

Ahora para probarlo, tenemos que crear nuestra ruta en ./src/api/routes/users.js que tendrá el siguiente código.

const express = require('express');
const router = express.Router();
// Cargamos nuestro controlador
const usersController = require('../controllers/users');
// Con el método POST, en el endpoint /signup
// podremos registrar a nuestro usuario
router.post('/signup', usersController.signup);
module.exports = router;

Lo que hicimos fue, cargar nuestro controlador, desde nuestro index, por eso lo podemos llamar cómo un método, y utilizar un método post sobre nuestro router para ejecutar ese controlador.

Ahora hay que agregar esa ruta a nuestro archivo principal: ./server.js

...
const mongoose = require('mongoose');
const cors = require('cors');
// Rutas
// Cargamos las rutas de nuestros usuarios
const usersRoutes = require('./src/api/routes/users');

Y llámamos a nuestra ruta en el endpoint -> /api/v1/users

...
// Agregamos la ruta de users a nuestra API
app.use('/api/v1/users', usersRoutes);
// Configuración de la base de datos
mongoose.set('useCreateIndex', true);
...

Para probar, simplemente en ./src/api/controllers/users/index.js vamos a tener que cambiar algunas cosas, ya después de probar y agregar el login, lo vamos a regresar todo a la normalidad.

const signup = require('./signup');
// const login = require('./login');
module.exports = { signup };

Ahora probamos en POSTMAN, que todo haya salido bien.

Como ven en la imagen, nos regresó el TOKEN
El usuario se creo de manera correcta, con el password encriptado, el email y el nombre de usuario que le pasamos

Los errores los pueden comprobar en POSTMAN, se los dejo al lector como tarea.

Todo salió perfecto, ahora solamente hay que terminar nuestra action login

Creación de controlador para inicio de sesión

Nuestro código va a ser casi el mismo que signup, así es que solamente modificaremos algunas cosas.

Primero, solamente necesitaremos ocupar un password y un nombre de usuario, entonces, en nuestro body, solamente destructuraremos ese valor.

Vamos a buscar nuestro usuario de acuerdo a su nombre de usuario (username) para validar que exista, si no existe, entonces regresamos un error 401 (esto es súper importante por cuestiones de seguridad) y no un 404, dado que no queremos que alguien con malas intenciones descubra realmente que ese usuario si se encuentra en nuestra base de datos.

El mensaje de error que le mandaremos a los usuarios, será el mismo que le mandamos al no poder acceder con la contraseña correcta, eso con el fin de mejorar la seguridad.

module.exports = async ({ body }, res) => {
// Pedimos solamente el password y el nombre de usuario
const { password, username } = body;
try {
// Realizamos una búsqueda para validar si el usuario existe
const userRecord = await User.findOne({ username });

// Si el usuario existe, procedemos
if (userRecord) {
...
}

return res.status(401).json({
status: 401,
message: '¡Tu email o contraseña son incorrectos, por favor, veríficalo!',
});
} catch (error) {
// Este error se genera si se procesa mal la solicitud
// en la base de datos
return res.status(400).json({
status: 400,
message: error,
});
}

Si el usuario existe, procedemos a comparar con Bcrypt, que el password que no está dando es el mismo que se encuentra encriptado en la base de datos.

En dado caso de ser el password correcto, entonces firmamos la petición, utilizando los datos del usuario en la base de datos, para al finalizar regresar el token.

if (userRecord) {
// Comparamos el password que nos está dando el usuario
// en el inicio de sesión, contra el password que
// tenemos guardado en la base de datos con Bcrypt
if (Bcrypt.compareSync(password, userRecord.password)) {
// En dado caso de ser correcto, entonces firmamos
// la petición con jsonwebtoken
const token = jwt.sign(
// Es importante que se note, que utilizamos el
// usuario que ya buscamos en la base de datos
// y el "_id" en vez de "id"
{ email: userRecord.email, id: userRecord._id, username },
process.env.API_KEY,
{ expiresIn: process.env.TOKEN_EXPIRES_IN },
);
// Regresamos el token para verificar que el usuario
// ha iniciado sesión correctamente
return res.status(200).json({ token });
}
}

Y listo, tenemos un login, ahora hay que añadirlo a nuestras rutas.

Recuerda primero modificar el index.js

const signup = require('./signup');
const login = require('./login');
module.exports = { signup, login };

Ahora el ./src/api/routes/users.js

# Añadimos
// Inicio de sesión de usuarios
router.post('/login', usersController.login);

El código completo debe quedar de la siguiente forma, para nuestra action login.

Eso quiere decir que todo debe funcionar correctamente, vamos a probar en POSTMAN, que todo haya salido bien.

Al realizar la petición, nos regresa el TOKEN

Todo ha salido muy bien, ahora ya tenemos nuestra API lista, para utilizarla en el cliente que deseemos, pero eso será hasta la Parte 2 de nuestro tutorial.

El repositorio del proyecto es el siguiente:

Si mi contenido te gusta, no olvides seguirme para más contenido o visitar mi sitio web.

--

--

Edgar Talledos
Edgar Talledos

La única manera de lidiar con este mundo sin libertad, es volverte tan absolutamente libre que tu mera existencia sea un acto de rebelión