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.
The solution was:
const token = this.jwtService.sign(payload, { secret: 'topSecret' });
Thanks goes to MicaelLevi!