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(鈥榊ou 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 鈥渦sers鈥, se crea una tabla 鈥渁uth鈥 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 馃槂 鉂わ笍