¿Cómo integrar Passport.js con NestJS para la autenticación?
Para añadir autenticación robusta a nuestras aplicaciones NestJS, Passport.js es la opción ideal. Este módulo se integra fácilmente en la estructura de Nest.js, y ofrece varias estrategias de autenticación. La documentación oficial de Nest.js sugiere usar un módulo especializado de Passport para facilitar esta integración.
¿Qué librerías debemos instalar?
Para comenzar con la autenticación, necesitamos instalar varias librerías. Aquí está el paso a paso para integrar Passport.js en nuestro proyecto Nest.js:
Módulo de Passport para NestJS: Nos ofrece un puente entre Passport.js y la estructura de módulos de Nest.js.
Librería passport: Herramienta base para la autenticación.
Estrategia local para Passport: Para autenticar con usuario y contraseña.
Tipado para la estrategia local: Asegura que el tipado esté acorde con las funciones de autenticación.
En la terminal, ejecutamos la siguiente línea para instalar estas librerías:
Con las librerías instaladas, creamos un servicio de autenticación. Usaremos el generador de Nest.js:
$ nest generate service auth
Ubicaremos nuestro servicio dentro del módulo adecuado y lo configuraremos para importar UserService. Este servicio se encargará de verificar a los usuarios basándose en su email:
import{Injectable}from'@nestjs/common';import{UserService}from'../user/user.service';@Injectable()exportclassAuthService{constructor(private userService:UserService){}asyncvalidateUser(email:string, password:string):Promise<any>{const user =awaitthis.userService.findByEmail(email);if(user && user.password=== password){return user;}returnnull;}}
¿Cómo verificamos credenciales de usuario?
Al crear el método validateUser, compararemos las credenciales proporcionadas con las almacenadas en la base de datos utilizando técnicas seguras. Aquí usamos bcrypt para comparar contraseñas cifradas:
import*as bcryptfrom'bcrypt';asyncvalidateUser(email:string, password:string):Promise<any>{const user =awaitthis.userService.findByEmail(email);if(user &&await bcrypt.compare(password, user.password)){return user;}returnnull;}
¿Qué es una strategy en Passport.js?
Una strategy define cómo autenticar usuarios. Passport.js diversifica sus estrategias para diferentes placas, como Local, JWT, OAuth, entre otras.
¿Cómo creamos una estrategia local?
Ubicamos la strategy en una nueva carpeta strategies dentro de nuestro módulo:
import{Strategy}from'passport-local';import{PassportStrategy}from'@nestjs/passport';import{Injectable,UnauthorizedException}from'@nestjs/common';import{AuthService}from'./auth.service';@Injectable()exportclassLocalStrategyextendsPassportStrategy(Strategy){constructor(private authService:AuthService){super();}asyncvalidate(email:string, password:string):Promise<any>{const user =awaitthis.authService.validateUser(email, password);if(!user){thrownewUnauthorizedException();}return user;}}
¿Cómo integramos el módulo de Passport en el módulo de autenticación?
El último paso es asegurar que el módulo de autenticación integre todas las piezas correctamente. Importamos el PassportModule y registramos la estrategia local:
Con esto, hemos implementado la base para autenticación utilizando Passport.js y NestJS. Cada paso que sigues te acerca más a crear una aplicación segura. ¡No olvides explorar más estrategias para seguir mejorando!
Autenticación con Passport.js en Nest.js: Estrategias Locales
Autenticación con Passport.js en Nest.js: Estrategias Locales
Un aporte que tengo que decir con conocimiento mas profundamente de hashing , es que si bien Nico dijo que en “bcrypt.compare(password_entrada, password_guardada)” solamente el usuario “desencriptará” para ver si la contraseña es la original, esto no es así.
Nadie desencripta la contraseña, de hecho, con la funcion hash, es imposible saber su dominio. A esto se le llama one way function, una one way function es una funcion que toma una entrada y arroja una salida,
podemos establecer la relacion desde entrada -> salida, pero jamas salida -> entrada, es decir sabiendo la salida nunca vamos a determinar la entrada.
Por ende, en este caso, lo que hace bcrypt.compare realmente no es “deshashear” el hash guardado y ver si es compatible con la password_entrada en texto plano, seria muy inseguro esto, ya que le daria saber a los atacantes que se puede obtener entrada con salida y no.
Lo que hace realmente es tomar el password_entrada, y utilizar bcrypt.hash(password_entrada, salt) donde esta salt es la MISMA que la que se usó en el bcrypt.hash de la password_guardada, posteriormente este hash a la password_entrada se compara con password_guardada (que ya esta en version hash), de esta forma nadie desencriptará nada.
La salt y rounds son dos cosas distintas.
Salt es un texto extra que se le adiciona a la entrada para proveer de mas seguridad al hasheado, ejemplo, si el usuario digita su password para guardar “pepito123” y la salt es "qwerty) el hash guardado no sera el hash de “pepito123” sera el hash “pepito123”+salt , o sea concatenados.
Las rounds son cuantas veces se vuelve a hashear en un hash, o sea, si tengo un hash de 2 rounds, significa
password_entrada -> hash(password_entrada) -> hash(hash(password_entrada)), matematicamnete se conoce como composicion de funciones. La entrada de una es la salida de la anterior.
Si no les funciona la autenticacion probablemente sea porque estan usando el campo de email, passport por defecto espera un username y un password. Para que funcione con email en el contructor por medio del metodo super de local.stretegy.ts se debe indicar de la siguiente maneral:
[Nest]11756-03/19/2023,3:07:34PMERROR[ExceptionHandler]Class constructor MixinStrategy cannot be invoked without 'new'TypeError:Class constructor MixinStrategy cannot be invoked without 'new' at newLocalStrategy(/home/milo2003/personalProjects/nestjs/nestjs-typeorm/src/auth/strategies/local.strategy.ts:9:5) at Injector.instantiateClass(/home/milo2003/personalProjects/nestjs/nestjs-typeorm/node_modules/@nestjs/core/injector/injector.js:340:19) at callback(/home/milo2003/personalProjects/nestjs/nestjs-typeorm/node_modules/@nestjs/core/injector/injector.js:53:45) at processTicksAndRejections(node:internal/process/task_queues:95:5) at Injector.resolveConstructorParams(/home/milo2003/personalProjects/nestjs/nestjs-typeorm/node_modules/@nestjs/core/injector/injector.js:132:24) at Injector.loadInstance(/home/milo2003/personalProjects/nestjs/nestjs-typeorm/node_modules/@nestjs/core/injector/injector.js:57:13) at Injector.loadProvider(/home/milo2003/personalProjects/nestjs/nestjs-typeorm/node_modules/@nestjs/core/injector/injector.js:84:9) at asyncPromise.all(index 4) at InstanceLoader.createInstancesOfProviders(/home/milo2003/personalProjects/nestjs/nestjs-typeorm/node_modules/@nestjs/core/injector/instance-loader.js:47:9) at /home/milo2003/personalProjects/nestjs/nestjs-typeorm/node_modules/@nestjs/core/injector/instance-loader.js:32:13
Al parecer al usar super()
import{Injectable,UnauthorizedException}from'@nestjs/common';import{PassportStrategy}from'@nestjs/passport';import{Strategy}from'passport-local';import{AuthService}from'../services/auth.service';@Injectable()exportclassLocalStrategyextendsPassportStrategy(Strategy,'local'){constructor(privateauthService:AuthService){super();//Aquí}asyncvalidate(email: string,password: string){const user =this.authService.validateUser(email, password);if(!user){thrownewUnauthorizedException('not allow');}return user;}}