#Validate in JwtStrategy?

72 messages · Page 1 of 1 (latest)

long quartz
#

Hi all, I am having a doubt in the authentication and authorization when using strategies like JWT and combining it with a Guard. I don't know exactly when the guard activates and calls the strategy. My idea is to get the strategy through the "validate" function to search in the database the users with the payload of the token that is received when using guards in my endpoints.

My question is: Why don't we enter the logic of the "validate" function in the strategy?

#

My implementation is as follows:
jwt-auth.guard.ts

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(
    private readonly jwtService: JwtService,

    private readonly reflector: Reflector,
  ) {
    super();
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) return true;

    const request = context.switchToHttp().getRequest();
    const token = await this.extractTokenFromHeader(request);

    if (!token) throw new UnauthorizedException();

    try {
      console.log('Try');
      const payload = await this.jwtService.decode(token);
      console.log({ payload });
      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }

    return true;
  }

  private async extractTokenFromHeader(
    request: Request,
  ): Promise<string | undefined> {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}
#

The strategy is:

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @Inject(EnvConfig.KEY) private configService: ConfigType<typeof EnvConfig>,

    private readonly authService: AuthService,
  ) {
    console.log('JWT STRATEGY');
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.SECURITY.JWT_SECRET,
    });
  }

  async validate(payload: IJwtPayload): Promise<IAuthResponse> {
    console.log('VALIDATE JWT STRATEGY');
    const { id } = payload;

    const validate = await this.authService.validateUser(id);

    return validate;
  }
}

A endpoint looks like this:

@Get()
  @ApiBearerAuth()
  @ApiOkResponse({ description: 'Users found successfully.' })
  @ApiInternalServerErrorResponse({ description: 'Internal server error.' })
  @ApiUnauthorizedResponse({ description: 'Unauthorized.' })
  @UseGuards(JwtAuthGuard)
  async findAll(): Promise<User[]> {
    return await this.msUsersService.findAll();
  }
lapis monolith
#

Are you getting a 401 response without getting into the strategy's validate?

long quartz
# lapis monolith Are you getting a 401 response without getting into the strategy's `validate`?

Hi Jay, thanks for replying, yes, I receive the unauthorized without entering the strategy validate.
What concerns me is the following:
Suppose I create a user makes a singup and receive his access token, then I delete it from the database, that token for some reason is still useful to enter the endpoints, the token is supposed to have the user's email to validate through the strategy's validate that the user indeed exists and is active.

lapis monolith
#

If you're getting a 401 and the validate isn't called, the token being sent is invalid, so even if the user was removed, the point is moot here

#

Could be expired, could be wrong format (should be Bearer <token>), could be invalid signature

#

You can ad this to your JwtAuthGuard to get more debugging details:

handleRequest(...args: Parameters<InstanceType<ReturnType<typeof AuthGuard>>['handleRequest']>) {
  console.log(args);
  return super.handleRequest(...args);
}
long quartz
lapis monolith
#

I assume you have class JwtAuthGuard extends AuthGuard('jwt'), correct?

long quartz
#

Oh, no...
Currently my JwtAuthGuard looks like this:

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(
    private readonly jwtService: JwtService,

    private readonly reflector: Reflector,

    @Inject(EnvConfig.KEY)
    private readonly configService: ConfigType<typeof EnvConfig>,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) return true;

    const request = context.switchToHttp().getRequest();
    const token = await this.extractTokenFromHeader(request);

    if (!token) throw new UnauthorizedException();

    try {
      console.log('Try');

      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.SECURITY.JWT_SECRET_KEY,
      });

      console.log({ payload });

      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }

    return true;
  }

  /**
   * Extract JWT token from request header
   * @param request Request object
   * @returns JWT token or undefined
   */
  private async extractTokenFromHeader(
    request: Request,
  ): Promise<string | undefined> {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

However, I can migrate to the AuthGuard extension.

lapis monolith
#

So you're using the passport strategy without using the passport guard?

long quartz
#

I really tried to guide me from some tutorials, the first implementation I saw used the CanActivate, however, I already changed it for the Guard, I'll try and confirm you how it works, ok? Thanks!

#

As far as I can see, the guard is acting up to here:

try {
      console.log('Try');

      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.SECURITY.JWT_SECRET_KEY,
      });

      console.log({ payload });

      request['user'] = payload;
    } catch {
      throw new UnauthorizedException();
    }

Never print the {payload}

#

My AuthModule looks like this:

