No tienes acceso a esta clase

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

Aprende todo un fin de semana sin pagar una suscripción 🔥

Aprende todo un fin de semana sin pagar una suscripción 🔥

Regístrate

Comienza en:

3D
2H
30M
16S

Validando tokens para cambio de contraseña

18/20
Recursos

Aportes 10

Preguntas 7

Ordenar por:

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

o inicia sesión.

Cuando generas el token de recuperación y te llega al correo, podrías copiar ese token de recuperación y usarlo para acceder a cualquier endpoint, ya que al estar firmado con el mismo secreto que el access token sería valido

Hola de nuevo.
También les quiero dejar mi aporte de cómo elaboré mis validaciones para los nuevos endpoints con los routes de login, recovery y change-password.
Primero, creé un nuevo archivo dentro de los schemas llamado auth.schema.js con el siguiente contenido:

const Joi = require('joi');

const email = Joi.string().email(),
  password = Joi.string().min(8),
  newPassword = Joi.string().min(8),
  token = Joi.string().regex(
    /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_.+/=]*$/
  );

const loginAuthSchema = Joi.object({
  email: email.required(),
  password: password.required(),
});

const recoveryAuthSchema = Joi.object({
  email: email.required(),
});

const changePasswordAuthSchema = Joi.object({
  token: token.required(),
  newPassword: newPassword.required(),
});

module.exports = {
  loginAuthSchema,
  recoveryAuthSchema,
  changePasswordAuthSchema,
};

Finalmente, importé el middleware de validatorHandler y el schema creado anteriormente dentro de auth.router:

const express = require('express'),
  passport = require('passport'),
  AuthService = require('./../services/auth.service'),
  service = new AuthService(),
  router = express.Router(),
  validatorHandler = require('../middlewares/validator.handler'),
  {
    loginAuthSchema,
    recoveryAuthSchema,
    changePasswordAuthSchema,
  } = require('../schemas/auth.schema');

router.post(
  '/login',
  validatorHandler(loginAuthSchema, 'body'),
  passport.authenticate('local', { session: false }),
  async (req, res, next) => {
    try {
      const user = req.user;
      res.json(service.signToken(user));
    } catch (error) {
      next(error);
    }
  }
);

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

router.post(
  '/change-password',
  validatorHandler(changePasswordAuthSchema, 'body'),
  async (req, res, next) => {
    try {
      const { token, newPassword } = req.body;
      const rta = await service.changePassword(token, newPassword);
      res.json(rta);
    } catch (error) {
      next(error);
    }
  }
);

module.exports = router;

En una corta búsqueda que realicé en internet, encontré un método que utiliza regex para la validación del token. Sin embargo, esto no queire decir que sea su única forma de validación, lo pueden alterar a su gusto y conveniencia, al igual que añadir más reglas de validación para el password.

Una vez más, espero haya sido de utilidad para todos.

Saludos.

Hola gente.
En caso de que no deseen revelar el recoveryToken en el momento de hacer login, simplemente deben añadir una línea de código dentro del método getUser en auth.service:

async getUser(email, password) {
  const user = await service.findByEmail(email);
  if (!user) {
    throw boom.unauthorized();
  } else {
    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) {
      throw boom.unauthorized();
    } else {
      delete user.dataValues.password;
      delete user.dataValues.recoveryToken; // esta es la linea de codigo que se debe agregar
      return user;
    }
  }
}

Espero les sea de utilidad.

Saludos.

El email enviado al usuario para recuperar contraseña contiene un link para ello, al dar click en el link nos debe enviar a una vista para la recuperación de la contraseña y al dar click en Confirm, debemos recibir el token y la nueva contraseña. Por lo tanto, se debe hacer el proceso de validación del token (válido y sin expirar), si existe el usuario, etc.

En el auth.router.js se crea un nuevo endpoint /change-password para cambiar el password. Este endpoint va a obtener el token y la nueva contraseña del body, y ejecuta el método changePassword para validar que el token y la nueva contraseña sean correctos.

router.post('/change-password', async (req, res, next) => {
  try {
    const { token, newPassword } = req.body;
    const rta = await service.changePassword(token, newPassword);
    res.json(rta);
  } catch (error) {
    next(error);
  }
});

El método changePassword recibe el token y la nueva contraseña, primero se hace una validación donde se verifica el token, esto retorna el payload que tiene ese token. Del payload se obtiene el user (payload.sub) y lo busca en la BD con el método findOne.

Del usuario encontrado, se verifica que el campo recoveryToken sea el que está en la BD.

Posteriormente, para actualizar la contraseña se necesita borrar el token y hashear la nueva contraseña (service.update).

