¿Cómo implementar la autenticación JWT de NestJS?

En este post, aprenderemos a implementar la autenticación JWT de NestJS utilizando la estrategia JWT de Passport.

· 6 min de lectura
¿Cómo implementar la autenticación JWT de NestJS?

JWT son las siglas de JSON Web Tokens. Básicamente, estos tokens son emitidos por el servidor después de la autenticación del usuario y pueden ser utilizados para otras solicitudes siempre que el token sea válido.

El uso de JWT efectivamente puede hacer que nuestras aplicaciones sean apátridas desde el punto de vista de la autenticación.

Vamos a utilizar la autenticación JWT de NestJS utilizando la estrategia local como base para esta aplicación. Así que voy a recomendar a ir a través de ese post y luego continuar con este.

Requerimientos

A continuación se presentan los dos requisitos de alto nivel para nuestra demostración de autenticación JWT de NestJS.

  • Dejar que los usuarios se autentiquen con un nombre de usuario/contraseña. Tras la autenticación, devolver un JWT.
Este JWT puede ser utilizado para posteriores llamadas a la aplicación.
  • A continuación, implementaremos una ruta API a la que sólo se puede acceder con la ayuda de un JWT válido.

Mira la siguiente ilustración que explica el concepto.

Instalación de paquetes


Para empezar con NestJS JWT, necesitamos instalar los siguientes paquetes.

$ npm install --save @nestjs/jwt passport-jwt
$ npm install --save-dev @types/passport-jwt
  • El paquete @nestjs/jwt ayuda a la manipulación de JWT.
  • El paquete passport-jwt implementa la estrategia JWT.
  • Al paquete @types/passport-jwt proporciona las definiciones de tipo para facilitar el desarrollo.

Generando el JWT


El primer paso es que podamos generar un JWT y devolverlo como respuesta de la ruta /login de nuestra aplicación.

Para conseguirlo, primero crearemos un método loginWithCredentials() en el servicio auth.

auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class AuthService {
    constructor(private usersService: UsersService, private jwtTokenService: JwtService){}

    async validateUserCredentials(username: string, password: string): Promise<any> {
        const user = await this.usersService.findOne(username);

        if (user && user.password === password) {
            const {password, ...result} = user;
            return result;
        }
        return null;
    }

    async loginWithCredentials(user: any) {
        const payload = { username: user.username, sub: user.userId };

        return {
            access_token: this.jwtTokenService.sign(payload),
        };
    }
}

El método loginWithCredentials() acepta el objeto usuario. Usando este objeto, crea una carga útil y la pasa a la función sign() de la instancia del JWTService. Básicamente, la función sign() genera un JWT.

Devolvemos el mismo como salida de la función loginWithCredentials().

constants.ts
export const jwtConstants = {
    secret: 'secretKey',
}

Si llegados a este punto quieres complementar la información teórica, te dejo el siguiente video  

La configuración del segundo paso está dentro del AuthModule.

auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { LocalStrategy } from './local.strategy';

@Module({
  imports: [
    UsersModule, 
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: {expiresIn: '60s'}
    })],
  providers: [AuthService, LocalStrategy],
  exports: [AuthService]
})
export class AuthModule {}

Aquí, básicamente estamos registrando el JwtModule. Al hacerlo, especificamos la clave secreta y las signOptions.

La propiedad expiresIn significa que el JWT emitido por nuestro servidor tendrá un tiempo de expiración de 60 segundos.

En otras palabras, después de 60 segundos, el token será inválido y se necesitará un token nuevo.

Actualización de la ruta de inicio de sesión


En este punto, tenemos la configuración necesaria para firmar y emitir un JWT. Sin embargo, te preguntarás cómo el método loginWithCredentials() del AuthService recibirás el objeto usuario.

De eso nos encargamos en el siguiente código donde implementamos una ruta para el login. Si quieres saber más sobre la implementación de controladores, consulta este post detallado sobre Controladores NestJS.

app.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';

@Controller()
export class AppController {

  constructor(private authService: AuthService){}
  
  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return this.authService.loginWithCredentials(req.user);
  }
}

Como parte del proceso, el nombre de usuario y la contraseña entrantes se pasan al método validate() en el servicio auth. Si es un usuario válido, el método validate() devuelve el objeto usuario después de quitar la contraseña.

Sólo en el caso de que tengamos un usuario válido, se invoca el método loginWithCredentials() del controlador/manejador de peticiones.

En ese momento, el parámetro req contendrá la propiedad del usuario después de la autenticación. Simplemente pasamos la propiedad del usuario a la función loginWithCredentials() del servicio auth. El servicio de autenticación devuelve el JWT firmado y el gestor de peticiones devuelve lo mismo al cliente. De esta manera, el ciclo se completa y el cliente tiene un JWT válido.

En este punto, podemos iniciar nuestra aplicación y pulsar la ruta /login con un nombre de usuario y una contraseña válidos. Si todo está bien, recibiremos el token JWT como token de acceso.

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gTWFyc3RvbiIsInN1YiI6MSwiaWF0IjoxNjM5OTY5OTI3LCJleHAiOjE2Mzk5Njk5ODd9.X8jNA14s-zfpOYEwwVzKHOHJ_Q1ULpTsgO3OFuHEfMU"
}

Básicamente, podemos decir con confianza que nuestro proceso de autenticación está funcionando.