@Module({
  imports: [
    ConfigModule,
    NatsClientModule,

    PassportModule.register({ defaultStrategy: 'jwt' }),

    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [EnvConfig.KEY],
      useFactory: (configService: ConfigType<typeof EnvConfig>) => ({
        global: true,
        secret: configService.SECURITY.JWT_SECRET_KEY,
        signOptions: { expiresIn: configService.SECURITY.JWT_EXPIRES_IN },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, JwtService],
})
export class AuthModule {}
lapis monolith
#

Then I can only assume you pass an invalid jwt, correct? You don't log any error information, just throw the exception

long quartz
#

Could it be a token problem?
It seems strange to me, jwt.io describes it correctly.

lapis monolith
#

Y'know, it'd start being easier to debug if you knew why. If only there were some way to log the error when it is caught after trying some code

long quartz
#

The catch prints this for me:
{
err: TypeError: Cannot read properties of undefined (reading 'decode')
at JwtAuthGuard.canActivate (C:\Users\juanc\OneDrive\Documentos\Neron\neron-api\api-gateway\dist\webpack:\api-gateway\src\auth\guards\jwt-auth.guard.ts:54:45)
}

#

In theory we are not injecting the JwtService properly, could it be?

lapis monolith
#

Where are you using decode?

long quartz
#

I was using it in the try catch, I went back to the verifyAsync but I get the same error

lapis monolith
#

How do you bind this guard? What is the import statement for the JwtService in the guard's file?

long quartz
#

Sure, I show you
First, as you saw, in the JwtAuthGuard I am injecting the JwtService bringing it from @proper prism/jwt.

Second, my AuthModule looks like this:First, as you saw, in the JwtAuthGuard I am injecting the JwtService bringing it from @proper prism/jwt.

Second, my AuthModule looks like this:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigType } from '@nestjs/config';
import { JwtModule, JwtService } from '@nestjs/jwt';
import EnvConfig from 'src/common/config/env/env-config';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { NatsClientModule } from 'src/common/config/nats/nats-client.module';
import { JwtStrategy } from './strategies/jwt.strategy';
import { PassportModule } from '@nestjs/passport';

@Module({
  imports: [
    ConfigModule,
    NatsClientModule,

    PassportModule.register({ defaultStrategy: 'jwt' }),

    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [EnvConfig.KEY],
      useFactory: (configService: ConfigType<typeof EnvConfig>) => ({
        global: true,
        secret: configService.SECURITY.JWT_SECRET_KEY,
        signOptions: { expiresIn: configService.SECURITY.JWT_EXPIRES_IN },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, JwtService],
})
export class AuthModule {}
lapis monolith
#

You didn't answer any of my questions

safe bay
#

Why is the JwtService in providers array? 🤔

long quartz
lapis monolith
lapis monolith
long quartz
#

I remove the JwtServices from the providers, right?

lapis monolith
#

Also, how do you bind the guard?

long quartz
lapis monolith
long quartz
lapis monolith
long quartz
#

Oh, ok

#

The project we are working on is an api-gateway of some microservices, so, inside the api-gateway I have a folder structure that looks like this:

src
_ auth (is the module that is registering the JwtModule and where I have the strategy and the guard)
_ modules
_ ms-users
_ other connections to other microservices

In ms-users I have a controller that looks something like this:

@UseGuards(JwtAuthGuard)
@Controller('users')
@ApiTags('Users')
export class MsUsersController {
  constructor(private readonly msUsersService: MsUsersService) {}

  @Get()
  @ApiBearerAuth()
  @ApiOkResponse({ description: 'Users found successfully.' })
  @ApiInternalServerErrorResponse({ description: 'Internal server error.' })
  @ApiUnauthorizedResponse({ description: 'Unauthorized.' })
  async findAll(): Promise<User[]> {
    return await this.msUsersService.findAll();
  }
}

The JwtAuthGuard comes from: import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';

long quartz
lapis monolith
long quartz
#

Yes I did, I will check again, thank you very much Jay. I hope I can solve, if suddenly not, I will consult again here, thanks friend.

long quartz
lapis monolith
#

Please, stop putting my name in every message, there's no need

long quartz
#

I put it in the providers of the module that is using the Guard, and indeed, now it recognizes the jwtService, but for some reason I return to the initial problem, we do not enter the strategy

#

Indeed the Unauthorized error went away, but even when the user does not exist in the database.

#

Can I show you how the Guard and Strategy are currently doing?

long quartz
#

Ok, I'll show you the workflow

In my user module I imported the JwtService as a provider and the Guard works, so I'll show you some details like the Guard, the Strategy, the AuthModule and finally how I use the Guard in a controller.

#

guard:

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(
    private readonly jwtService: JwtService,

    private readonly reflector: Reflector,

    @Inject(EnvConfig.KEY)
    private readonly configService: ConfigType<typeof EnvConfig>,
  ) {
    super();
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) return true;

    const request = context.switchToHttp().getRequest();
    const token = await this.extractTokenFromHeader(request);

    if (!token) throw new UnauthorizedException();

    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.SECURITY.JWT_SECRET_KEY,
      });

      request.user = payload;
    } catch (err) {
      console.log({ err });
      throw new UnauthorizedException();
    }

    return true;
  }

  /**
   * Extract JWT token from request header
   * @param request Request object
   * @returns JWT token or undefined
   */
  private async extractTokenFromHeader(
    request: Request,
  ): Promise<string | undefined> {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}
