So my next step in NestJs is being able to use asymetric jwt validation with Passport. I've made it work with asymetric validation without using Passport and with symetric validation in Passport but when I change the fields to what I think it's correct to set to asymetric validation in Passport it doesn't work and I'm not being able to find any working asymetric examples. Here's what I got:
AuthModule:
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DbRepo } from 'src/dataObjects/dbRepo';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
const jwtFactory = {
useFactory: async (configService: ConfigService) => {
let privateKey = configService.get<string>('JWT_PRIVATE_KEY_BASE64', '');
let publicKey = configService.get<string>('JWT_PUBLIC_KEY_BASE64', '');
// let privateKey = configService.get<string>('JWT_SECRET', '');
return {
privateKey,
publicKey,
signOptions: {
expiresIn: configService.get('JWT_EXP_H'),
},
};
},
inject: [ConfigService],
};
@Module({
imports: [
JwtModule.registerAsync(jwtFactory),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
controllers: [AuthController],
providers: [AuthService, DbRepo, JwtStrategy],
exports: [DbRepo, JwtModule, JwtStrategy, PassportModule],
})
export class AuthModule { }
AuthController:
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserDto } from 'src/dataObjects/users-create-new.dto';
import { AuthService } from './auth.service';
import { User } from 'src/dataObjects/user.entity';
import { AuthCredentialsDto } from 'src/dataObjects/user-auth-credentials.dto';
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('/signin')
signin(
@Body() authCredentialsDto: AuthCredentialsDto,
): Promise<{ accessToken: string }> {
return this.authService.signin(authCredentialsDto);
}
@Post('/signup')
signup(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.authService.signup(createUserDto);
}
}
AuthService:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthCredentialsDto } from 'src/dataObjects/user-auth-credentials.dto';
import { User } from 'src/dataObjects/user.entity';
import { CreateUserDto } from 'src/dataObjects/users-create-new.dto';
import { DbRepo } from 'src/dataObjects/dbRepo';
import { JwtService } from '@nestjs/jwt';
import { UserJwtPayload } from 'src/dataObjects/user-jwt-payload.interface';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class AuthService {
constructor(private dbRepo: DbRepo, private jwtService: JwtService, configService: ConfigService) {}
async signup(createUserDto: CreateUserDto): Promise<User> {
return await this.dbRepo.createUser(createUserDto);
}
async signin(
authCredentialsDto: AuthCredentialsDto,
): Promise<{ accessToken: string }> {
const username: string = authCredentialsDto.username;
const user = await this.dbRepo.userFindByNameAndMatchingPassword(
authCredentialsDto,
);
if (user) {
const typeid = user.typeid;
const payload: UserJwtPayload = { username, typeid };
const accessToken: string = this.jwtService.sign(payload);
return { accessToken };
} else {
throw new UnauthorizedException('Incorrect login credentials!');
}
}
}
JwtStrategy:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserJwtPayload } from 'src/dataObjects/user-jwt-payload.interface';
import { User } from 'src/dataObjects/user.entity';
import { DbRepo } from 'src/dataObjects/dbRepo';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private dbRepo: DbRepo,
private configService: ConfigService,
) {
let publicKey = configService.get<string>('JWT_PUBLIC_KEY_BASE64', '');
// let publicKey = configService.get<string>('JWT_SECRET', '');
super({
secretOrKey: publicKey,
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
algorithms: ['ES512']
});
}
async validate(payload: UserJwtPayload): Promise<User> {
console.log('payload', payload);
const { username, typeid } = payload;
const users: User[] = await this.dbRepo.getUsers({ username });
const user: User = users[0];
if (typeid > 2 || Object.keys(user).length <= 0) {
throw new UnauthorizedException();
}
return user;
}
}
.env.dev: (this is a study project, nothing here is production, so it doesn't matter to show the keys)
APP_PORT=3000
APP_GLOBAL_PREFIX=tickets
JWT_SECRET=abcdABCD1234554321
JWT_PUBLIC_KEY_BASE64=-----BEGIN PUBLIC KEY----- MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA5w7oeLUYmCBB6kvpfU1fp5nq93SI 3nZ/Ihv8fxIgYlK1XEIp6MxjdzK1+O9ykIGuSFVAzo8xvSbmkHOyGYHn+AoBKFat Cmfn2hUw41xQcQiHV7ZCljAobmFfHNH0U5SXlqvNv4urZWcDmKOThB1sOsQhju79 5gjYoauIaR741sVlf9o= -----END PUBLIC KEY-----
JWT_PRIVATE_KEY_BASE64=-----BEGIN EC PRIVATE KEY----- MIHcAgEBBEIA1yAjkQ36YE8fzrqorkP++eFQkTHY4RGdXXkI7EsnyW9mS3lpPvd5 y4+oZyPfr3wEvgpendFV13CJzgGG5Oy2jVWgBwYFK4EEACOhgYkDgYYABADnDuh4 tRiYIEHqS+l9TV+nmer3dIjedn8iG/x/EiBiUrVcQinozGN3MrX473KQga5IVUDO jzG9JuaQc7IZgef4CgEoVq0KZ+faFTDjXFBxCIdXtkKWMChuYV8c0fRTlJeWq82/ i6tlZwOYo5OEHWw6xCGO7v3mCNihq4hpHvjWxWV/2g== -----END EC PRIVATE KEY-----
JWT_EXP_H=3600s
JWT_EXP_D=1d
Guarded class:
<...>
@Controller('users')
@UseGuards(AuthGuard())
export class UsersController {
constructor(private userService: UsersService) {}
@Get()
async getUsers(@Headers('Authorization') authorization = '', @Query() filterDto: UserDataDto): Promise<User[]> {
return this.userService.getUsers(filterDto);
}
<...> more methods
}
What happens is that when I call signin it does emit a token, but if I put it in bearer token it returns unauthorized.
I tried to remove BEGIN/END text in the keys, but the result was the same. This very code works fine in the symetric version. I mean, if I remove public/privateKey and algorithms options from AuthModule and JwtStrategy and using only secretOrKey with JWT_SECRET environment variable.
Finally if I include algorithm: ['ES512']
in AuthModule's signOptions I get this error:
src/auth/auth.module.ts:33:27 - error TS2345: Argument of type '{ useFactory: (configService: ConfigService) => Promise<{ privateKey: string; publicKey: string; signOptions: { expiresIn: any; algorithm: string; }; }>; inject: (typeof ConfigService)[]; }' is not assignable to parameter of type 'JwtModuleAsyncOptions'.
The types returned by 'useFactory(...)' are incompatible between these types.
Type 'Promise<{ privateKey: string; publicKey: string; signOptions: { expiresIn: any; algorithm: string; }; }>' is not assignable to type 'JwtModuleOptions | Promise<JwtModuleOptions>'.
Type 'Promise<{ privateKey: string; publicKey: string; signOptions: { expiresIn: any; algorithm: string; }; }>' is not assignable to type 'Promise<JwtModuleOptions>'.
Type '{ privateKey: string; publicKey: string; signOptions: { expiresIn: any; algorithm: string; }; }' is not assignable to type 'JwtModuleOptions'.
The types of 'signOptions.algorithm' are incompatible between these types.
Type 'string' is not assignable to type 'Algorithm | undefined'.
33 JwtModule.registerAsync(jwtFactory),
I did this final test because it's advised in this SO question.
How can I make this work ?
Edit
I tried to change AuthService sign method to:
const accessToken: string = this.jwtService.sign(payload, { algorithm: 'ES512' });
But I got a very strange error:
Error: error:1E08010C:DECODER routines::unsupported
at Sign.sign (node:internal/crypto/sig:131:29)
at sign (/vagrant/node_modules/jwa/index.js:152:45)
at Object.sign (/vagrant/node_modules/jwa/index.js:200:27)
at Object.jwsSign [as sign] (/vagrant/node_modules/jws/lib/sign-stream.js:32:24)
at Object.module.exports [as sign] (/vagrant/node_modules/jsonwebtoken/sign.js:204:16)
at JwtService.sign (/vagrant/node_modules/@nestjs/jwt/dist/jwt.service.js:28:20)
at AuthService.signin (/vagrant/src/auth/auth.service.ts:31:51)
ES512 is supported by jsonwebtoken and by node. If it wasn´t my non-passport-jwt asymetric version wouldn't work.
Edit 2
I put the project in this github repo: https://github.com/nelson777/nest-asymetric-validation
Maybe it's useful if someone wants to run the project
This is a repository with symetric validation working: https://github.com/nelson777/nest-symetric-validation