No tienes acceso a esta clase

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

No se trata de lo que quieres comprar, sino de quién quieres ser. Invierte en tu educación con el precio especial

Antes: $249

Currency
$209

Paga en 4 cuotas sin intereses

Paga en 4 cuotas sin intereses
Suscríbete

Termina en:

11 Días
18 Hrs
54 Min
10 Seg

Validando tokens para cambio de contraseña

18/20
Recursos

Aportes 18

Preguntas 8

Ordenar por:

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

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.

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

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' }
  }

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);
    }
  }
);

para no mostrar el recoveryToken lo hice de la siguiente manera

  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();
    }
    return { ...user.dataValues, password: undefined, recoveryToken: undefined};
  }

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 😃 ❤️

Hola! excelente curso y ya que se llega casi al final alguien me puede guiar en como seria la implmentacion de este back con el front?... Gracias
en que curso explican el frontend ya integrado con el backend? pregunto porque yo hice algunos de frontend pero solo era HTML, CSS y javascript y no hacian peticiones hacia el backend
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
Bueno por algún motivo no me dejo responder la pregunta de Luis la cual es: Se me ocurrió en que se podría hacer un hash de los tokens para así darle una capa de seguridad al token, pero... esto realmente es posible? como por ejemplo con bcrypt? seguiria funcionando? Esta es mi respuesta: Hola Luis👋 Me dejaste pensando y busque poco . Sabemos que JWT firma el token no lo encripta , cual es la diferencia? , que cuando firmas el token el payload es accesible y cuando lo encriptas ya no lo es , entiendo la intención de usar bycript pero ten en cuenta que bycript no encripta como tal hace un proceso de hash unidireccional que impediría revertir el estado original del token, un método para lograr la encriptación es de la propia librería jose aquí te dejo un código de ejemplo Espero te sirva💚
hola

Me daba unauthorized y no lograba dar con la causa. Observé que para hacer el llamado al método changePassword, debo enviar los parametros como arrays.

async changePassword({ token, newPassword })

Este curso no tiene desperdicio 🥳

Para no enviar el recoveryToken en el json vamos a auth.service.js:

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

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)?!