#

strategy:

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @Inject(EnvConfig.KEY) private configService: ConfigType<typeof EnvConfig>,

    private readonly authService: AuthService,
  ) {
    console.log('JWT STRATEGY');
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.SECURITY.JWT_SECRET_KEY,
    });
  }

  /**
   * Validate if the token matches the user in the payload
   * @param payload JWT Payload
   * @returns User and token data
   */
  async validate(payload: IJwtPayload): Promise<IAuthResponse> {
    console.log('VALIDATE JWT STRATEGY');
    const { id } = payload;

    const validate = await this.authService.validateUser(id);

    return validate;
  }
}

AuthModule:

@Module({
  imports: [
    NatsClientModule,

    PassportModule.register({ defaultStrategy: 'jwt' }),

    JwtModule.registerAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        global: true,
        secret: configService.get<string>('JWT_SECRET_KEY'),
        signOptions: { expiresIn: configService.get<string>('JWT_EXPIRES_IN') },
      }),
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtStrategy, JwtAuthGuard, JwtService],
})
export class AuthModule {}
#

Finally, this is how I use the guard in a controller:

#
@UseGuards(JwtAuthGuard)
@Controller('users')
@ApiTags('Users')
export class MsUsersController {
  constructor(private readonly msUsersService: MsUsersService) {}

  @Get()
  @ApiBearerAuth()
  @ApiOkResponse({ description: 'Users found successfully.' })
  @ApiInternalServerErrorResponse({ description: 'Internal server error.' })
  @ApiUnauthorizedResponse({ description: 'Unauthorized.' })
  async findAll(): Promise<User[]> {
    return await this.msUsersService.findAll();
  }
}
lapis monolith
#

Do you understand how class extension and classes in general work, with regards to Object Oriented Programming?

lapis monolith
# long quartz Yes

So you're then aware that doing extends AuthGuard('jwt') in your JwtAuthGuard is doing nothing, because there's no super.canActivate, right?

long quartz
#

Ok, I understand... I should pass you the context then, right?

lapis monolith
#

To super.canActivate? Yes

long quartz
#

How can I pass a context to this super? With the request on the controller?

lapis monolith
#

By doing that, it also means you don't need to extract the jwt from the authorization header, verify its signature, and validate the format of the header, because passport will do that all for you. It'll also already assign req.user to the return of the related Strategy's validate method

lapis monolith
long quartz
#

Ok, I understand, but I still don't understand how to pass the request to the guard, or rather, how to pass it to super.canActivate.

lapis monolith
#

You don't call the guard, the framework does, and it knows what to pass to canActivate, that's why all guards end up implementing the CanActivate interface

long quartz
#

In other words...
I should do this super.canActivate(context);, right?

async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) return true;

    const request = context.switchToHttp().getRequest();
    const token = await this.extractTokenFromHeader(request);

    if (!token) throw new UnauthorizedException();

    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.SECURITY.JWT_SECRET_KEY,
      });

      request.user = payload;
    } catch (err) {
      console.log({ err });
      throw new UnauthorizedException();
    }

    super.canActivate(context);

    return true;
  }
lapis monolith
#

Yes, but like I said, everything you have about this.extractTokenFromHeader and this.jwtService.verifyAsync will already be handled by passport under the hood

long quartz
#

Great, it was a matter of calling the super, I don't know why I didn't think of it before....

lapis monolith
#

So instead, you could do something like:

async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) return true;

    return super.canActivate(context) as Promise<boolean>;
  }
long quartz
#

Dude, thanks for the patience, haha. It worked, you are a pro and I assure you that you helped someone who is looking to learn and improve!

lapis monolith
#

It also helps to read the docs 😉