async changePassword(token, newPassword) {
    try {
      const payload = await jwt.verify(token, config.jwtRecoverySecret);
      const user = await service.findOne(payload.sub);

      if (user.recoveryToken !== token) {
        throw boom.unauthorized();
      }

      const hash = await bcrypt.hash(newPassword, 10);
      await service.update(user.id, { recoveryToken: null, password: hash });
      return { message: 'Password changed' };
    } catch (error) {
      throw boom.unauthorized();
    }
  }

Como buena practica evitemos atrapar errores en los proveedores o servicios, podemos lanzarlos desde los servicios, y debido a que ese service esta siendo llamado desde el controller dentro de un try, pues que el controller o router se encargue de atraparlos y mostrarlos.

router.post('/change-password',
  async (req, res, next) => {
    try {
      const { token, newPassword } = req.body;
      const mailInfo = await authService.changePassword(token, newPassword);
      res.json(mailInfo);
    } catch (error) {
      next(error);
    }
  }
)
  async changePassword(token, newPassword) {
    const payload = jwt.verify(token, process.env.JWT_SECRET);

    const user = await userService.findOne(payload.sub); // id

    if (!user || user.recoveryToken !== token) {
      throw boom.unauthorized();
    }

    const hash = bcrypt.hash(newPassword, 10);
    await userService.update(user.id, { recoveryToken: null, password: hash });
    return { message: 'Password updated' }
  }

Tenemos una vulnerabilidad abierta también amigos porque un atacante puede abusar de nuestro envio de correo sugiero agregar esta linea de codigo en ¨sendRecovery¨ justo despues de validar que el usuario exista.

jwt.verify(user.recoveryToken, config.jwtSecret, (err) => {
if (!err) throw boom.badRequest(‘You already have a active token.’);
});

así no podrá generar más tokens ni correos hasta que expire

Token Expired

Les comento que yo quise implementar un mensaje explícito de error por si el token había expirado. Encontré en la documentación que el método .verify() puede recibir un callback cuyo primer argumento es un error, este error tiene la forma:

      err = {
        name: 'TokenExpiredError',
        message: 'jwt expired',
        expiredAt: 1408621000
      }

Así que implementé esta lógica:

  async changePassword(token, newPassword) {
    const payload = jwt.verify(token, config.jwtSecret, (err, decoded) => {
      if (err) {
        throw boom.notAcceptable(err.name);
      }
      return decoded;
    }); //como saber si el token expiró?
    const user = await userService.findOne(payload.sub);
    if (token !== user.recoveryToken) {
      throw boom.notAcceptable("Sorry, valid but not the same token");
    }

    await userService.update(user.id, {
      recoveryToken: null,
      password: newPassword
    });
    return { message: "password changed successfully" };
  }

P.D: Sugiero que no se use Try -Catch en el servicio para que boom sí pueda lanzar errores donde se lo indicamos y los errores mayores los recoge el router que sí implementa Try - Catch.

También se puede crear desde el back una ruta que envíe html al client con un formulario
para cambiar la contraseña

router.get('/recover/form',
  validUserRecoveryToken,
  passport.authenticate('jwt', {session: false}),
  async (req, res, next) => {
    try {
      const user = req.user;
      const {token} = this.generateUserToken(user);
      await userService.update(user.id, {recoveryToken: token});

        const newPasswordForm =`
          <h1>Recover Password</h1>
          <form action="/api/v1/auth/recover/end?token=${token}" method="post">
            <div>
              <label for="newPassword">New Password:</label>
              <input type="password" id="newPassword" name="newPassword" />
            </div>
            <div>
              <label for="passwordConfirm">Repeat Password</label>
              <input type="password" id="passwordConfirm" name="passwordConfirm" />
            </div>
            <input type="submit" value="Submit" />
          </form>
        `
      res.send(newPasswordForm)
    } catch (error) {
      next(error);
    }
  }
);

este curso fue 95% perfecto ese 5% se le resta por que el endpoint profile.auth no existe y no sé que hace ni como lo hace 😦 … o en el curso de next.js como se haria para con el mismo backend mantener una sesion logeada y que no te mande inauthorizado despues de hacer refres(F5 | Ctrl +R)?!

para manejar las contraseñas se puede crear una tabla aparte en la base de datos, es decir, en vez de guardarla directamente en “users”, se crea una tabla “auth” donde iría el password y el id del usuario correspondiente.
Es un poco más complicado de manejar, pero de esta forma no tendríamos que estar tan al pendiente de no mandar info delicada cuando se manda información del usuario.
pd: este curso esta INCREÍBLE 😃 ❤️