No tienes acceso a esta clase

隆Contin煤a aprendiendo! 脷nete y comienza a potenciar tu carrera

Implemetando login con Passport.js

7/20
Recursos

Aportes 12

Preguntas 3

Ordenar por:

驴Quieres ver m谩s aportes, preguntas y respuestas de la comunidad?

o inicia sesi贸n.

Soluci贸n de error con Passport.Js

En el momento de nosotros loguearnos nos lanza el siguiente error 馃憞馃徑

message": "req.logIn is not a function",

para poder solucionar este error sin tener que bajar nuestra version de passport podemos hacer lo siguiente 馃懆鈥嶐煉

Index.js

const passport = require('passport')
// Antes de nuestras rutas debemos de colocar esto
app.use(passport.initialize());
// Rutas
routerApi(app);

Esto sucede ya que debemos de inicializar el middleware de passport antes de nuestras rutas

y ahora todo deberia de funcionar con normalidad 馃コ

Passport js summary:

  • Install:

    npm install passport

    You may install all strategies as you want, some of the available strategies are:

    passport-local

    passport-facebook

    passport-twitter

    **passport-jwt**

  • Configure environment:

    Following a good fold structure, we can follow the next structure

    • utils/
      index.js

      • auth/
        • strategies
    • Utils will contain all utilities in our program, one of these is Authentication tools

    • Inside auth, strategies stores all strategies that will be used in our application.

    • Finally to configure our strategies, create anindex.js file.

  • Programming strategies:

    • Step 1: Config Strategy

      Each strategy, have its own way to be programmed, in this case, local strategy is configured:

      //Destructure passport-local strategy and get just Strategy
      const { Strategy } = require('passport-local');
      //Import service which will be used for authentication
      const Service = require("./../../../services/user.service.js")
      const userService = new Service();
      const boom = require('@hapi/boom');
      const bcrypt = require('bcrypt');
      //Create a new strategy and configure it
      const localStrategy = new Strategy({
              usernameField: 'email'
          },
          async(email, password, done) => {
              try {
                  const user = await userService.findByMail(email);
      
                  if (!user) {
                      //return error and it wont be authenticated
                      done(boom.unauthorized(), false);
                      return;
                  }
                  //Using bcrypt to compare the password with the hash
                  const isMatch = await bcrypt.compare(password, user.password);
                  if (!isMatch) {
                      done(boom.unauthorized(), isMatch);
                      return;
                  }
                  delete user.dataValues.password;
                  //Null errors and return user
                  done(null, user);
              } catch (err) {
                  //Return error and false, it wont be authenticated
                  done(err, false);
              }
          });
      
      //Export the strategy
      module.exports = localStrategy;
      
    • Step 2: Config index.js

      Just need to configure in this way all strategies which will be used in our application

      //Require passport
      const passport = require('passport');
      //Require strategy used
      const localStrategy = require('./strategies/local.strategy');
      //Tell to passport we'll use localStrategy 
      passport.use(localStrategy);
      
    • Step 3: Config a route: routes/auth.router.js

      const express = require('express');
      
      const passport = require('passport');
      const router = express.Router();
      
      router.post('/login',
          //We are using the local strategy and not using sessions
          passport.authenticate('local', { session: false }),
          async(req, res, next) => {
              try {
                  res.json(req.user);
              } catch (err) {
                  next(err);
              }
          }
      );
      
      module.exports = router;
      
    • Step 4: Make our application work with passport

      In file index.js of root applications, just configure passport before initializing routes.

      const passport = require('passport');
      app.use(passport.initialize({ session: false }));
      
      • Final index.js example

        /*Server basics*/
        const express = require('express');
        const routerApi = require('./routes');
        /*Middlewares*/
        const { logErrors, errorHandler, boomErrorHandler, queryErrorHandler } = require('./middlewares/error.handler');
        const checkApiKey = require('./middlewares/auth.handler');
        const app = express();
        const port = process.env.PORT || 3000;
        const passport = require('passport');
        app.use(passport.initialize({ session: false }));
        
        app.use(express.json());
        
        app.get('/', checkApiKey, (req, res) => {
            res.send('<h1 style="text-align: center;font-family: Roboto sans-serif;">Hello Everybody (:</h1>');
        });
        /*Routes config*/
        
        routerApi(app);
        
        require("./utils/auth");
        /*Middlewares Config */
        app.use(logErrors);
        app.use(boomErrorHandler);
        app.use(errorHandler);
        app.use(queryErrorHandler);
        /*Serve*/
        app.listen(port, () => {
            // eslint-disable-next-line no-console
            console.log(`Server running in port ${port}`);
        });
        

