Skip to content

Commit

Permalink
Merge pull request #1486 from mr-yum/async-secret-or-key-provider
Browse files Browse the repository at this point in the history
feat(): support for asynchronous version of `secretOrKeyProvider`
  • Loading branch information
kamilmysliwiec authored Nov 9, 2023
2 parents d5401b1 + b0089b4 commit 5c1050e
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 18 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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\<string\>

Expand All @@ -161,6 +163,7 @@ The asynchronous `.sign()` method.
#### jwtService.verify\<T extends object = any>(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\<T extends object = any>(token: string, options?: JwtVerifyOptions): Promise\<T\>

Expand All @@ -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<jwt.Secret>` (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
Expand Down
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './interfaces';
export * from './jwt.errors';
export * from './jwt.module';
export * from './jwt.service';
4 changes: 3 additions & 1 deletion lib/interfaces/jwt-module-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface JwtModuleOptions {
requestType: JwtSecretRequestType,
tokenOrPayload: string | object | Buffer,
options?: jwt.VerifyOptions | jwt.SignOptions
) => jwt.Secret;
) => jwt.Secret | Promise<jwt.Secret>;
verifyOptions?: jwt.VerifyOptions;
}

Expand All @@ -46,3 +46,5 @@ export interface JwtVerifyOptions extends jwt.VerifyOptions {
secret?: string | Buffer;
publicKey?: string | Buffer;
}

export type GetSecretKeyResult = string | Buffer | jwt.Secret;
1 change: 1 addition & 0 deletions lib/jwt.errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class WrongSecretProviderError extends Error {}
1 change: 1 addition & 0 deletions lib/jwt.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
64 changes: 49 additions & 15 deletions lib/jwt.service.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
)
})
);
}

Expand All @@ -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;
}

Expand All @@ -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<T>;
}

Expand Down Expand Up @@ -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<GetSecretKeyResult> {
const secret = this.options.secretOrKeyProvider
? this.options.secretOrKeyProvider(secretRequestType, token, options)
: options?.secret ||
this.options.secret ||
Expand All @@ -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);
}
}

0 comments on commit 5c1050e

Please sign in to comment.