Reputation: 6580
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
Upvotes: 2
Views: 1448
Reputation: 6580
But I finally figured out how to do it. I got almost everything right, except for the right way to put the keys in .env
. This is the correct way:
JWT_PUBLIC_KEY_BASE64="-----BEGIN PUBLIC KEY-----\nMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA5w7oeLUYmCBB6kvpfU1fp5nq93SI\n3nZ/Ihv8fxIgYlK1XEIp6MxjdzK1+O9ykIGuSFVAzo8xvSbmkHOyGYHn+AoBKFat\nCmfn2hUw41xQcQiHV7ZCljAobmFfHNH0U5SXlqvNv4urZWcDmKOThB1sOsQhju79\n5gjYoauIaR741sVlf9o=\n-----END PUBLIC KEY-----\n"
JWT_PRIVATE_KEY_BASE64="-----BEGIN EC PRIVATE KEY-----\nMIHcAgEBBEIA1yAjkQ36YE8fzrqorkP++eFQkTHY4RGdXXkI7EsnyW9mS3lpPvd5\ny4+oZyPfr3wEvgpendFV13CJzgGG5Oy2jVWgBwYFK4EEACOhgYkDgYYABADnDuh4\ntRiYIEHqS+l9TV+nmer3dIjedn8iG/x/EiBiUrVcQinozGN3MrX473KQga5IVUDO\njzG9JuaQc7IZgef4CgEoVq0KZ+faFTDjXFBxCIdXtkKWMChuYV8c0fRTlJeWq82/\ni6tlZwOYo5OEHWw6xCGO7v3mCNihq4hpHvjWxWV/2g==\n-----END EC PRIVATE KEY-----\n"
Please note the start/end double quotes, the \n
where was a new line (there are several along the line, the \n
before -----END * and the \n
on the very end.
Thanks to user @MarceloFonseca for this answer here:https://stackoverflow.com/a/61978298/2752520
Here goes the corrected code:
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) => {
return {
privateKey: configService.get<string>('JWT_PRIVATE_KEY_BASE64', ''),
publicKey: configService.get<string>('JWT_PUBLIC_KEY_BASE64', ''),
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, private 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, {
secret: this.configService.get('JWT_PRIVATE_KEY_BASE64', ''),
algorithm: 'ES512'
});
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';
import { readFileSync } from 'node:fs';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private dbRepo: DbRepo,
private configService: ConfigService,
) {
super({
secretOrKey: configService.get<string>('JWT_PUBLIC_KEY_BASE64', ''),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
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:
APP_PORT=3000
APP_GLOBAL_PREFIX=tickets
JWT_SECRET=abcdABCD1234554321
JWT_PUBLIC_KEY_BASE64="-----BEGIN PUBLIC KEY-----\nMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA5w7oeLUYmCBB6kvpfU1fp5nq93SI\n3nZ/Ihv8fxIgYlK1XEIp6MxjdzK1+O9ykIGuSFVAzo8xvSbmkHOyGYHn+AoBKFat\nCmfn2hUw41xQcQiHV7ZCljAobmFfHNH0U5SXlqvNv4urZWcDmKOThB1sOsQhju79\n5gjYoauIaR741sVlf9o=\n-----END PUBLIC KEY-----\n"
JWT_PRIVATE_KEY_BASE64="-----BEGIN EC PRIVATE KEY-----\nMIHcAgEBBEIA1yAjkQ36YE8fzrqorkP++eFQkTHY4RGdXXkI7EsnyW9mS3lpPvd5\ny4+oZyPfr3wEvgpendFV13CJzgGG5Oy2jVWgBwYFK4EEACOhgYkDgYYABADnDuh4\ntRiYIEHqS+l9TV+nmer3dIjedn8iG/x/EiBiUrVcQinozGN3MrX473KQga5IVUDO\njzG9JuaQc7IZgef4CgEoVq0KZ+faFTDjXFBxCIdXtkKWMChuYV8c0fRTlJeWq82/\ni6tlZwOYo5OEHWw6xCGO7v3mCNihq4hpHvjWxWV/2g==\n-----END EC PRIVATE KEY-----\n"
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
}
I pushed the changes to the repo I had created. So now there's a working example there with asymetric validation on NestJs and Passport.
Upvotes: 1