Passport.js es una serie de librer铆as y estrategias que nos brinda para hacer la capa de autenticaci贸n, permite generar varias estrategias (twitter, github, facebook, etc.) teniendo un c贸digo base para logearnos de diferentes maneras.

P谩gina oficial https://www.passportjs.org/

  • Instalando passport: npm i passport

  • passport-local permite hacer un login b谩sico usando username y password, su instalaci贸n es: npm install passport-local

Se crea la carpeta utils/auth/strategies donde se colocar谩n las estrategias a implementar.

Se crea el archivo local.strategy.js, ah铆 se crea una instancia de Strategy la cual recibe una funci贸n callback con la l贸gica de negocio. Recibe tres par谩metros: email, password y la funci贸n done que se usar谩 cuando todo salga bien o mal.

En este mismo archivo se crea una serie de validaciones, en primer lugar valida si existe el email y en segundo lugar valida si la password es correcta.

Para la primera validaci贸n se busca el email y si no se encuentra, se ejecuta la funci贸n done lanzando un error de tipo boom (unauthorized) con un false (no se pudo hacer la validaci贸n).

En la segunda validaci贸n ya se encontraron los datos de usuario en la base de datos, por lo tanto, verifica que la contrase帽a que env铆a el usuario sea la misma que la que se encuentra en la BD (hace una comparaci贸n). Si las contrase帽as no son iguales se ejecuta la funci贸n done lanzando un error de tipo boom con un false.

Si todo es correcto se ejecuta done indicando que no hay error (null) y enviando el usuario (user).

Por defecto, la estrategia local maneja username y password, es posible cambiar esos valores por email y password mandando esas opciones antes de la funci贸n as铆ncrona con el uso de usernameField y passwordField.

const { Strategy } = require('passport-local');
const bcrypt = require('bcrypt');
const boom = require('@hapi/boom');

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

const LocalStrategy = new Strategy(
  {
    usernameField: 'email',
    passwordField: 'password',
  },
  async (email, password, done) => {
    try {
      const user = await service.findByEmail(email);
      if (!user) {
        done(boom.unauthorized(), false);
      }

      const isMatch = await bcrypt.compare(password, user.password);
      if (!isMatch) {
        done(boom.unauthorized(), false);
      }
      delete user.dataValues.password;
      done(null, user);
    } catch (error) {
      done(error, false);
    }
  }
);

module.exports = LocalStrategy;

En el archivo utils/auth/index.js se definen las estrategias a usar, cabe mencionar que puede tener muchas estrategias de autenticaci贸n. Algo muy importante es que debe implementarse en el index.js del proyecto con require('./utils/auth'):

const passport = require('passport');
const LocalStrategy = require('./strategies/local.strategy');

passport.use(LocalStrategy);

En user.service.js se hacen las modificaciones permanentes, en este caso se crea el m茅todo findByEmail para buscar por el email que es nuestro username.

async findByEmail(email) {
    const rta = await models.User.findOne({
      where: { email },
    });
    return rta;
  }

Para implementar la estrategia se crea un nuevo routing auth.router.js, cada estrategia tiene un nombre clave y debido a que funciona como un middleware, se necesita dejarlo pasar a la l贸gica de servicio o conectarlo a un servicio principal, pero antes hacer la validaci贸n.

En este caso se usa passport y con el m茅todo authenticate se indica cu谩l estrategia se va a autenticar dici茅ndole que no se desea manejar sesiones (session: false) ya que posteriormente se implementar谩 JWT para manejar sesiones.

Si todo bien, entonces en el siguiente middleware se le env铆a un request con el usuario (req.user).

const express = require('express');
const passport = require('passport');

const router = express.Router();

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

module.exports = router;

Se implementa el nuevo router en routes/index.js:

const express = require('express');

const productsRouter = require('./api/products.route');
const usersRouter = require('./api/users.router');
const categoriesRouter = require('./api/categories.route');
const orderRouter = require('./api/orders.router');
const customerRouter = require('./api/customer.route');
const authRouter = require('./api/auth.route');

function routerApi(app) {
  const router = express.Router();
  app.use('/api', router);
  router.use('/products', productsRouter);
  router.use('/users', usersRouter);
  router.use('/categories', categoriesRouter);
  router.use('/orders', orderRouter);
  router.use('/customers', customerRouter);
  router.use('/auth', authRouter);
}

