20

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.

Artyom Ionash
  • 405
  • 7
  • 17
noblerare
  • 10,277
  • 23
  • 78
  • 140
  • The problem may be in your request from Postman. Try to create new request and be cautious what you place in headers. If you are using bearer token place it in auth section, not in headers. Or place it in headers, not in auth section. Make a few experiments, it can help. – James Kent Jan 31 '21 at 09:49

10 Answers10

14

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!

Artyom Ionash
  • 405
  • 7
  • 17
firxworx
  • 1,161
  • 11
  • 10
  • 1
    Thank you. This was a great help. What was happening is that I was using a package called `jsonwebtoken` and doing something like `import * as jwt from 'jsonwebtoken'` followed by `jwt.sign(...)`. When using the actual `JwtService` from `@nestjs/jwt`, that fixed it. – noblerare Jul 09 '20 at 15:38
  • I'm glad this helped, for sure using the actual `@nestjs/jwt` is the way to go with this approach! Cheers! – firxworx Jul 09 '20 at 17:17
  • Writing a quick JS file helped me find the problem. Thank you for the suggestion! – Saief El Gebali Feb 17 '22 at 17:18
7

I had the exact same problem.My problem was that JwtModule secret and JwtStrategy secretOrKey was different. Hope this might help someone stuck with this!

hansss
  • 321
  • 4
  • 4
  • I have exactly same problem! Related to .env not being loaded correctly. It's the 3rd problem I've noticed related to this and still can't figure it out. – Pramus Nov 06 '20 at 20:54
7

I had the same issue.

The issue in my case that the validate endpoint params where email and password,
while the nest auth documentation states that they should be username and password as the following:

  async validate(username: string, password: string): Promise<any> {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    return user;
  }

Also be aware to send username and password in the body of the request.

Credits: https://github.com/nestjs/docs.nestjs.com/issues/875#issuecomment-619472086

Artyom Ionash
  • 405
  • 7
  • 17
Safi Habhab
  • 981
  • 10
  • 17
5

Mine was that I was using RS256 algorithm to sign the JWT and I had a "Invalid algorithm" error.

So I added the "RS256" to my jwtStrategy constructor so now it look like this:

constructor(private configService: ConfigService) {
  super({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    ignoreExpiration: false,
    algorithms:["RS256"],
    secretOrKey: configService.get('jwtPublicKey'),
  });
}

Then it gave me an error complaining about "no start line" on my public key file, the error was that I had an ssh-rsa key format instead of rsa-pem format, I solved with Get PEM file from ssh-rsa key pair

And the finally it worked.

I got all this info, putting a logger between the strategy output and the guard output, doing this JWT Auth Guard example

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
4

I had a similar 401 status. My problem was that the token expiration was really short (60s). Make sure also to have a reasonable expiration period when testing jwt.

soscler
  • 227
  • 4
  • 4
  • I had the same problem. Copied the code from the official documentation with '60s' :) Thanks man! – RobbeR Mar 03 '23 at 11:45
0

I figured it out, make sure you are using Form-Encoded instead of plain form. otherwise request fields will not detect and validate method wont be called by passport. make sure you are using correct payload method (body,form-encoded,json) something like that.

Vikas Kandari
  • 1,612
  • 18
  • 23
0
Make Sure that inside your strategy the path of secretKey in 
secretOrKey is implemented correctly.

    
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: appConfig().appSecret,
    });
  }
 
    async validate(payload: any) {
    return { userId: payload.userId, username: payload.username };
  }
}
Vivek Raj
  • 1
  • 1
0

I had a similar 401 status. My problem was that the token expiration was set to '240' I thought wrong it is 240 seconds... but not it was 240 miliseconds... after i change to '240s' it works fine../ read more about the 'zeit/ms' format here

yehonatan yehezkel
  • 1,116
  • 18
  • 28
0

Make sure you ctrl-s Postman after setting the token. ‍♂️

Soyal7
  • 381
  • 5
  • 10
0
const token = await response.json()
token.accessToken

i didnt notice that the jwt was an object so when posting it make sure to pass it as a jwt string