I'm trying to understand jwt and authentication using nestJS. I've created two separate microservices, one of them is an auth service, after successful login the client gets jwt token and with this token he can access to the other microservice.
Here is the code of the JwtStrategy and AuthModule of the auth service :
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'secretKey'
});
}
async validate(payload: any) {
return payload;
}
}
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { PassportModule } from '@nestjs/passport';
import { LocalStrategy } from './local.strategy';
import { JwtStrategy } from './jwt.strategy';
import { JwtModule } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { AuthController } from './auth.controller';
import * as fs from 'fs';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: 'secretKey',
signOptions: { expiresIn: '1h' },
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
And here is the code of the other service:
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'secretKey',
});
}
async validate(payload: any) {
return payload;
}
}
I've figured out that there is no sense in using the same secret key for both services(because if I will create 10 microservices I won't use the same key for all of them) so I create a private and public key using openssl. At the AuthModule, I copy the private key instead of the 'secretKey' string and at the other service I copy the public key instead of the 'secretKey' string but I get a 401, unauthorized error. What have I missed here? why the JwtStrategy doesn't verify the public key?
With JWT, the possession and the use of the key materials are exactly the same as any other contexts where cypher operations occur. For signing: The private key is owned by the issuer and is used to compute the signature. The public key can be shared with all parties that need to verify the signature.
Security-wise, SWT can only be symmetrically signed by a shared secret using the HMAC algorithm. However, JWT and SAML tokens can use a public/private key pair in the form of a X.509 certificate for signing.
Since it's been days, I am guessing this was solved. I am just adding my two cents here for future readers.
The problem lies with the JwtModule and the JwtStrategy instantiation. They aren't configured properly. You need to pass in the algorithms you'd use for signing and verifying the tokens, along with the keys. To verify whether the tokens are actually getting generated with the RS256 algo, check the header in the token at https://jwt.io/. It would probably show HS256, and since your code didn't use the correct algorithm to sign the token. And it fails while token gets verified using the public key.
To generate signed tokens properly with the RSA key pair:
Auth Module
@Module({
imports: [
ConfigModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
const options: JwtModuleOptions = {
privateKey: configService.get('JWT_PRIVATE_KEY'),
publicKey: configService.get('JWT_PUB LIC_KEY'),
signOptions: {
expiresIn: '3h',
issuer: '<Your Auth Service here>',
algorithm: 'RS256',
},
};
return options;
},
inject: [ConfigService],
}),
],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
controllers: [AuthController],
})
export class AuthModule {}
Auth Service
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
) {}
async generateToken(
user: User,
signOptions: jwt.SignOptions = {},
): Promise<AuthJwtToken> {
const payload = { sub: user.id, email: user.email, scopes: user.roles };
return {
accessToken: this.jwtService.sign(payload, signOptions),
};
}
}
JwtStrategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get('JWT_PUBLIC_KEY'),
algorithms: ['RS256'],
});
}
async validate(payload: any) {
const { sub: userId, email, scopes: roles } = payload;
return {
id: userId,
email,
roles,
};
}
}
In your other micro-services, You can use the same JwtStrategy we used in the Auth module.
Since you're creating a distributed app, you need to share the PUBLIC_KEY with the other micro-services by manually adding the key or by exposing it using some API endpoint. Either way, you have to use the PUBLIC_KEY for the other services to verify. You must not share or expose the PRIVATE_KEY.
NOTE: The following code assumes a ConfigService which would provide the RSA key pair form env. It's strongly suggested not to check in the keys in the code.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With