module.exports = routerApi;

Hay un problema con la version de passport. Si utilizan la misma version que en el curso, la 0.4.1, todo sale bien. Si utilizan la ultima version, la 0.5.0, van a tener el error TypeError: req.logIn is not a function, por lo que hay que reescribir la estrategia. Yo decidi dejarla como en el curso y utilizar la version 0.4.1, despues la actualizo

Por si a alguien le pasa lo mismo que a mi , por mas que pusiera 鈥渁pp.use(passport.initialize());鈥 en el index me seguia dando el error de que la funcion authenticate no existia y que si o si tenia que nombrar la estrategia, la unica solucion que me funciono fue declarar la new strategy en el mismo index de la carpeta auth mi codigo quedo asi:

const boom = require('@hapi/boom');
const bcrypt = require('bcrypt');
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy;
const UserService = require('../../services/user.service')
const service = new UserService()

passport.use(new LocalStrategy({
  usernameField: 'email',
  passwordField: 'password'
}, async (email, password, done) => {
  try {
    const user = await service.findByEmail(email)
    if(!user){
      done(boom.unauthorized(), false)
    }
    const isMatch = await bcrypt.compare(password, user.password)
    if(!isMatch){
      done(boom.unauthorized(), false)
    }
    delete user.dataValues.password
    done(null, user)
  } catch (error) {
    done(error, false)
  }
}))
npm i passport passport-local

En lo personal elijo mantener el esquema de validaci贸n.
Lo coloque antes de la authenticacion, ya que, si hay un error con los datos enviados, passport solo nos lanzara un 鈥淏ad request鈥, en cambio con el esquema podemos mostrar cuales son los datos que no est谩n siendo enviados correctamente en el 鈥渂ody鈥 de la petici贸n.

router.post(
  "/login",
  handleValidator(loginAuthSchema, "body"),
  passport.authenticate("local", { session: false }),
  async (req, res, next) => {
    try {
      res.json(req.user);
    } catch (err) {
      next(err);
    }
  }
);

Con esquema:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "\"password\" is required"
}

Sin esquema:

Bad Request

馃寜 Clase #7: Implementando login con Passport.js 7/20 馃寜

驴Qu茅 es Passport js? 馃殻


Passport contiene una serie de librer铆as y estrategias (m谩s de 500 estrategias) que permite hacer la capa de autenticaci贸n. (Fuente: aqu铆).

驴Qu茅 significa que Passport sea vers谩til? 鉀碉笍


Passport permite logearnos mediante diferentes estrategias o formas como Twitter, Google, Facebook, etc. Lo que se hace es teniendo nuestro c贸digo base, aplicamos con Passport la estrategia con la que queremos hacer login a trav茅s del endpoint /auth/login.

  • Vamos a instalar la primera estrategia: passport-local que es la estrategia m谩s b谩sica que permite logearnos con nuestro usuario y contrase帽a.
  • Para instalar passport y passport-local vamos a la terminal y ejecutamos:
npm i passport passport-local

  • Ahora vamos a VSC para implementar la estrategia, creamos la carpeta utils, dentro de utils, se crea otra carpeta llamada auth, dentro de auth, creamos el archivo index.js, el c贸digo queda:
const passport = require('passport');
//Aqui se definen qu茅 estrategias se van a usar
const LocalStrategy = require('./strategies/local.strategy');

passport.use(LocalStrategy);


Guardamos, dentro de la carpeta de auth, se crea la carpeta donde estar谩n todas las estrategias: strategies, dentro de strategies, creamos el archivo local.strategy.js donde se implementar谩 la l贸gica del passport-local, el c贸digo queda:

//Nos treamos a passport-local
const { Strategy } = require('passport-local');
//Usamos a boom como manejador de errores
const boom = require('@hapi/boom');
const bcrypt = require('bcrypt');

const UserService = require('../../../services/user.service');
//Se crea una instancia de servicio
const service = new UserService();

//Constructor
const LocalStrategy = new Strategy({
	//La estrategia local es la m谩s b谩sica
	usernameField: 'email',
	passwordField: 'password'
	},
	async (email, password, done) => {
		try {
			//Buscar con findByEmail el usuario
			const user = await service.findByEmail(email);
			//Validai贸n para cuando el usuario no exista
			if (!user) {
				//As铆 se env铆an los errores con false
				done(boom.unauthorized(), false);
			}
			//Comprobaci贸n del password con el hash
			const isMatch = await bcrypt.compare(password, user.password);
			//Si el password no coincide, sale un mensaje que el usuario no est谩 autorizado
			if (!isMatch) {
				done(boom.unauthorized(), false);
			}
			delete user.dataValues.password;
			//Si todo sali贸 bien, se ejecuta el done enviando null diciendo que nno hay error
			done(null, user);
		} catch (error) {
			done(error, false);
		}
	}
);
module.exports = LocalStrategy;


