No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Generación Segura de Links para Recuperación de Contraseña

17/20
Recursos

¿Cómo generar enlaces de recuperación de contraseña de forma segura?

Generar enlaces de recuperación de contraseña es crucial para garantizar la seguridad de tus usuarios. Vamos a ver cómo podemos hacer esto de manera eficiente y segura utilizando un método separado para enviar correos electrónicos, permitiéndonos reutilizar el código para otras funcionalidades.

¿Cómo separar responsabilidades en la gestión de correos electrónicos?

Para empezar, separamos la parte de envío de correos electrónicos en una función distinta para poder reutilizarla en diferentes casos de uso. Creamos un método llamado resetPassword que recibirá el email del usuario y ejecutará la lógica de envío de correo. Aquí es importante mantener las configuraciones de transporte, mientras que desacoplamos la lógica específica del correo.

¿Cómo funciona el proceso de creación del token de recuperación?

El proceso parte de la creación de un token usando JWT, que asegurará que el enlace sea único y seguro para cada usuario.

const payload = { sub: user.ID };
const token = generateToken(payload, secret);

El token generado deberá enviarse al usuario dentro de un enlace, el cual permitirá la recuperación de su contraseña en el front end cuando sea necesario.

¿Cómo integrar el enlace de recuperación en el correo electrónico?

Una vez generado el token, lo integramos en un enlace dentro del correo que permite al usuario acceder directamente al sistema de recuperación de contraseñas:

const link = `http://myfrontend.com/recovery?token=${token}`;

Es fundamental que el front end cuente con una vista que maneje este proceso de recuperación y capture el token proporcionado.

¿Cómo manejar la seguridad del token?

El token no solo debe ser único, sino que además debe almacenarse de manera segura en la base de datos. Se debe crear un campo adicional en la base de datos para almacenar este token, lo cual requiere una migración en una base de datos relacional:

await queryInterface.addColumn('User', 'recoveryToken', {
  type: DataTypes.STRING,
  allowNull: true,
});

¿Cómo se verifica y expira el token?

Este token debe configurarse para que expire automáticamente después de un tiempo definido (por ejemplo, 15 minutos), proporcionando un nivel adicional de seguridad y obligando al usuario a solicitar un nuevo enlace en caso de caducar.

¿Cómo preparar la base de datos para la recuperación de contraseñas?

Al ejecutar el comando de migración, agregamos un nuevo campo recoveryToken en la base de datos:

npx sequelize-cli db:migrate

Esto nos permitirá realizar un "match" con el token enviado al usuario para validar su autenticidad.

¿Qué hacer después de enviar el enlace de recuperación?

Una vez el usuario reciba el correo y acceda al enlace, el sistema de back end debe estar preparado para:

  1. Verificar la validez del token.
  2. Permitir al usuario cambiar su contraseña.
  3. Alertar a los responsables del front end para adecuar las vistas y funcionalidades necesarias.

La capacidad de desarrollar un sistema seguro y eficiente de recuperación de contraseñas es un aspecto crucial del desarrollo de software moderno. A través de separaciones lógicas y una estructura de gestión de tokens, se puede proporcionar a los usuarios una experiencia fluida y segura al recuperar sus credenciales.

Aportes 8

Preguntas 3

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

Se reacomoda un poco el servicio auth.service.js haciendo una separación de responsabilidades, se crea el método sendRecovery el cual contiene la lógica para generar un link para recuperar la contraseña y el método sendMail contiene la configuración para poder enviar un email (transporter).

Lo que hace es:

  1. Validar si el email se encuentra en la base de datos, si todo bien, se obtiene el user.

  2. Se genera un payload con el user.id.

  3. Se genera un token que expira en 15 minutos, incluye el payload con el id del usuario que solicita recuperar su contraseña. Por seguridad, el token debe guardarse en la BD y comprobarlo para evitar que envíen un token indeseado.

  4. Se genera un link que incluye el token necesario para recuperar la contraseña. Desde el frontend debe haber una vista para ello.

  5. Se llama el método update de UserService para actualizar los datos del usuario asignandole el token generado.

  6. Se establece el cuerpo del email.

  7. Se ejecuta el método sendMail que recibe el cuerpo del email.

const boom = require('@hapi/boom');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const nodemailer = require('nodemailer');

const { config } = require('../config/config');

const UserService = require('./user.service');
const service = new UserService();

