Implementación de Recuperación de Contraseña en API con NotMailer

Clase 16 de 20Curso de Backend con Node.js: Autenticación con Passport.js y JWT

Contenido del curso

Passport y JSON Web Tokens

Resumen

Centralizar la lógica de autenticación y conectar el envío de correos dentro de una API es un paso fundamental para construir sistemas seguros y mantenibles. Aquí se explica cómo crear un auth service que encapsule la verificación de usuarios, la firma de tokens y el envío de correos de recuperación de contraseña usando Nodemailer.

¿Por qué crear un auth service para centralizar la autenticación?

Cuando la lógica de autenticación está dispersa entre routers, strategies y endpoints, el código se vuelve difícil de mantener. La solución es crear un archivo dedicado llamado auth.service.js que agrupe todas las responsabilidades relacionadas con autenticación [0:45].

Este servicio contiene tres métodos principales:

  • getUser: recibe email y password, busca al usuario y compara contraseñas con bcrypt.
  • signToken: genera un JWT firmado a partir del usuario autenticado.
  • sendMail: valida que el usuario exista y envía el correo de recuperación.

javascript const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const nodemailer = require('nodemailer'); const { config } = require('../config/config');

class AuthService { constructor() { // se importa el servicio de usuarios }

async getUser(email, password) { const user = await this.userService.findByEmail(email); if (!user) { throw boom.unauthorized(); } const isMatch = await bcrypt.compare(password, user.password); if (!isMatch) { throw boom.unauthorized(); } delete user.password; return user; }

signToken(user) { const payload = { sub: user.id, role: user.role }; const token = jwt.sign(payload, config.jwtSecret); return { user, token }; } }

Al migrar esta lógica, el local strategy queda limpio: simplemente llama a authService.getUser(email, password) y retorna el resultado [2:18]. Los errores se lanzan con Boom, una librería que facilita respuestas HTTP estandarizadas usando throw boom.unauthorized().

¿Cómo se simplifica el login con este servicio?

En el router de autenticación, el endpoint de login ya no necesita contener la firma del token directamente. Solo se instancia el servicio y se llama a signToken [4:15]:

javascript const AuthService = require('../services/auth.service'); const service = new AuthService();

router.post('/login', passport.authenticate('local', { session: false }), (req, res) => { res.json(service.signToken(req.user)); } );

Como signToken es síncrono (no hay operaciones asíncronas), se puede resolver directamente sin await.

¿Cómo implementar el endpoint de recovery con Nodemailer?

Se crea una ruta POST /recovery que no requiere autenticación, ya que el usuario precisamente no puede ingresar [0:22]. Del cuerpo de la petición solo se extrae el email:

javascript router.post('/recovery', async (req, res) => { const { email } = req.body; const rta = await service.sendMail(email); res.json(rta); });

Dentro del método sendMail del auth service, se configura el transporter de Nodemailer y se realiza el envío [6:15]:

javascript async sendMail(email) { const user = await this.userService.findByEmail(email); if (!user) { throw boom.unauthorized(); } const transporter = nodemailer.createTransport({ host: 'smtp.gmail.com', port: 465, secure: true, auth: { user: 'tu_correo@gmail.com', pass: 'tu_password' } }); await transporter.sendMail({ from: 'tu_correo@gmail.com', to: user.email, subject: 'Recuperación de contraseña', html: '<b>Enlace de recuperación</b>' }); return { message: 'mail sent' }; }

¿Por qué validar el usuario antes de enviar el correo?

Antes de disparar el envío, se verifica que el email realmente exista en la base de datos [7:00]. Si no existe, se responde con un error genérico de no autorizado.

Esta práctica es crucial en seguridad: si respondieras "este correo no existe", un atacante podría realizar un ataque de fuerza bruta probando distintos correos hasta encontrar uno válido [8:50]. La respuesta correcta es siempre un mensaje genérico como unauthorized, sin dar contexto adicional.

¿Qué precauciones tomar con las credenciales del transporter?

Las credenciales de Gmail usadas en el transporter (usuario y contraseña) deben almacenarse como variables de entorno [10:05]. Dejarlas directamente en el código expone información sensible a cualquier persona con acceso al repositorio.

  • Usa un archivo .env para definir SMTP_USER y SMTP_PASS.
  • Accede a ellas mediante config o process.env.
  • Nunca subas credenciales al control de versiones.

Al probar en Insomnia con un email inexistente como xyz@gmail.com, el endpoint responde con error 401 [8:30]. Con un email válido registrado en la base de datos, el correo se envía correctamente y aparece en la bandeja de entrada del destinatario [9:30].

Con esta estructura, tienes un servicio reutilizable que separa responsabilidades y facilita agregar funcionalidades como la generación de un link de verificación para completar el flujo de recuperación de contraseña. ¿Ya tienes tus credenciales configuradas como variables de entorno?