Guardamos, vamos a la carpeta de services y abrimos el archivo de user.services.js, despu茅s de la funci贸n find(), se implementa la funci贸n findByEmail(email) que permitir谩 buscar el usuario por el email, el c贸digo queda:

//Encontrar el primer usuario que tenga el email qu茅 adem谩s es 煤nico
async findByEmail(email) {
	const rta = await models.User.findOne({
		where: { email }
	});
	return rta;
}


Guardamos, vamos a la carpeta routes, creamos el archivo auth.router.js, el c贸digo queda:

const express = require('express');
const passport = require('passport');
//No se necesita validar con el schema
//const validatorHandler = require('./../middlewares/validator.handler');
//const { loginAuthSchema } = require('./../schemas/user.schema');

const router = express.Router();

//Funciona como un middleware
router.post('/login',
  passport.authenticate('local', {session: false}),
  async (req, res, next) => {
    try {
      //Pasa el usuario
      res.json(req.user);
    } catch (error) {
      next(error);
    }
  }
);

/*router.post(
  "/login",
  validatorHandler(loginAuthSchema, "body"),
  passport.authenticate("local", { session: false }),
  async (req, res, next) => {
    try {
      res.json(req.user);
    } catch (err) {
      next(err);
    }
  }
);*/

module.exports = router;


Guardamos, abrimos dentro de la carpeta routes el archivo index.js, agregamos donde est谩n las constantes al final de 茅stas:

const authRouter = require('./auth.router');


Tambi茅n agregamos la ruta dentro de routerApi():

router.use('/auth', authRouter);


Guardamos, abrimos el archivo de index.js (el index m谩s externo) y agregamos un require din谩mico para que entienda donde se debe dirigir cuando se haga el endpoint:

//require de forma din谩mica para ejecutar el archivo de la estrategia
require('./utils/auth');


Guardamos, si no est谩 corriendo en la terminal en modo de desarrollador, se ejecuta: npm run dev

  • Vamos a Insomnia, creamos una carpeta llamada Auth, dentro creamos la petici贸n POST llamada Login, en la direcci贸n se coloca: .API_URL/api/v1/auth/login, en el body del JSON, colocamos:
{
	"email": "[email protected]",
	"password": "12356 .2020",
	"role": "admin"
}


En la salida debe salir el c贸digo 200 OK (esto significa que nuestra solicitud de logear con ese email y contrase帽a son correctos):

{
	"id": 3,
	"email": "[email protected]",
	"role": "admin",
	"createdAt": "2023-03-17T02:47:32.524Z"
}


En caso de ingresar un email que no sea correcto la salida se tiene un c贸digo 401 Unauthorized:

{
	"statusCode": 401,
	"error": "Unauthorized",
	"message": "Unauthorized"
}


Nota: Si se quiere editar los nombres de los campos de entrada para el Login, en el archivo local.strategy.js de la ruta: utils/auth/strategies se debe indicar los nombres autorizados:

usernameField: 'email',
passwordField: 'password'

Me arroja 鈥淏ad Request鈥, no se como solucionarlo 馃槙

Recuerden que para tener mejores mensajes de error, podemos agregar un argumento a boom:

if (!user) {
	done(boom.unauthorized("Sorry, this email does not exist."), false); // email no existe
  }

const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
	done(boom.unauthorized("Ups! Wrong password."), false); // contrase帽a incorrecta
}

Para evitar que en cualquier consulta que involucre los usuarios puede indicar un getter en modelo User en el campo password que retorne siempre nulo este valor鈥

  password: {
    allowNull: false,
    type: DataTypes.STRING,
    get() {
      return null;
    },
  },

para los que les salga este error:
.
=> Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

es por que no estan retornando la funcion del callback de done y el servidor trata de enviar ma麓s de una respuesta, esto pasa porue se sigue ejecuntando las lineas despu茅sde mandar el error de esta manera
.

done(boom.unauthorized(), false);

solucion: retornar la funcion cb done
=>

  • return done(boom.unauthorized(), false);