class AuthService {
  async getUser(email, password) {
    const user = await service.findByEmail(email);
    if (!user) {
      throw boom.unauthorized();
    }

    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) {
      throw boom.unauthorized();
    }
    delete user.dataValues.password;
    return user;
  }

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

  async sendRecovery(email) {
    const user = await service.findByEmail(email);
    if (!user) {
      throw boom.unauthorized();
    }

    const payload = { sub: user.id };
    const token = jwt.sign(payload, config.jwtRecoverySecret, {
      expiresIn: '15min',
    });
    const link = `https://myfrontend.com/recovery?token=${token}`;
    await service.update(user.id, { recoveryToken: token });
    const mail = {
      from: `"Foo Boo 👻" <${config.mailerEmail}>`,
      to: `${user.email}`,
      subject: 'Email para recuperar contraseña',
      html: `<b>Ingresa a este link para recuperar tu contraseña: ${link}</b>`,
    };

    const rta = await this.sendMail(mail);
    return rta;
  }

  async sendMail(infoMail) {
    const transporter = nodemailer.createTransport({
      host: 'smtp.gmail.com',
      secure: true, // true for 465, false for other ports
      port: 465,
      auth: {
        user: config.mailerEmail,
        pass: config.mailerPassword,
      },
    });

    await transporter.sendMail(infoMail);
    return { message: 'Mail sent' };
  }
}

module.exports = AuthService;

En el endpoint /recovery de auth.router.js se hace el cambio del método sendMail por sendRecovery:

router.post('/recovery', async (req, res, next) => {
  try {
    const { email } = req.body;
    const rta = await service.sendRecovery(email);
    res.json(rta);
  } catch (error) {
    next(error);
  }
});

Debido a que se necesita ingresar un nuevo campo, se requiere generar una nueva migración (recovery-token-field), en el boilerplate de dicha migración se agrega una nueva columna a USER_TABLE. Esto contiene el nombre de la tabla, el nombre del nuevo campo y los tipos de datos, así como un método para hacer rollback (removeColumn):

'use strict';

const { USER_TABLE } = require('../models/user.model');

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.addColumn(USER_TABLE, 'recovery_token', {
      field: 'recovery_token',
      allowNull: true,
      type: Sequelize.DataTypes.STRING,
    });
  },

  down: async (queryInterface) => {
    await queryInterface.removeColumn(USER_TABLE, 'recovery_token');
  },
};

También es necesario agregar un nuevo campo al modelo user.model.js:

recoveryToken: {
    field: 'recovery_token',
    allowNull: true,
    type: DataTypes.STRING,
  },

Después de tener el boilerplate listo y haber modificado el modelo de usuario, se corre la migración con npm run migrations:run.

Definitivamente cualquier persona que quiera ser full stack debe de tomar este maravilloso curso. Pa’ lante compañeros el camino es largo pero la recompensa es jugosa. 😁💻🍗

Yo hice un MailService para tener la logica de los correos ahi

const nodemailer = require('nodemailer');
const config = require('../config');
const { host, port, user, password } = config.smtp;

class MailService {
  constructor() {
    this.transporter = nodemailer.createTransport({
      host,
      port,
      secure: true,
      auth: {
        user,
        pass: password,
      },
    });
  }

  async sendEmail(emailData) {
    await this.transporter.sendMail({
      from: emailData.from,
      to: emailData.to,
      subject: emailData.subject,
      text: emailData.text,
      html: emailData.html,
    });
  }
}
module.exports = MailService;


si les da un error como este.

{
  "message": "self signed certificate in certificate chain",
  "stack": "Error: self signed certificate in certificate chain\n    at TLSSocket.onConnectSecure (node:_tls_wrap:1535:34)\n    at TLSSocket.emit (node:events:513:28)\n    at TLSSocket._finishInit (node:_tls_wrap:949:8)\n    at TLSWrap.ssl.onhandshakedone (node:_tls_wrap:730:12)"
}```

deben poner el siguiente código asi


tls: {
rejectUnauthorized: false
}



nodemailer.createTransport({
host: process.env.MAIL_SERVER,
secure: false,
port: 587,
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD
},
tls: {
rejectUnauthorized: false
}
}

Con eso se me arreglo.


Así configure la constante con la información del correo:

const mail = {
      from: config.emailUser,
      to: `${user.email}`,
      subject: "ApiFakeStore: Recupera tu cuenta 🗝️",
      html: `
        <b>Ingresa a este link 👇👇 para recuperar tu cuenta 🗝️</b>
        <br>
        <a href="${link}" target="_blank">Recuperar cuenta</a>
      `,
    }

Para generar el link yo utilicé lo siguiente:

const url = `${req.protocol}://${req.get('host')}`
const link = `${url}/recovery?token=${token}`

De esta manera obtengo el dominio desde donde se está haciendo la petición para hacer el link.

No me di cuenta en que momento del video cambiamos el router para agregarle el sendRecovery. Excelente clase.

estas clases son geniales