Search code examples
nestjsnestjs-confignestjs-jwt

Nestjs with JWT - secretOrPrivateKey must have a value error - when calling a this.jwtService.sign()


I am building a simple Nestjs (v9) api and trying to implement the login functionality. Creating a user in the database worked (postgres, docker image) and the password is hashed. However, when I try to log in (http://localhost:3000/users/login), there is an exception, most likely on sign() function. I don't have a .env file yet, so the "secret" is not hidden.

When I try to log in with a valid password, I see the following error:

Password provided by user: testPassword
Hashed password found in database: $2b$10$Y.RV92fRdH0jdUq9etHqaeIHH/1BsN/dzt2McmK1usW8rIgoKZ6Zy
I can see it in the console
[Nest] 30807  - 05/19/2023, 6:26:58 PM   ERROR [ExceptionsHandler] secretOrPrivateKey must have a value
Error: secretOrPrivateKey must have a value
    at Object.module.exports [as sign]
    at JwtService.sign
    at AuthService.signIn

This is what my auth.service.ts class looks like:

// src/auth/auth.service.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { CreateUserDto } from '../users/dto/create-user.dto';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { AuthCredentialsDto } from './dto/auth-credentials.dto';
import { Repository } from 'typeorm';
import { UserEntity } from 'src/users/user.entity';

@Injectable()
export class AuthService {
  constructor(
    @InjectRepository(UserEntity)
    private userRepository: Repository<UserEntity>,
    private readonly jwtService: JwtService,
  ) {}

  async signUp(createUserDto: CreateUserDto) {
    const user = this.userRepository.create(createUserDto);

    // Hash the password
    const salt = await bcrypt.genSalt();
    user.password = await bcrypt.hash(user.password, salt);

    await this.userRepository.save(user);
  }

  async signIn(authCredentialsDto: AuthCredentialsDto) {
    const { username, password } = authCredentialsDto;
    const user = await this.userRepository.findOne({ where: { username } });
    console.log(`Password provided by user: ${password}`);
    console.log(`Hashed password found in database: ${user.password}`);
    console.log('I can see it in the console');

    console.log(
      'Payload: ' +
        this.jwtService.sign({ username: user.username, sub: user.id }),
    );
    console.log('I cannot see it in the console');

    if (!user || !(await bcrypt.compare(password, user.password))) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const payload = { username: user.username, sub: user.id };
    const token = this.jwtService.sign(payload);

    return { accessToken: token };
  }
}

And this is my auth.module.ts

// src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './jwt.strategy';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PassportModule } from '@nestjs/passport';
import { AuthController } from './auth.controller';
import { UsersModule } from 'src/users/users.module';
import { UserEntity } from 'src/users/user.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([UserEntity]),
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({
      secret: 'topSecret',
      signOptions: { expiresIn: '24h' },
    }),
    UsersModule,
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

I have also jwt-auth.guard.ts and jwt.strategy.ts. Guard:

// src/auth/jwt-auth.guard.ts

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.replace('Bearer ', '');

    if (!token) {
      return false;
    }

    try {
      const payload = this.jwtService.verify(token);
      request.user = payload;
      return true;
    } catch (error) {
      return false;
    }
  }
}

Strategy:

// src/auth/jwt.strategy.ts

import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtPayload } from './jwt-payload.interface';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '../users/user.entity';
import { Repository } from 'typeorm';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    @InjectRepository(UserEntity)
    private userRepository: Repository<UserEntity>,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'topSecret',
    });
  }

  async validate(payload: JwtPayload): Promise<UserEntity> {
    const { username } = payload;
    const user = await this.userRepository.findOne({ where: { username } });

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

    return user;
  }
}

I don't know if I'm doing this right. Please let me know what other files I should provide to find and fix the error.


Solution

  • The solution was:

    const token = this.jwtService.sign(payload, { secret: 'topSecret' });
    

    Thanks goes to MicaelLevi!