diff --git a/README.md b/README.md index c79cf95c..e749121c 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ export class AuthService { ## Secret / Encryption Key options -If you want to control secret and key management dynamically you can use the `secretOrKeyProvider` function for that purpose. +If you want to control secret and key management dynamically you can use the `secretOrKeyProvider` function for that purpose. You also can use asynchronous version of `secretOrKeyProvider`. +NOTE: For asynchronous version of `secretOrKeyProvider`, synchronous versions of `.sign()` and `.verify()` will throw an exception. ```typescript JwtModule.register({ @@ -153,6 +154,7 @@ The `JwtService` uses [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) #### jwtService.sign(payload: string | Object | Buffer, options?: JwtSignOptions): string The sign method is an implementation of jsonwebtoken `.sign()`. Differing from jsonwebtoken it also allows an additional `secret`, `privateKey`, and `publicKey` properties on `options` to override options passed in from the module. It only overrides the `secret`, `publicKey` or `privateKey` though not a `secretOrKeyProvider`. +NOTE: Will throw an exception for asynchronous version of `secretOrKeyProvider`; #### jwtService.signAsync(payload: string | Object | Buffer, options?: JwtSignOptions): Promise\ @@ -161,6 +163,7 @@ The asynchronous `.sign()` method. #### jwtService.verify\(token: string, options?: JwtVerifyOptions): T The verify method is an implementation of jsonwebtoken `.verify()`. Differing from jsonwebtoken it also allows an additional `secret`, `privateKey`, and `publicKey` properties on `options` to override options passed in from the module. It only overrides the `secret`, `publicKey` or `privateKey` though not a `secretOrKeyProvider`. +NOTE: Will throw an exception for asynchronous version of `secretOrKeyProvider`; #### jwtService.verifyAsync\(token: string, options?: JwtVerifyOptions): Promise\ @@ -173,7 +176,7 @@ The decode method is an implementation of jsonwebtoken `.decode()`. The `JwtModule` takes an `options` object: - `secret` is either a string, buffer, or object containing the secret for HMAC algorithms -- `secretOrKeyProvider` function with the following signature `(requestType, tokenOrPayload, options?) => jwt.Secret` (allows generating either secrets or keys dynamically) +- `secretOrKeyProvider` function with the following signature `(requestType, tokenOrPayload, options?) => jwt.Secret | Promise` (allows generating either secrets or keys dynamically) - `signOptions` [read more](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) - `privateKey` PEM encoded private key for RSA and ECDSA with passphrase an object `{ key, passphrase }` [read more](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback) - `publicKey` PEM encoded public key for RSA and ECDSA diff --git a/lib/index.ts b/lib/index.ts index 194bf8b8..7856c30c 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,4 @@ export * from './interfaces'; +export * from './jwt.errors'; export * from './jwt.module'; export * from './jwt.service'; diff --git a/lib/interfaces/jwt-module-options.interface.ts b/lib/interfaces/jwt-module-options.interface.ts index 8aab8699..d37ed58f 100644 --- a/lib/interfaces/jwt-module-options.interface.ts +++ b/lib/interfaces/jwt-module-options.interface.ts @@ -20,7 +20,7 @@ export interface JwtModuleOptions { requestType: JwtSecretRequestType, tokenOrPayload: string | object | Buffer, options?: jwt.VerifyOptions | jwt.SignOptions - ) => jwt.Secret; + ) => jwt.Secret | Promise; verifyOptions?: jwt.VerifyOptions; } @@ -46,3 +46,5 @@ export interface JwtVerifyOptions extends jwt.VerifyOptions { secret?: string | Buffer; publicKey?: string | Buffer; } + +export type GetSecretKeyResult = string | Buffer | jwt.Secret; diff --git a/lib/jwt.errors.ts b/lib/jwt.errors.ts new file mode 100644 index 00000000..5f9a2167 --- /dev/null +++ b/lib/jwt.errors.ts @@ -0,0 +1 @@ +export class WrongSecretProviderError extends Error {} diff --git a/lib/jwt.service.spec.ts b/lib/jwt.service.spec.ts index 39b33905..9fe63e0b 100644 --- a/lib/jwt.service.spec.ts +++ b/lib/jwt.service.spec.ts @@ -6,6 +6,7 @@ import { } from './interfaces/jwt-module-options.interface'; import { JwtModule } from './jwt.module'; import { JwtService } from './jwt.service'; +import { WrongSecretProviderError } from './jwt.errors'; const setup = async (config: JwtModuleOptions) => { const module = await Test.createTestingModule({ diff --git a/lib/jwt.service.ts b/lib/jwt.service.ts index 5d688c7f..d540babe 100644 --- a/lib/jwt.service.ts +++ b/lib/jwt.service.ts @@ -1,12 +1,14 @@ import { Inject, Injectable, Logger, Optional } from '@nestjs/common'; import * as jwt from 'jsonwebtoken'; import { + GetSecretKeyResult, JwtModuleOptions, JwtSecretRequestType, JwtSignOptions, JwtVerifyOptions } from './interfaces'; import { JWT_MODULE_OPTIONS } from './jwt.constants'; +import { WrongSecretProviderError } from './jwt.errors'; @Injectable() export class JwtService { @@ -35,6 +37,14 @@ export class JwtService { JwtSecretRequestType.SIGN ); + if (secret instanceof Promise) { + secret.catch(() => {}); // suppress rejection from async provider + this.logger.warn( + 'For async version of "secretOrKeyProvider", please use "signAsync".' + ); + throw new WrongSecretProviderError(); + } + const allowedSignOptKeys = ['secret', 'privateKey']; const signOptKeys = Object.keys(signOptions); if ( @@ -86,9 +96,13 @@ export class JwtService { } return new Promise((resolve, reject) => - jwt.sign(payload, secret, signOptions, (err, encoded) => - err ? reject(err) : resolve(encoded) - ) + Promise.resolve() + .then(() => secret) + .then((scrt: GetSecretKeyResult) => { + jwt.sign(payload, scrt, signOptions, (err, encoded) => + err ? reject(err) : resolve(encoded) + ) + }) ); } @@ -101,6 +115,14 @@ export class JwtService { JwtSecretRequestType.VERIFY ); + if (secret instanceof Promise) { + secret.catch(() => {}); // suppress rejection from async provider + this.logger.warn( + 'For async version of "secretOrKeyProvider", please use "verifyAsync".' + ); + throw new WrongSecretProviderError(); + } + return jwt.verify(token, secret, verifyOptions) as T; } @@ -117,9 +139,14 @@ export class JwtService { ); return new Promise((resolve, reject) => - jwt.verify(token, secret, verifyOptions, (err, decoded) => - err ? reject(err) : resolve(decoded as T) - ) + Promise.resolve() + .then(() => secret) + .then((scrt: GetSecretKeyResult) => { + jwt.verify(token, scrt, verifyOptions, (err, decoded) => + err ? reject(err) : resolve(decoded as T) + ) + }) + .catch(reject) ) as Promise; } @@ -148,13 +175,24 @@ export class JwtService { : this.options[key]; } + private overrideSecretFromOptions(secret: GetSecretKeyResult) { + if (this.options.secretOrPrivateKey) { + this.logger.warn( + `"secretOrPrivateKey" has been deprecated, please use the new explicit "secret" or use "secretOrKeyProvider" or "privateKey"/"publicKey" exclusively.` + ); + secret = this.options.secretOrPrivateKey; + } + + return secret; + } + private getSecretKey( token: string | object | Buffer, options: JwtVerifyOptions | JwtSignOptions, key: 'publicKey' | 'privateKey', secretRequestType: JwtSecretRequestType - ): string | Buffer | jwt.Secret { - let secret = this.options.secretOrKeyProvider + ): GetSecretKeyResult | Promise { + const secret = this.options.secretOrKeyProvider ? this.options.secretOrKeyProvider(secretRequestType, token, options) : options?.secret || this.options.secret || @@ -164,12 +202,8 @@ export class JwtService { this.options.publicKey) || this.options[key]; - if (this.options.secretOrPrivateKey) { - this.logger.warn( - `"secretOrPrivateKey" has been deprecated, please use the new explicit "secret" or use "secretOrKeyProvider" or "privateKey"/"publicKey" exclusively.` - ); - secret = this.options.secretOrPrivateKey; - } - return secret; + return secret instanceof Promise + ? secret.then((sec) => this.overrideSecretFromOptions(sec)) + : this.overrideSecretFromOptions(secret); } }