4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master
6 | [travis-url]: https://travis-ci.org/nestjs/nest
7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux
8 | [linux-url]: https://travis-ci.org/nestjs/nest
9 |
10 | A progressive Node.js framework for building efficient and scalable server-side applications.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
23 |
24 | ## Description
25 |
26 | JWT utilities module for [Nest](https://github.com/nestjs/nest) based on the [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) package.
27 |
28 | ## Installation
29 |
30 | ```bash
31 | $ npm i --save @nestjs/jwt
32 | ```
33 |
34 | ## Usage
35 |
36 | Import `JwtModule`:
37 |
38 | ```typescript
39 | @Module({
40 | imports: [JwtModule.register({ secret: 'hard!to-guess_secret' })],
41 | providers: [...],
42 | })
43 | export class AuthModule {}
44 | ```
45 |
46 | Inject `JwtService`:
47 |
48 | ```typescript
49 | @Injectable()
50 | export class AuthService {
51 | constructor(private readonly jwtService: JwtService) {}
52 | }
53 | ```
54 |
55 | ## Secret / Encryption Key options
56 |
57 | 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`.
58 | NOTE: For asynchronous version of `secretOrKeyProvider`, synchronous versions of `.sign()` and `.verify()` will throw an exception.
59 |
60 | ```typescript
61 | JwtModule.register({
62 | /* Secret has precedence over keys */
63 | secret: 'hard!to-guess_secret',
64 |
65 | /* public key used in asymmetric algorithms (required if non other secrets present) */
66 | publicKey: '...',
67 |
68 | /* private key used in asymmetric algorithms (required if non other secrets present) */
69 | privateKey: '...',
70 |
71 | /* Dynamic key provider has precedence over static secret or pub/private keys */
72 | secretOrKeyProvider: (
73 | requestType: JwtSecretRequestType,
74 | tokenOrPayload: string | Object | Buffer,
75 | verifyOrSignOrOptions?: jwt.VerifyOptions | jwt.SignOptions
76 | ) => {
77 | switch (requestType) {
78 | case JwtSecretRequestType.SIGN:
79 | // retrieve signing key dynamically
80 | return 'privateKey';
81 | case JwtSecretRequestType.VERIFY:
82 | // retrieve public key for verification dynamically
83 | return 'publicKey';
84 | default:
85 | // retrieve secret dynamically
86 | return 'hard!to-guess_secret';
87 | }
88 | },
89 | });
90 | ```
91 |
92 | ## Async options
93 |
94 | Quite often you might want to asynchronously pass your module options instead of passing them beforehand. In such case, use `registerAsync()` method, that provides a couple of various ways to deal with async data.
95 |
96 | **1. Use factory**
97 |
98 | ```typescript
99 | JwtModule.registerAsync({
100 | useFactory: () => ({
101 | secret: 'hard!to-guess_secret'
102 | })
103 | });
104 | ```
105 |
106 | Obviously, our factory behaves like every other one (might be `async` and is able to inject dependencies through `inject`).
107 |
108 | ```typescript
109 | JwtModule.registerAsync({
110 | imports: [ConfigModule],
111 | useFactory: async (configService: ConfigService) => ({
112 | secret: configService.get('SECRET'),
113 | }),
114 | inject: [ConfigService],
115 | }),
116 | ```
117 |
118 | **2. Use class**
119 |
120 | ```typescript
121 | JwtModule.registerAsync({
122 | useClass: JwtConfigService
123 | });
124 | ```
125 |
126 | Above construction will instantiate `JwtConfigService` inside `JwtModule` and will leverage it to create options object.
127 |
128 | ```typescript
129 | class JwtConfigService implements JwtOptionsFactory {
130 | createJwtOptions(): JwtModuleOptions {
131 | return {
132 | secret: 'hard!to-guess_secret'
133 | };
134 | }
135 | }
136 | ```
137 |
138 | **3. Use existing**
139 |
140 | ```typescript
141 | JwtModule.registerAsync({
142 | imports: [ConfigModule],
143 | useExisting: ConfigService,
144 | }),
145 | ```
146 |
147 | It works the same as `useClass` with one critical difference - `JwtModule` will lookup imported modules to reuse already created `ConfigService`, instead of instantiating it on its own.
148 |
149 | ## API Spec
150 |
151 | The `JwtService` uses [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) underneath.
152 |
153 | #### jwtService.sign(payload: string | Object | Buffer, options?: JwtSignOptions): string
154 |
155 | 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`.
156 | NOTE: Will throw an exception for asynchronous version of `secretOrKeyProvider`;
157 |
158 | #### jwtService.signAsync(payload: string | Object | Buffer, options?: JwtSignOptions): Promise\
159 |
160 | The asynchronous `.sign()` method.
161 |
162 | #### jwtService.verify\(token: string, options?: JwtVerifyOptions): T
163 |
164 | 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`.
165 | NOTE: Will throw an exception for asynchronous version of `secretOrKeyProvider`;
166 |
167 | #### jwtService.verifyAsync\(token: string, options?: JwtVerifyOptions): Promise\
168 |
169 | The asynchronous `.verify()` method.
170 |
171 | #### jwtService.decode(token: string, options: DecodeOptions): object | string
172 |
173 | The decode method is an implementation of jsonwebtoken `.decode()`.
174 |
175 | The `JwtModule` takes an `options` object:
176 |
177 | - `secret` is either a string, buffer, or object containing the secret for HMAC algorithms
178 | - `secretOrKeyProvider` function with the following signature `(requestType, tokenOrPayload, options?) => jwt.Secret | Promise` (allows generating either secrets or keys dynamically)
179 | - `signOptions` [read more](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback)
180 | - `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)
181 | - `publicKey` PEM encoded public key for RSA and ECDSA
182 | - `verifyOptions` [read more](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback)
183 | - `secretOrPrivateKey` (DEPRECATED!) [read more](https://github.com/auth0/node-jsonwebtoken#jwtsignpayload-secretorprivatekey-options-callback)
184 |
185 | ## Support
186 |
187 | 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).
188 |
189 | ## Stay in touch
190 |
191 | - Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
192 | - Website - [https://nestjs.com](https://nestjs.com/)
193 | - Twitter - [@nestframework](https://twitter.com/nestframework)
194 |
195 | ## License
196 |
197 | Nest is [MIT licensed](LICENSE).
198 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import eslint from '@eslint/js';
3 | import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
4 | import globals from 'globals';
5 | import tseslint from 'typescript-eslint';
6 |
7 | export default tseslint.config(
8 | {
9 | ignores: ['tests/**'],
10 | },
11 | eslint.configs.recommended,
12 | ...tseslint.configs.recommendedTypeChecked,
13 | eslintPluginPrettierRecommended,
14 | {
15 | languageOptions: {
16 | globals: {
17 | ...globals.node,
18 | ...globals.jest,
19 | },
20 | ecmaVersion: 5,
21 | sourceType: 'module',
22 | parserOptions: {
23 | projectService: true,
24 | tsconfigRootDir: import.meta.dirname,
25 | },
26 | },
27 | },
28 | {
29 | rules: {
30 | '@typescript-eslint/no-explicit-any': 'off',
31 | '@typescript-eslint/no-unsafe-assignment': 'off',
32 | '@typescript-eslint/no-unsafe-call': 'off',
33 | '@typescript-eslint/no-unsafe-member-access': 'off',
34 | '@typescript-eslint/no-unsafe-function-type': 'off',
35 | '@typescript-eslint/no-unsafe-argument': 'off',
36 | '@typescript-eslint/no-unsafe-return': 'off',
37 | '@typescript-eslint/no-misused-promises': 'off',
38 | '@typescript-eslint/ban-ts-comment': 'warn'
39 | },
40 | },
41 | );
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './dist';
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | function __export(m) {
3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
4 | }
5 | exports.__esModule = true;
6 | __export(require("./dist"));
7 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dist';
2 |
--------------------------------------------------------------------------------
/jest.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["ts", "tsx", "js", "json"],
3 | "transform": {
4 | "^.+\\.tsx?$": "ts-jest"
5 | },
6 | "testRegex": "/lib/.*\\.(test|spec).(ts|tsx|js)$",
7 | "collectCoverageFrom": [
8 | "lib/**/*.{js,jsx,tsx,ts}",
9 | "!**/node_modules/**",
10 | "!**/vendor/**"
11 | ],
12 | "coverageReporters": ["json", "lcov"]
13 | }
14 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './interfaces';
2 | export * from './jwt.errors';
3 | export * from './jwt.module';
4 | export * from './jwt.service';
5 | export {
6 | TokenExpiredError,
7 | NotBeforeError,
8 | JsonWebTokenError
9 | } from 'jsonwebtoken';
10 |
--------------------------------------------------------------------------------
/lib/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './jwt-module-options.interface';
2 |
--------------------------------------------------------------------------------
/lib/interfaces/jwt-module-options.interface.ts:
--------------------------------------------------------------------------------
1 | import { ModuleMetadata, Provider, Type } from '@nestjs/common';
2 | import * as jwt from 'jsonwebtoken';
3 |
4 | export enum JwtSecretRequestType {
5 | SIGN,
6 | VERIFY
7 | }
8 |
9 | /**
10 | * @publicApi
11 | */
12 | export interface JwtModuleOptions {
13 | global?: boolean;
14 | signOptions?: jwt.SignOptions;
15 | secret?: string | Buffer;
16 | publicKey?: string | Buffer;
17 | privateKey?: jwt.Secret;
18 | /**
19 | * @deprecated
20 | */
21 | secretOrPrivateKey?: jwt.Secret;
22 | secretOrKeyProvider?: (
23 | requestType: JwtSecretRequestType,
24 | tokenOrPayload: string | object | Buffer,
25 | options?: jwt.VerifyOptions | jwt.SignOptions
26 | ) => jwt.Secret | Promise;
27 | verifyOptions?: jwt.VerifyOptions;
28 | }
29 |
30 | /**
31 | * @publicApi
32 | */
33 | export interface JwtOptionsFactory {
34 | createJwtOptions(): Promise | JwtModuleOptions;
35 | }
36 |
37 | /**
38 | * @publicApi
39 | */
40 | export interface JwtModuleAsyncOptions extends Pick {
41 | global?: boolean;
42 | useExisting?: Type;
43 | useClass?: Type;
44 | useFactory?: (...args: any[]) => Promise | JwtModuleOptions;
45 | inject?: any[];
46 | extraProviders?: Provider[];
47 | }
48 |
49 | /**
50 | * @publicApi
51 | */
52 | export interface JwtSignOptions extends jwt.SignOptions {
53 | secret?: string | Buffer;
54 | privateKey?: jwt.Secret;
55 | }
56 |
57 | /**
58 | * @publicApi
59 | */
60 | export interface JwtVerifyOptions extends jwt.VerifyOptions {
61 | secret?: string | Buffer;
62 | publicKey?: string | Buffer;
63 | }
64 |
65 | export type GetSecretKeyResult = string | Buffer | jwt.Secret;
66 |
--------------------------------------------------------------------------------
/lib/jwt.constants.ts:
--------------------------------------------------------------------------------
1 | export const JWT_MODULE_OPTIONS = 'JWT_MODULE_OPTIONS';
2 |
--------------------------------------------------------------------------------
/lib/jwt.errors.ts:
--------------------------------------------------------------------------------
1 | export class WrongSecretProviderError extends Error {}
2 |
--------------------------------------------------------------------------------
/lib/jwt.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Module, Provider } from '@nestjs/common';
2 | import {
3 | JwtModuleAsyncOptions,
4 | JwtModuleOptions,
5 | JwtOptionsFactory
6 | } from './interfaces/jwt-module-options.interface';
7 | import { JWT_MODULE_OPTIONS } from './jwt.constants';
8 | import { createJwtProvider } from './jwt.providers';
9 | import { JwtService } from './jwt.service';
10 |
11 | /**
12 | * @publicApi
13 | */
14 | @Module({
15 | providers: [JwtService],
16 | exports: [JwtService]
17 | })
18 | export class JwtModule {
19 | static register(options: JwtModuleOptions): DynamicModule {
20 | return {
21 | module: JwtModule,
22 | global: options.global,
23 | providers: createJwtProvider(options)
24 | };
25 | }
26 |
27 | static registerAsync(options: JwtModuleAsyncOptions): DynamicModule {
28 | return {
29 | module: JwtModule,
30 | global: options.global,
31 | imports: options.imports || [],
32 | providers: [
33 | ...this.createAsyncProviders(options),
34 | ...(options.extraProviders ?? [])
35 | ]
36 | };
37 | }
38 |
39 | private static createAsyncProviders(
40 | options: JwtModuleAsyncOptions
41 | ): Provider[] {
42 | if (options.useExisting || options.useFactory) {
43 | return [this.createAsyncOptionsProvider(options)];
44 | }
45 | return [
46 | this.createAsyncOptionsProvider(options),
47 | {
48 | provide: options.useClass,
49 | useClass: options.useClass
50 | }
51 | ];
52 | }
53 |
54 | private static createAsyncOptionsProvider(
55 | options: JwtModuleAsyncOptions
56 | ): Provider {
57 | if (options.useFactory) {
58 | return {
59 | provide: JWT_MODULE_OPTIONS,
60 | useFactory: options.useFactory,
61 | inject: options.inject || []
62 | };
63 | }
64 | return {
65 | provide: JWT_MODULE_OPTIONS,
66 | useFactory: async (optionsFactory: JwtOptionsFactory) =>
67 | await optionsFactory.createJwtOptions(),
68 | inject: [options.useExisting || options.useClass]
69 | };
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/jwt.providers.ts:
--------------------------------------------------------------------------------
1 | import { JwtModuleOptions } from './interfaces/jwt-module-options.interface';
2 | import { JWT_MODULE_OPTIONS } from './jwt.constants';
3 |
4 | /**
5 | * @publicApi
6 | */
7 | export function createJwtProvider(options: JwtModuleOptions): any[] {
8 | return [{ provide: JWT_MODULE_OPTIONS, useValue: options || {} }];
9 | }
10 |
--------------------------------------------------------------------------------
/lib/jwt.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test } from '@nestjs/testing';
2 | import * as jwt from 'jsonwebtoken';
3 | import {
4 | JwtModuleOptions,
5 | JwtSecretRequestType
6 | } from './interfaces/jwt-module-options.interface';
7 | import { JwtModule } from './jwt.module';
8 | import { JwtService } from './jwt.service';
9 |
10 | const setup = async (config: JwtModuleOptions) => {
11 | const module = await Test.createTestingModule({
12 | imports: [JwtModule.register(config)]
13 | }).compile();
14 |
15 | return module.get(JwtService);
16 | };
17 |
18 | const config = {
19 | secretOrKeyProvider: (requestType: JwtSecretRequestType) =>
20 | requestType === JwtSecretRequestType.SIGN ? 'sign_secret' : 'verify_secret',
21 | secret: 'default_secret',
22 | publicKey: 'public_key',
23 | privateKey: 'private_key'
24 | };
25 |
26 | describe('JwtService', () => {
27 | let verifySpy: jest.SpyInstance;
28 | let signSpy: jest.SpyInstance;
29 | const getRandomString = () => `${Date.now()}`;
30 |
31 | beforeEach(() => {
32 | signSpy = jest
33 | .spyOn(jwt, 'sign')
34 | .mockImplementation((token: string, secret, options, callback) => {
35 | const result = 'signed_' + token + '_by_' + (secret as string);
36 | return callback ? callback(null, result) : result;
37 | });
38 |
39 | verifySpy = jest
40 | .spyOn(jwt, 'verify')
41 | .mockImplementation((token, secret, options, callback) => {
42 | const result = 'verified_' + token + '_by_' + (secret as string);
43 | return callback ? callback(null, result as any) : result;
44 | });
45 | });
46 |
47 | afterEach(() => {
48 | verifySpy.mockRestore();
49 | signSpy.mockRestore();
50 | });
51 |
52 | describe('should use config.secretOrKeyProvider', () => {
53 | let jwtService: JwtService;
54 | const testPayload: string = getRandomString();
55 |
56 | beforeAll(async () => {
57 | jwtService = await setup(config);
58 | });
59 |
60 | it('signing should use config.secretOrKeyProvider', () => {
61 | expect(jwtService.sign(testPayload)).toBe(
62 | `signed_${testPayload}_by_sign_secret`
63 | );
64 | });
65 |
66 | it('signing (async) should use config.secretOrKeyProvider', async () => {
67 | await expect(jwtService.signAsync(testPayload)).resolves.toBe(
68 | `signed_${testPayload}_by_sign_secret`
69 | );
70 | });
71 |
72 | it('verifying should use config.secretOrKeyProvider', async () => {
73 | expect(await jwtService.verify(testPayload)).toBe(
74 | `verified_${testPayload}_by_verify_secret`
75 | );
76 | });
77 |
78 | it('verifying (async) should use config.secretOrKeyProvider', async () => {
79 | await expect(jwtService.verifyAsync(testPayload)).resolves.toBe(
80 | `verified_${testPayload}_by_verify_secret`
81 | );
82 | });
83 | });
84 |
85 | describe('should use config.secret', () => {
86 | let jwtService: JwtService;
87 | const testPayload: string = getRandomString();
88 |
89 | beforeAll(async () => {
90 | jwtService = await setup({ ...config, secretOrKeyProvider: undefined });
91 | });
92 |
93 | it('signing should use config.secret', () => {
94 | expect(jwtService.sign(testPayload)).toBe(
95 | `signed_${testPayload}_by_default_secret`
96 | );
97 | });
98 |
99 | it('signing (async) should use config.secret', async () => {
100 | await expect(jwtService.signAsync(testPayload)).resolves.toBe(
101 | `signed_${testPayload}_by_default_secret`
102 | );
103 | });
104 |
105 | it('verifying should use config.secret', async () => {
106 | expect(await jwtService.verify(testPayload)).toBe(
107 | `verified_${testPayload}_by_default_secret`
108 | );
109 | });
110 |
111 | it('verifying (async) should use config.secret', async () => {
112 | await expect(jwtService.verifyAsync(testPayload)).resolves.toBe(
113 | `verified_${testPayload}_by_default_secret`
114 | );
115 | });
116 | });
117 |
118 | describe('should use config.privateKey and config.publicKey', () => {
119 | let jwtService: JwtService;
120 | const testPayload: string = getRandomString();
121 |
122 | beforeAll(async () => {
123 | jwtService = await setup({
124 | ...config,
125 | secretOrKeyProvider: undefined,
126 | secret: undefined
127 | });
128 | });
129 |
130 | it('signing should use config.privateKey', () => {
131 | expect(jwtService.sign(testPayload)).toBe(
132 | `signed_${testPayload}_by_private_key`
133 | );
134 | });
135 |
136 | it('signing (async) should use config.privateKey', async () => {
137 | await expect(jwtService.signAsync(testPayload)).resolves.toBe(
138 | `signed_${testPayload}_by_private_key`
139 | );
140 | });
141 |
142 | it('verifying should use config.publicKey', async () => {
143 | expect(await jwtService.verify(testPayload)).toBe(
144 | `verified_${testPayload}_by_public_key`
145 | );
146 | });
147 |
148 | it('verifying (async) should use config.publicKey', async () => {
149 | await expect(jwtService.verifyAsync(testPayload)).resolves.toBe(
150 | `verified_${testPayload}_by_public_key`
151 | );
152 | });
153 | });
154 |
155 | describe('should use config.secretOrPrivateKey but warn about deprecation', () => {
156 | let jwtService: JwtService;
157 | let consoleWarnSpy: jest.SpyInstance;
158 | const testPayload: string = getRandomString();
159 |
160 | beforeAll(async () => {
161 | jwtService = await setup({
162 | ...config,
163 | secretOrPrivateKey: 'deprecated_key'
164 | });
165 | consoleWarnSpy = jest.spyOn(jwtService['logger'], 'warn');
166 | });
167 |
168 | it('signing should use deprecated secretOrPrivateKey', () => {
169 | expect(jwtService.sign(testPayload)).toBe(
170 | `signed_${testPayload}_by_deprecated_key`
171 | );
172 | expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
173 | });
174 |
175 | it('signing (async) should use deprecated secretOrPrivateKey', async () => {
176 | await expect(jwtService.signAsync(testPayload)).resolves.toBe(
177 | `signed_${testPayload}_by_deprecated_key`
178 | );
179 | expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
180 | });
181 |
182 | it('verifying should use deprecated secretOrPrivateKey', async () => {
183 | expect(await jwtService.verify(testPayload)).toBe(
184 | `verified_${testPayload}_by_deprecated_key`
185 | );
186 | expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
187 | });
188 |
189 | it('verifying (async) should use deprecated secretOrPrivateKey', async () => {
190 | await expect(jwtService.verifyAsync(testPayload)).resolves.toBe(
191 | `verified_${testPayload}_by_deprecated_key`
192 | );
193 | expect(consoleWarnSpy).toHaveBeenCalledTimes(1);
194 | });
195 |
196 | afterEach(() => {
197 | consoleWarnSpy.mockClear();
198 | });
199 | });
200 |
201 | describe('should allow buffers for secrets', () => {
202 | let jwtService: JwtService;
203 | let secretB64: Buffer;
204 | const testPayload = { foo: 'bar' };
205 |
206 | beforeEach(async () => {
207 | secretB64 = Buffer.from('ThisIsARandomSecret', 'base64');
208 | jwtService = await setup({ secret: secretB64 });
209 | verifySpy.mockRestore();
210 | signSpy.mockRestore();
211 | });
212 |
213 | it('verifying should use base64 buffer key', () => {
214 | const token = jwt.sign(testPayload, secretB64);
215 |
216 | expect(jwtService.verify(token)).toHaveProperty('foo', 'bar');
217 | });
218 |
219 | it('verifying (async) should use base64 buffer key', async () => {
220 | const token = jwt.sign(testPayload, secretB64);
221 |
222 | await expect(jwtService.verifyAsync(token)).resolves.toHaveProperty(
223 | 'foo',
224 | 'bar'
225 | );
226 | });
227 | });
228 |
229 | describe('should use secret key from options', () => {
230 | let jwtService: JwtService;
231 | const testPayload: string = getRandomString();
232 |
233 | beforeAll(async () => {
234 | jwtService = await setup({
235 | ...config,
236 | secretOrKeyProvider: undefined
237 | });
238 | });
239 |
240 | const secret = 'custom_secret';
241 |
242 | it('signing should use secret key from options', () => {
243 | expect(jwtService.sign(testPayload, { secret })).toBe(
244 | `signed_${testPayload}_by_custom_secret`
245 | );
246 | });
247 |
248 | it('signing (async) should use secret key from options', async () => {
249 | await expect(jwtService.signAsync(testPayload, { secret })).resolves.toBe(
250 | `signed_${testPayload}_by_custom_secret`
251 | );
252 | });
253 |
254 | it('verifying should use secret key from options', async () => {
255 | expect(await jwtService.verify(testPayload, { secret })).toBe(
256 | `verified_${testPayload}_by_custom_secret`
257 | );
258 | });
259 |
260 | it('verifying (async) should use secret key from options', async () => {
261 | await expect(
262 | jwtService.verifyAsync(testPayload, { secret })
263 | ).resolves.toBe(`verified_${testPayload}_by_custom_secret`);
264 | });
265 | });
266 |
267 | describe('should use private/public key from options', () => {
268 | let jwtService: JwtService;
269 | const testPayload: string = getRandomString();
270 |
271 | beforeAll(async () => {
272 | jwtService = await setup({
273 | ...config,
274 | secretOrKeyProvider: undefined,
275 | secret: undefined
276 | });
277 | });
278 |
279 | const privateKey = 'customPrivateKey';
280 | const publicKey = 'customPublicKey';
281 |
282 | it('signing should use private key from options', () => {
283 | expect(jwtService.sign(testPayload, { privateKey })).toBe(
284 | `signed_${testPayload}_by_customPrivateKey`
285 | );
286 | });
287 |
288 | it('signing (async) should use private key from options', async () => {
289 | await expect(
290 | jwtService.signAsync(testPayload, { privateKey })
291 | ).resolves.toBe(`signed_${testPayload}_by_customPrivateKey`);
292 | });
293 |
294 | it('verifying should use public key from options', async () => {
295 | expect(await jwtService.verify(testPayload, { publicKey })).toBe(
296 | `verified_${testPayload}_by_customPublicKey`
297 | );
298 | });
299 |
300 | it('verifying (async) should use public key from options', async () => {
301 | await expect(
302 | jwtService.verifyAsync(testPayload, { publicKey })
303 | ).resolves.toBe(`verified_${testPayload}_by_customPublicKey`);
304 | });
305 | });
306 |
307 | describe('should not use invalid sign options', () => {
308 | let jwtService: JwtService;
309 | const testPayloadStr: string = getRandomString();
310 |
311 | beforeAll(async () => {
312 | jwtService = await setup({ secretOrKeyProvider: undefined });
313 | });
314 |
315 | it('should not "sign" expect errors with a "payload" string and "secret"', () => {
316 | // @ts-expect-no-error
317 | expect(() => jwtService.sign(testPayloadStr, { secret: 'secret' }));
318 | });
319 |
320 | it('should not "signAsync" expect errors with a "payload" string and "privateKey"', () => {
321 | // @ts-expect-no-error
322 | expect(() =>
323 | jwtService.signAsync(testPayloadStr, { privateKey: 'privateKey' })
324 | );
325 | });
326 | });
327 |
328 | describe('should use invalid sign options', () => {
329 | const signOptions: jwt.SignOptions = {
330 | expiresIn: '1d'
331 | };
332 |
333 | let jwtService: JwtService;
334 | const testPayloadStr: string = getRandomString();
335 | const testPayloadObj: object = {};
336 |
337 | beforeAll(async () => {
338 | jwtService = await setup({ signOptions, secretOrKeyProvider: undefined });
339 | });
340 |
341 | it('should "sign" expect errors with a "payload" string with "expiresIn"', () => {
342 | expect(() =>
343 | // @ts-expect-error
344 | jwtService.sign(testPayloadStr, { expiresIn: 60 })
345 | ).toThrowError(
346 | 'Payload as string is not allowed with the following sign options: expiresIn'
347 | );
348 | });
349 |
350 | it('should "signAsync" expect errors with a "payload" string with "notBefore"', () => {
351 | expect(() =>
352 | // @ts-expect-error
353 | jwtService.signAsync(testPayloadStr, { notBefore: 60 })
354 | ).toThrowError(
355 | 'Payload as string is not allowed with the following sign options: expiresIn, notBefore'
356 | );
357 | });
358 |
359 | it('should not "sign" expect errors with a "payload" object with "notBefore" ', () => {
360 | // @ts-expect-no-error
361 | expect(() => jwtService.sign(testPayloadObj, { notBefore: 60 }));
362 | });
363 |
364 | it('should not "signAsync" expect errors with a "payload" object with "notBefore" ', () => {
365 | // @ts-expect-no-error
366 | expect(() => jwtService.signAsync(testPayloadObj, { notBefore: 60 }));
367 | });
368 |
369 | it('should "sign" expect errors using "payload" string with already defined invalid sign options', () => {
370 | expect(() => jwtService.sign(testPayloadStr)).toThrowError(
371 | 'Payload as string is not allowed with the following sign options: expiresIn'
372 | );
373 | });
374 |
375 | it('should "signAsync" expect errors using "payload" string with already defined invalid sign options', () => {
376 | expect(() => jwtService.signAsync(testPayloadStr)).toThrowError(
377 | 'Payload as string is not allowed with the following sign options: expiresIn'
378 | );
379 | });
380 | });
381 | });
382 |
--------------------------------------------------------------------------------
/lib/jwt.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, Logger, Optional } from '@nestjs/common';
2 | import * as jwt from 'jsonwebtoken';
3 | import {
4 | GetSecretKeyResult,
5 | JwtModuleOptions,
6 | JwtSecretRequestType,
7 | JwtSignOptions,
8 | JwtVerifyOptions
9 | } from './interfaces';
10 | import { JWT_MODULE_OPTIONS } from './jwt.constants';
11 | import { WrongSecretProviderError } from './jwt.errors';
12 |
13 | /**
14 | * @publicApi
15 | */
16 | @Injectable()
17 | export class JwtService {
18 | private readonly logger = new Logger('JwtService');
19 |
20 | constructor(
21 | @Optional()
22 | @Inject(JWT_MODULE_OPTIONS)
23 | private readonly options: JwtModuleOptions = {}
24 | ) {}
25 |
26 | sign(
27 | payload: string,
28 | options?: Omit
29 | ): string;
30 | sign(payload: Buffer | object, options?: JwtSignOptions): string;
31 | sign(payload: string | Buffer | object, options?: JwtSignOptions): string {
32 | const signOptions = this.mergeJwtOptions(
33 | { ...options },
34 | 'signOptions'
35 | ) as jwt.SignOptions;
36 | const secret = this.getSecretKey(
37 | payload,
38 | options,
39 | 'privateKey',
40 | JwtSecretRequestType.SIGN
41 | );
42 |
43 | if (secret instanceof Promise) {
44 | secret.catch(() => {}); // suppress rejection from async provider
45 | this.logger.warn(
46 | 'For async version of "secretOrKeyProvider", please use "signAsync".'
47 | );
48 | throw new WrongSecretProviderError();
49 | }
50 |
51 | const allowedSignOptKeys = ['secret', 'privateKey'];
52 | const signOptKeys = Object.keys(signOptions);
53 | if (
54 | typeof payload === 'string' &&
55 | signOptKeys.some((k) => !allowedSignOptKeys.includes(k))
56 | ) {
57 | throw new Error(
58 | 'Payload as string is not allowed with the following sign options: ' +
59 | signOptKeys.join(', ')
60 | );
61 | }
62 |
63 | return jwt.sign(payload, secret, signOptions);
64 | }
65 |
66 | signAsync(
67 | payload: string,
68 | options?: Omit
69 | ): Promise;
70 | signAsync(
71 | payload: Buffer | object,
72 | options?: JwtSignOptions
73 | ): Promise;
74 | signAsync(
75 | payload: string | Buffer | object,
76 | options?: JwtSignOptions
77 | ): Promise {
78 | const signOptions = this.mergeJwtOptions(
79 | { ...options },
80 | 'signOptions'
81 | ) as jwt.SignOptions;
82 | const secret = this.getSecretKey(
83 | payload,
84 | options,
85 | 'privateKey',
86 | JwtSecretRequestType.SIGN
87 | );
88 |
89 | const allowedSignOptKeys = ['secret', 'privateKey'];
90 | const signOptKeys = Object.keys(signOptions);
91 | if (
92 | typeof payload === 'string' &&
93 | signOptKeys.some((k) => !allowedSignOptKeys.includes(k))
94 | ) {
95 | throw new Error(
96 | 'Payload as string is not allowed with the following sign options: ' +
97 | signOptKeys.join(', ')
98 | );
99 | }
100 |
101 | return new Promise((resolve, reject) =>
102 | Promise.resolve()
103 | .then(() => secret)
104 | .then((scrt: GetSecretKeyResult) => {
105 | jwt.sign(payload, scrt, signOptions, (err, encoded) =>
106 | err ? reject(err) : resolve(encoded)
107 | );
108 | })
109 | );
110 | }
111 |
112 | verify(token: string, options?: JwtVerifyOptions): T {
113 | const verifyOptions = this.mergeJwtOptions({ ...options }, 'verifyOptions');
114 | const secret = this.getSecretKey(
115 | token,
116 | options,
117 | 'publicKey',
118 | JwtSecretRequestType.VERIFY
119 | );
120 |
121 | if (secret instanceof Promise) {
122 | secret.catch(() => {}); // suppress rejection from async provider
123 | this.logger.warn(
124 | 'For async version of "secretOrKeyProvider", please use "verifyAsync".'
125 | );
126 | throw new WrongSecretProviderError();
127 | }
128 |
129 | return jwt.verify(token, secret, verifyOptions) as T;
130 | }
131 |
132 | verifyAsync(
133 | token: string,
134 | options?: JwtVerifyOptions
135 | ): Promise {
136 | const verifyOptions = this.mergeJwtOptions({ ...options }, 'verifyOptions');
137 | const secret = this.getSecretKey(
138 | token,
139 | options,
140 | 'publicKey',
141 | JwtSecretRequestType.VERIFY
142 | );
143 |
144 | return new Promise((resolve, reject) =>
145 | Promise.resolve()
146 | .then(() => secret)
147 | .then((scrt: GetSecretKeyResult) => {
148 | jwt.verify(token, scrt, verifyOptions, (err, decoded) =>
149 | err ? reject(err) : resolve(decoded as T)
150 | );
151 | })
152 | .catch(reject)
153 | );
154 | }
155 |
156 | decode(token: string, options?: jwt.DecodeOptions): T {
157 | return jwt.decode(token, options) as T;
158 | }
159 |
160 | private mergeJwtOptions(
161 | options: JwtVerifyOptions | JwtSignOptions,
162 | key: 'verifyOptions' | 'signOptions'
163 | ): jwt.VerifyOptions | jwt.SignOptions {
164 | delete options.secret;
165 | if (key === 'signOptions') {
166 | delete (options as JwtSignOptions).privateKey;
167 | } else {
168 | delete (options as JwtVerifyOptions).publicKey;
169 | }
170 | return options
171 | ? {
172 | ...(this.options[key] || {}),
173 | ...options
174 | }
175 | : this.options[key];
176 | }
177 |
178 | private overrideSecretFromOptions(secret: GetSecretKeyResult) {
179 | if (this.options.secretOrPrivateKey) {
180 | this.logger.warn(
181 | `"secretOrPrivateKey" has been deprecated, please use the new explicit "secret" or use "secretOrKeyProvider" or "privateKey"/"publicKey" exclusively.`
182 | );
183 | secret = this.options.secretOrPrivateKey;
184 | }
185 |
186 | return secret;
187 | }
188 |
189 | private getSecretKey(
190 | token: string | object | Buffer,
191 | options: JwtVerifyOptions | JwtSignOptions,
192 | key: 'publicKey' | 'privateKey',
193 | secretRequestType: JwtSecretRequestType
194 | ): GetSecretKeyResult | Promise {
195 | const secret = this.options.secretOrKeyProvider
196 | ? this.options.secretOrKeyProvider(secretRequestType, token, options)
197 | : options?.secret ||
198 | this.options.secret ||
199 | (key === 'privateKey'
200 | ? (options as JwtSignOptions)?.privateKey || this.options.privateKey
201 | : (options as JwtVerifyOptions)?.publicKey ||
202 | this.options.publicKey) ||
203 | this.options[key];
204 |
205 | return secret instanceof Promise
206 | ? secret.then((sec) => this.overrideSecretFromOptions(sec))
207 | : this.overrideSecretFromOptions(secret);
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@nestjs/jwt",
3 | "version": "11.0.0",
4 | "description": "Nest - modern, fast, powerful node.js web framework (@jwt)",
5 | "author": "Kamil Mysliwiec",
6 | "license": "MIT",
7 | "scripts": {
8 | "format": "prettier --write \"**/*.ts\"",
9 | "lint": "eslint \"lib/**/*.ts\" --fix",
10 | "test": "jest --config=jest.json",
11 | "test:watch": "jest --config=jest.json --watch",
12 | "test:coverage": "jest --config=jest.json --coverage --coverageDirectory=coverage",
13 | "build": "rm -rf dist && tsc -p tsconfig.build.json",
14 | "precommit": "lint-staged",
15 | "prepublish:npm": "npm run build",
16 | "publish:npm": "npm publish --access public",
17 | "prerelease": "npm run build",
18 | "release": "release-it",
19 | "prepare": "husky"
20 | },
21 | "peerDependencies": {
22 | "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
23 | },
24 | "devDependencies": {
25 | "@commitlint/cli": "19.8.1",
26 | "@commitlint/config-angular": "19.8.1",
27 | "@eslint/eslintrc": "3.3.1",
28 | "@eslint/js": "9.28.0",
29 | "@nestjs/common": "11.1.2",
30 | "@nestjs/core": "11.1.2",
31 | "@nestjs/testing": "11.1.2",
32 | "@types/jest": "29.5.14",
33 | "@types/node": "22.15.29",
34 | "eslint": "9.28.0",
35 | "eslint-config-prettier": "10.1.5",
36 | "eslint-plugin-prettier": "5.4.1",
37 | "globals": "16.2.0",
38 | "husky": "9.1.7",
39 | "jest": "29.7.0",
40 | "lint-staged": "16.1.0",
41 | "prettier": "3.5.3",
42 | "reflect-metadata": "0.2.2",
43 | "release-it": "19.0.3",
44 | "rxjs": "7.8.2",
45 | "ts-jest": "29.3.4",
46 | "typescript": "5.8.3",
47 | "typescript-eslint": "8.33.1"
48 | },
49 | "dependencies": {
50 | "@types/jsonwebtoken": "9.0.9",
51 | "jsonwebtoken": "9.0.2"
52 | },
53 | "lint-staged": {
54 | "**/*.{ts,json}": []
55 | },
56 | "repository": {
57 | "type": "git",
58 | "url": "https://github.com/nestjs/jwt"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "semanticCommits": true,
3 | "packageRules": [{
4 | "depTypeList": ["devDependencies"],
5 | "automerge": true
6 | }],
7 | "extends": [
8 | "config:base"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "**/*.spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "noImplicitAny": false,
6 | "removeComments": true,
7 | "noLib": false,
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "target": "ES2021",
11 | "sourceMap": false,
12 | "outDir": "./dist",
13 | "rootDir": "./lib",
14 | "skipLibCheck": true
15 | },
16 | "include": [
17 | "lib/**/*",
18 | "../index.ts"
19 | ],
20 | "exclude": [
21 | "node_modules"
22 | ]
23 | }
--------------------------------------------------------------------------------