4

I've followed the official Nest doc (https://docs.nestjs.com/security/authentication) step by step, but I can't get validate() method called when using @AuthGuard('local') or @AuthGuard(LocalAuthGuard) on login action.

If I don't use that guard decorator, all works as expected (but I need to use it to get my token added to request object).

auth.controller.ts

  @UseGuards(AuthGuard('local')) // or AuthGuard(LocalAuthGuard)
  @Post('login')
  async login(
    @Request() req
  ) {
    const { access_token } = await this.authService.login(req.user);
    return access_token;
  }
}

local.strategy.ts

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private authService: AuthService) {
    super({ usernameField: 'email' });
  }

  async validate(email: string, password: string): Promise<any> { // class is constructed but this method is never called
    const user: UserDto = await this.authService.login({
      email,
      password,
    });
    
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }
}

auth.module.ts

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.register({
      secret: "bidon", 
      signOptions: {
        expiresIn: '3600',
      },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  exports: [AuthService, PassportModule, JwtModule],
  controllers: [AuthController],
})
export class AuthModule {}

PS : I've already read all stack overflow related posts (for exemple : NestJS' Passport Local Strategy "validate" method never called) but they didn't help me.

Artyom Ionash
  • 405
  • 7
  • 17
Gigs
  • 199
  • 1
  • 12
  • Are you sending a POST request with an `email` and `password` property in the `body` of the request? – Jay McDoniel Jan 28 '22 at 16:58
  • Yes, exactly... – Gigs Jan 28 '22 at 17:01
  • In your `LocalAuthGuard` can you add `handleRequest(err, user, info, context, status) { console.log({ err, user, info, context, status}); return super.handleRequest(err, user, info, context, status); }`? Should print out whatever errors passport is saying there are – Jay McDoniel Jan 28 '22 at 17:14
  • Thanks for trying to help @JayMcDoniel . I really can't understand what I was doing wrong ! I don't remember changing anything and now it works as expected (after many heures of debbuging). Thanks again. – Gigs Jan 30 '22 at 17:14

5 Answers5

6

I found that if we don't pass email or password, also the wrong value of both, the guard will response Unauthorized message. The problem is how to ensure that validation of the required field before run guard logic if it not defined, In other words, frontend not pass it to server. If we add @Body() data: loginDto in controller method, it won't validate the body params.

to solve it, I add some validation code in local.guard.ts file. Here is my code in my project:

import { HttpException, HttpStatus, Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
  handleRequest(err, user, info, context, status) {
    const request = context.switchToHttp().getRequest();
    const { mobile, password } = request.body;
    if (err || !user) {
      if (!mobile) {
        throw new HttpException({ message: '手机号不能为空' }, HttpStatus.OK);
      } else if (!password) {
        throw new HttpException({ message: '密码不能为空' }, HttpStatus.OK);
      } else {
        throw err || new UnauthorizedException();
      }
    }
    return user;
  }
}
jenemy
  • 91
  • 3
  • 1
    Hello @jenemy ! For this case, you can take a look on ValidationPipe class (from @nestjs/common). It allows you to verify your params. I use it like this : `@Body(new ValidationPipe()) createUserDto: CreateUserDto): Promise ` on controller, and `export class CreateUserDto { @IsNotEmpty() alias: string; @IsNotEmpty() @IsEmail() email: string; @IsNotEmpty() password: string; }` on my createUserDto. It works great! – Gigs Feb 09 '22 at 09:29
  • 1
    I had add ValidationPipe in `main.ts` as a global Pipe, but it still not works great for me. If I removed `@UseGuards(LocalAuthGuard)` decorator, the validation works well. I also tried it on another nest.js project it does't works right like above. – jenemy Feb 10 '22 at 01:54
3

ValidationPipe doesn't validate your request. Because, Gurads are executed before any interceptor or pipe. But, guards are executed after middleware. So, we can create a validation middleware to solve this issue. Here is my solution. Hope it will help somebody.

login.dto.ts

import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty } from 'class-validator';

export class LoginDto {
  @ApiProperty({ required: true })
  @IsNotEmpty()
  @IsEmail()
  username: string;

  @ApiProperty({ required: true })
  @IsNotEmpty()
  password: string;
}

authValidationMiddleware.ts

import {
  Injectable,
  NestMiddleware,
  BadRequestException,
} from '@nestjs/common';
import { Response, NextFunction } from 'express';
import { validateOrReject } from 'class-validator';
import { LoginDto } from '../dto/login.dto';

@Injectable()
export class AuthValidationMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    const body = req.body;
    const login = new LoginDto();
    const errors = [];

    Object.keys(body).forEach((key) => {
      login[key] = body[key];
    });

    try {
      await validateOrReject(login);
    } catch (errs) {
      errs.forEach((err) => {
        Object.values(err.constraints).forEach((constraint) =>
          errors.push(constraint),
        );
      });
    }

    if (errors.length) {
      throw new BadRequestException(errors);
    }

    next();
  }
}

auth.module.ts

import { MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthValidationMiddleware } from './middleware/authValidationMiddleware';

@Module({
  imports: ['.. imports'],
  controllers: [AuthController],
})
export class AuthModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthValidationMiddleware)
      .forRoutes({ path: 'auth/login', method: RequestMethod.POST });
  }
}
Arjun G
  • 2,194
  • 1
  • 18
  • 19
1

When you use NestJs Guard then it executed before Pipe therefore ValidationPipe() doesn't validate your request.

https://docs.nestjs.com/guards

Guards are executed after all middleware, but before any interceptor or pipe.

Tyler2P
  • 2,324
  • 26
  • 22
  • 31
user379367
  • 31
  • 4
1

This local strategy expects your body to have username and password fields, on your code change email to username

Bdayz
  • 11
  • 1
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Oct 02 '22 at 16:39
0

My use case requires only one parameter.

import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { Request } from 'express'
import { Strategy } from 'passport-custom'
import { AuthService } from '../auth.service'

@Injectable()
export class CustomStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super()
  }

  async validate(req: Request): Promise<any> {
    // req.body.xxx can verify any parameter
    if (!req.body.code) {
      throw new BadRequestException('Missing code parameters!')
      // Using the above, this is how the response would look:
      // {
      //   "message": "Missing code parameters!",
      //   "error": "Bad Request",
      //   "statusCode": 400,
      // }
    }
    const user = await this.authService.validateUser(req.body.code)
    console.log('user', user)
    if (!user) {
      throw new UnauthorizedException()
    }
    return user
  }
}
weiliang
  • 663
  • 8
  • 13