via GIPHY

Quiero más

Por supuesto! gracias al apoyo que se ha conseguido por todos ustedes (comentando, suscribiendote y compartiendo) se agregaron nuevos videos, en esta ocasión iniciamos el curso de testing en angular, curso de node, curso mongo y mucho más

Implementación de la estrategia del pasaporte JWT


Ahora, podemos implementar la estrategia del passport JWT. Necesitamos hacer esto para poder abordar nuestro segundo requisito de proteger los puntos finales utilizando JWT.

Crearemos un nuevo archivo jwt.strategy.ts dentro de la carpeta auth y colocaremos el siguiente código.


jwt.strategy.ts
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { jwtConstants } from './constants';

export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor() {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: false,
            secretOrKey: jwtConstants.secret
        })
    }

    async validate(payload: any) {
        return {userId: payload.sub, username: payload.username}
    }
}

Básicamente, aquí extendemos la clase PassportStrategy y especificamos que queremos extender la estrategia del paquete passport-jwt.

A continuación, inicializamos la estrategia pasando algunos parámetros en la llamada a super(). A continuación se detallan los parámetros:

  • jwtFromRequest: Este parámetro especifica el método con el que extraeremos el JWT de la petición.

Básicamente, utilizamos el método fromAuthHeaderAsBearerToken() del paquete ExtractJwt. Esto se debe a que es una práctica estándar pasar el token JWT como token portador en la cabecera de autorización mientras se realizan las solicitudes de la API.

  • ignoreExpiration: Establecemos esta propiedad como false. Esto significa básicamente que queremos bloquear las peticiones con tokens caducados. Si se realiza una llamada con un token caducado, nuestra aplicación devolverá una respuesta 401 o Unauthorized.
  • secretOrKey: Esto es básicamente la clave secreta del archivo de constantes. Lo ideal es que esta clave se publique codificada en pem. Además, no debería ser expuesta públicamente.

A continuación, implementamos la función validate(). En la función, simplemente devolvemos el objeto usuario.

Esto puede parecer confuso ya que no estamos procesando nada en la función. Esto es porque para la estrategia JWT, el passport primero verifica la firma del JWT y decodifica el objeto JSON.

Sólo entonces llama a la función validate() y pasa el JSON decodificado como carga útil. Básicamente, esto asegura que si la función validate() es llamada, significa que el JWT también es válido.

En la función validate(), simplemente devolvemos el objeto usuario. Passport adjunta este objeto de usuario al objeto Request.

Actualizar el módulo Auth


Por último, añadimos la JwtStrategy como proveedor del módulo Auth.

auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UsersModule } from 'src/users/users.module';
import { AuthService } from './auth.service';
import { jwtConstants } from './constants';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [
    UsersModule, 
    PassportModule,
    JwtModule.register({
      secret: jwtConstants.secret,
      signOptions: {expiresIn: '60s'}
    })],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService]
})
export class AuthModule {}


El cambio anterior permite el uso de JwtStrategy en el contexto de nuestra aplicación.

Creando el Auth Guard


Finalmente, creamos un Auth Guard que utiliza la estrategia JWT. Nuestro JwtAuthGuard extiende el AuthGuard proporcionado por el paquete @nestjs/passport. Hacemos referencia al guard por su nombre por defecto, es decir, jwt.

auth/jwt-auth.guard.ts
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

Ahora, podemos simplemente utilizar este guard en una de las rutas que queremos proteger.

app.controller.ts
import { Controller, Get, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth/auth.service';
import { JwtAuthGuard } from './auth/jwt-auth.guard';

@Controller()
export class AppController {

  constructor(private authService: AuthService){}
  
  @UseGuards(AuthGuard('local'))
  @Post('login')
  async login(@Request() req) {
    return this.authService.loginWithCredentials(req.user);
  }

  @UseGuards(JwtAuthGuard)
  @Get('user-info')
  getUserInfo(@Request() req) {
    return req.user
  }
}

Aquí, el manejador de peticiones getUserInfo() utiliza el JwtAuthGuard. Básicamente, esto significa que a menos que haya un JWT válido en la cabecera Authorization de la petición HTTP entrante, el endpoint no proporcionará una respuesta válida.

Ahora podemos probar nuestra aplicación generando primero un token usando la ruta /login.
$ curl -X POST http://localhost:3000/login -d '{"username": "John Marston", "password": "rdr1"}' -H "Content-Type: application/json"

Una vez obtenido el token, podemos llamar a la ruta /user-info.

curl http://localhost:3000/user-info -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IkpvaG4gTWFyc3RvbiIsInN1YiI6MSwiaWF0IjoxNjM5OTY1NTMzLCJleHAiOjE2Mzk5NjU1OTN9..."

Además, si esperamos 60 segundos y luego usamos el mismo token, obtenemos un error 401.

Conclusión


Con esto, hemos aprendido con éxito cómo implementar la autenticación JWT de NestJS utilizando la estrategia de pasaporte JWT.

El código de este post está disponible en Github.

Plataforma de cursos gratis sobre programación

Artículos Relacionados

¿Cómo crear un microservicio NestJS Redis?
· 4 min de lectura
Tenemos Twitter Spaces 🥳
· 6 min de lectura
Bootcamp Real
· 8 min de lectura
Standalone components en Angular
· 8 min de lectura
Arquitectura Hexagonal
· 8 min de lectura