Local strategy

Using credentials for identification.

First of all, let's allow the AuthModule to use a usersRepository in its context. Therefore, let's insert in its imports the following.

TypeOrmModule.forFeature([User]),

Then, inject the repository in the AuthService.

@InjectRepository(User)
private readonly usersRepository: Repository<User>,

We'll now implement the method validateLocal(), which has email and password as parameters. This name is used for the method because Local is the passport's strategy for identifying a user through credentials, which will afterwards be used in conjunction with this method. Soon we'll understand this better.

async validateLocal(email: string, password: string) {}

Inside this method, first we find a user with this email and select only its id and password, as only these fields will be used.

const user = await this.usersRepository.findOne({
  select: {
    id: true,
    password: true,
  },
  where: { email },
});

After that, if the user is not found, we throw an exception.

if (!user) {
  throw new UnauthorizedException('Invalid email');
}

In this case, an UnauthorizedException is thrown due to the different context in which the user was not found.

Next, we should check if the provided password is correct. However, the password is now hashed before being stored in the database. Hence, we cannot make a simple comparison. We'll have to use the compare() method from the HashingService.

const isMatch = await this.hashingService.compare(password, user.password);
if (!isMatch) {
  throw new UnauthorizedException('Invalid password');
}

If you prefer, you can use a generic error message for a failed login attempt, in order to don't give away what a potential malicious user may be getting wrong. Something like 'Invalid credentials', for example.

Finally, we return the id. In a future moment, we'll use it to create a JWT.

return { id: user.id };

Now, we'll use the Local strategy. Create the file auth -> strategies -> local.strategy with following content. The usernameField field is necessary to indicate the name of the field used to identify the user if the default one isn't used, which is username.

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

  validate(email: string, password: string) {
    return this.authService.validateLocal(email, password);
  }
}

Strategy should be imported from passport-local.

After this, we should go back to the AuthModule and, in its imports, add the PassportModule. Then, in its providers, the LocalStrategy. With this, we're creating a flow that will be explained in a few moments.

The next step is to create a login() route. Let's do it in the AuthController. Don't worry, everything will be better explained very soon.

@UseGuards(AuthGuard('local'))
@Post('login')
login(@Request() req) {
  return req.user;
}

This is a POST route, therefore the default status if everything goes well is CREATED. However, this route does not create anything. For better semantic correctness, we can override the default status to OK by using over the route @HttpCode(HttpStatus.OK).

A Guard protects routes based on certain criteria. It returns a boolean, it being true when access is granted and false when it is denied. We are using on this route the AuthGuard of type local, but by using it like this we depend on magic strings. That is, strings that must be specific, untyped values to do something. So, to avoid this, let's create another guard to represent the same thing.

nest g gu auth/guards/local-auth

And with this content, it is the same thing, but we no longer need to insert a generic string.

export class LocalAuthGuard extends AuthGuard('local') {}

Now we can replace that guard with this new one.

@UseGuards(LocalAuthGuard)

This is a nice start. This route is still not performing the login, but we should stop now to discuss what we have done so far. I know it may have been a lot at once to digest, but let's analyse this entire flow carefully to better understand each step of this process before continuing.

  1. The login() route is being protected by an AuthGuard of type local. This means that, when this route is accessed, the LocalStrategy will be activated.

  2. Inside the LocalStrategy, it will be checked if the request has in its body the login fields: email and password. Then, the validate() method will be called, using them as arguments.

  3. Then, in the AuthService, in the method validateLocal(), the login attempt is validated through the process already explained previously. If successful, an object containing the user's id is returned.

  4. Finally, this return is appended to the request as the user field. That's why we can see the user's id beign returned. This will be used to perform the login in a future section. Before it, we'll just implement several improvements for better type safety.

Commit - Local strategy

Last updated