From ad2fe754bf85c32b3b3aa7ed2d2393d1e791b0b3 Mon Sep 17 00:00:00 2001 From: Damien Baty Date: Fri, 23 May 2025 01:23:10 +0200 Subject: [PATCH] perf: Allow `secret` and `publicKey` options to be `crypto.KeyObject` When JwtService is initialized with `publicKey` as a string or Buffer, `verify()` and `verifyAsync()` pass it to "jsonwebtoken.verify()", which creates an instance of `crypto.KeyObject` from it via `crypto.createPublicKey()`. This is not free. Initializing `publicKey` with a `KeyObject` avoids this transformation in "jsonwebtoken". On my laptop, it makes `verify` twice faster. The same goes for `secret`, used in `sign()`, `verify()` and their asynchronous variants. Initializing with a `KeyObject` (built via `crypto.createSecretKey`) makes these functions ~50 times faster. See also auth0/node-jsonwebtoken@966. --- README.md | 10 +++ .../jwt-module-options.interface.ts | 4 +- lib/jwt.service.spec.ts | 75 +++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eef6c4e1..5666eede 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,16 @@ The `JwtModule` takes an `options` object: - `verifyOptions` [read more](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback) - `secretOrPrivateKey` (DEPRECATED!) [read more](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) +For performance purposes, it is advised to pass instances of `KeyObject` to `secret`, `privateKey` and `publicKey` properties of this `options` object. These `KeyObject` can be generated with `createSecretKey()`, `createPrivateKey()` and `createPublicKey()` from Node.js `crypto` module. For example: + +```typescript +import { createSecretKey } from 'crypto'; + +new JwtService({ + secret: createSecretKey(Buffer.from('the secret key')) +}); +``` + ## Support Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). diff --git a/lib/interfaces/jwt-module-options.interface.ts b/lib/interfaces/jwt-module-options.interface.ts index cea71ebc..6022e4cc 100644 --- a/lib/interfaces/jwt-module-options.interface.ts +++ b/lib/interfaces/jwt-module-options.interface.ts @@ -12,8 +12,8 @@ export enum JwtSecretRequestType { export interface JwtModuleOptions { global?: boolean; signOptions?: jwt.SignOptions; - secret?: string | Buffer; - publicKey?: string | Buffer; + secret?: jwt.Secret; + publicKey?: jwt.Secret; privateKey?: jwt.Secret; /** * @deprecated diff --git a/lib/jwt.service.spec.ts b/lib/jwt.service.spec.ts index 67c6fce0..8b1bdab2 100644 --- a/lib/jwt.service.spec.ts +++ b/lib/jwt.service.spec.ts @@ -1,3 +1,10 @@ +import { + createPrivateKey, + createPublicKey, + createSecretKey, + generateKeyPairSync, + KeyObject +} from 'crypto'; import { Test } from '@nestjs/testing'; import * as jwt from 'jsonwebtoken'; import { @@ -152,6 +159,44 @@ describe('JwtService', () => { }); }); + describe('should allow KeyObject for privateKey and publicKey', () => { + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } + }); + const privKeyAsObject: KeyObject = createPrivateKey(privateKey); + const pubKeyAsObject: KeyObject = createPublicKey(publicKey); + const testPayload = { foo: 'bar' }; + + let jwtService: JwtService; + + beforeEach(async () => { + jwtService = await setup({ + privateKey: privKeyAsObject, + publicKey: pubKeyAsObject, + signOptions: { algorithm: 'RS256' } + }); + verifySpy.mockRestore(); + signSpy.mockRestore(); + }); + + it('verifying should work', () => { + const token = jwtService.sign(testPayload); + + expect(jwtService.verify(token)).toHaveProperty('foo', 'bar'); + }); + + it('verifying (async) should work', () => { + const token = jwtService.sign(testPayload); + + expect(jwtService.verifyAsync(token)).resolves.toHaveProperty( + 'foo', + 'bar' + ); + }); + }); + describe('should use config.secretOrPrivateKey but warn about deprecation', () => { let jwtService: JwtService; let consoleWarnSpy: jest.SpyInstance; @@ -226,6 +271,36 @@ describe('JwtService', () => { }); }); + describe('should allow KeyObject for secret', () => { + const secretB64: KeyObject = createSecretKey( + Buffer.from('ThisIsARandomSecret', 'base64') + ); + const testPayload = { foo: 'bar' }; + + let jwtService: JwtService; + + beforeEach(async () => { + jwtService = await setup({ secret: secretB64 }); + verifySpy.mockRestore(); + signSpy.mockRestore(); + }); + + it('verifying should use base64 buffer key', () => { + const token = jwt.sign(testPayload, secretB64); + + expect(jwtService.verify(token)).toHaveProperty('foo', 'bar'); + }); + + it('verifying (async) should use base64 buffer key', async () => { + const token = jwt.sign(testPayload, secretB64); + + await expect(jwtService.verifyAsync(token)).resolves.toHaveProperty( + 'foo', + 'bar' + ); + }); + }); + describe('should use secret key from options', () => { let jwtService: JwtService; const testPayload: string = getRandomString();