nestjspassport.jspassport-local

Nestjs returns 401 (Unauthorized) even with valid user ft. passport-local


Hi awesome developers,

I'm trying to implement Authentication using passport-local and Nestjs with reference to https://progressivecoder.com/how-to-implement-nestjs-passport-authentication-using-local-strategy/.

I have implemented exactly same but Nestjs always returns 401 Unauthorized even with valid user. I can't seem to find what I am missing.

Code Structure

Authentication Module
User Module

Here's the code:

Authentication Module:

authentication.module.ts

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from 'src/user/user.module';
import { AuthenticationController } from './controllers/authentication.controller';
import { AuthenticationService } from './services/authentication.service';
import { LocalStrategy } from './strategies/local.strategy';

@Module({
  imports:[UserModule, PassportModule],
  controllers: [AuthenticationController],
  providers: [AuthenticationService, LocalStrategy]
})
export class AuthenticationModule {}

authentication.controller.ts

import { Controller, Post, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { UserService } from 'src/user/services/user.service';

@Controller('authentication')
export class AuthenticationController {

    constructor(private userService: UserService){}

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

authentication.service.ts

import { Injectable } from '@nestjs/common';
import { UserService } from 'src/user/services/user.service';

@Injectable()
export class AuthenticationService {

    constructor(private userService: UserService) {}
    
    async validateUser(email: string, password: string): Promise<any> {
        const user = await this.userService.readUserByEmail(email);

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

local.strategy.ts

import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-local";
import { AuthenticationService } from "../services/authentication.service";

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy){

    constructor(private authenticationService: AuthenticationService){
        super();
    }
    
    async validate(username: string, password: string): Promise<any> {
        const user = await this.authenticationService.validateUser(username, password);

        if (!user) {
            throw new UnauthorizedException();
        }

        return user;
    }
}

User Module:

user.module.ts

import { Module } from '@nestjs/common';
import { UserController } from './controllers/user.controller';
import { UserService } from './services/user.service';

@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService]
})
export class UserModule {}

user.controller.ts

Skipping user controller as it is irrelevent

user.service.ts

import { Injectable } from '@nestjs/common';
import { prisma } from 'src/main';
import { CreateUserDTO } from '../dto/create-user.dto';
import { UpdateUserDTO } from '../dto/update-user.dto';

@Injectable()
export class UserService {

    private readonly users = [
        {
            id: "1",
            name: "Ajitesh",
            email: "ajitesh@example.com",
            password: "secret"
        }
    ]

    //....other methods

    async readUserByEmail(email: string){
        return this.users.find(user => user.email === email);
    }
}

Request:

{
 "email": "ajitesh@example.com",
 "password": "secret"
}

Thanks in advance.


Solution

  • passport-local expects req.body to be populated with username and password fields. If you plan to use something else for the username, like email, then you need to tell passport about that in your strategy using the usernameField option in super

    import { Injectable, UnauthorizedException } from "@nestjs/common";
    import { PassportStrategy } from "@nestjs/passport";
    import { Strategy } from "passport-local";
    import { AuthenticationService } from "../services/authentication.service";
    
    @Injectable()
    export class LocalStrategy extends PassportStrategy(Strategy){
    
        constructor(private authenticationService: AuthenticationService){
            super({
                usernameField: 'email',
            });
        }
        
        async validate(username: string, password: string): Promise<any> {
            const user = await this.authenticationService.validateUser(username, password);
    
            if (!user) {
                throw new UnauthorizedException();
            }
    
            return user;
        }
    }