Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nest.js Auth Guard JWT Authentication constantly returns 401 unauthorized

Using Postman to test my endpoints, I am able to successfully "login" and receive a JWT token. Now, I am trying to hit an endpoint that supposedly has an AuthGuard to ensure that now that I am logged in, I can now access it.

However, it constantly returns 401 Unauthorized even when presented the JWT token in Postman.

Here is my code:

user.controller.ts

@Controller('users')
export class UsersController {
    constructor(private readonly usersService: UsersService) {}

    @UseGuards(AuthGuard())
    @Get()
    getUsers() {
        return this.usersService.getUsersAsync();
    }
}

jwt.strategy.ts

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor(
        private readonly authenticationService: AuthenticationService,
    ) {
        super({
            jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
            ignoreExpiration: false,
            secretOrKey: 'SuperSecretJWTKey',
        });
    }

    async validate(payload: any, done: Function) {
        console.log("I AM HERE"); // this never gets called.
        const user = await this.authenticationService.validateUserToken(payload);

        if (!user) {
            return done(new UnauthorizedException(), false);
        }

        done(null, user);
    }
}

I have tried ExtractJWT.fromAuthHeaderWithScheme('JWT') as well but that does not work.

authentication.module.ts

@Module({
    imports: [
        ConfigModule,
        UsersModule,
        PassportModule.register({ defaultStrategy: 'jwt' }),
        JwtModule.register({
            secret: 'SuperSecretJWTKey',
            signOptions: { expiresIn: 3600 },
        }),
    ],
    controllers: [AuthenticationController],
    providers: [AuthenticationService, LocalStrategy, JwtStrategy],
    exports: [AuthenticationService, LocalStrategy, JwtStrategy],
})
export class AuthenticationModule {}

authentication.controller.ts

@Controller('auth')
export class AuthenticationController {
    constructor(
        private readonly authenticationService: AuthenticationService,
        private readonly usersService: UsersService,
    ) {}

    @UseGuards(AuthGuard('local'))
    @Post('login')
    public async loginAsync(@Response() res, @Body() login: LoginModel) {
        const user = await this.usersService.getUserByUsernameAsync(login.username);

        if (!user) {
            res.status(HttpStatus.NOT_FOUND).json({
                message: 'User Not Found',
            });
        } else {
            const token = this.authenticationService.createToken(user);
            return res.status(HttpStatus.OK).json(token);
        }
    }
}

In Postman, I am able to use my login endpoint to successfully login with the proper credentials and receive a JWT token. Then, I add an Authentication header to a GET request, copy and paste in the JWT token, and I have tried both "Bearer" and "JWT" schemes and both return 401 Unauthorized as you can see in the images below.

enter image description here

enter image description here

I used the JWT.IO debugger, to check if there's anything wrong with my token and it appears correct: enter image description here

I am at a lost as to what could be the issue here. Any help would be greatly appreciated.

like image 586
noblerare Avatar asked Dec 14 '22 08:12

noblerare


1 Answers

Note that the validate() function in your JWT strategy is only called after successful validation of the JWT. If you are consistently getting a 401 response when trying to use the JWT then you can't expect this function to be called.

The return from the validate() method is injected into the request object of any operation that's guarded with JWT authentication.

I'm not sure about the done() function that you're calling, but here's a working validate() method from a current project of mine:

async validate(payload: JwtPayload): Promise<User> {
  const { email } = payload
  const user = await this.authService.getActiveUser(email)

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

  return user
}

It looks like you're on the right track in the desire to return a user. Be sure that's what authenticationService.validateUserToken() actually does.

In the strategy, jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken() seems correct, and in Postman using Authorization header with Bearer TOKEN also looks correct.

Regarding your authentication.controller.ts file, be careful about using @Request and @Response objects directly within your controllers in NestJS. These access the underlying framework e.g. Express and are liable to bypass many of the features implemented by Nest. Refer to https://docs.nestjs.com/faq/request-lifecycle to see what you're skipping out on...

You can return objects and throw errors directly from a decorated controller method (e.g. @Get(), Post(), etc) in NestJS and the framework will take care of the rest: HTTP code, JSON, etc.

From your controller consider ditching the @Reponse res and using throw new UnauthorizedException('User Not Found') and a simple return { token } (or similar) approach instead.

In your protected route I have found that explicitly declaring AuthGuard('jwt') works better and doesn't produce warnings in certain cases, even if you did set your default strategy to be JWT.

Do you actually need the AuthGuard('local') on your login route?

Inside your loginAsync() method DO NOT forget the crucial step of actually signing your token with your payload. You didn't provide your code for the createToken() method implementation in your authentication service, but I suspect that this may be what you're missing.

Consider this working implementation of a login service (which is simply called by it's controller's login function):

  async login(authCredentialsDto: AuthCredentialsDto): Promise<{ accessToken: string }> {
    const { email, password } = authCredentialsDto

    const success = await this.usersRepository.verifyCredentials(email, password)

    if (!success) {
      throw new UnauthorizedException('Invalid credentials')
    }

    // roles, email, etc can be added to the payload - but don't add sensitive info!
    const payload: JwtPayload = { email } 
    const accessToken = this.jwtService.sign(payload)

    this.logger.debug(`Generated JWT token with payload ${JSON.stringify(payload)}`)

    return { accessToken }
  }

Note that the jwtService is injected into the class via Dependency Injection by adding private jwtService: JwtService to the constructor params.

Also note in the above how an interface is defined for the JwtPayload so it is explicitly typed. This is better than using any as you are in your code.

Finally, if your JWT still doesn't validate, make absolutely sure that you are correctly using your token in Postman. Be extremely careful that you're not adding leading/trailing spaces, newlines, etc. I have made this mistake myself. You may want to sanity check by writing a quick JS file to try your API and make a fetch request that sets the Authorization header with value Bearer ${token}.

I hope this helps, good luck!

like image 66
firxworx Avatar answered Dec 31 '22 10:12

firxworx