@dismissible/nestjs-jwt-auth-hook 0.0.1 → 0.0.2-canary.585db17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dismissible/nestjs-jwt-auth-hook",
3
- "version": "0.0.1",
3
+ "version": "0.0.2-canary.585db17.0",
4
4
  "description": "JWT authentication hook for Dismissible applications using OIDC well-known discovery",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -12,30 +12,38 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "jwks-rsa": "^3.1.0",
16
- "jsonwebtoken": "^9.0.0"
15
+ "@nestjs/axios": "^4.0.1",
16
+ "@dismissible/nestjs-dismissible-hooks": "^0.0.2-canary.585db17.0",
17
+ "@dismissible/nestjs-dismissible-request": "^0.0.2-canary.585db17.0",
18
+ "@dismissible/nestjs-logger": "^0.0.2-canary.585db17.0",
19
+ "@dismissible/nestjs-validation": "^0.0.2-canary.585db17.0",
20
+ "jwks-rsa": "^3.2.0",
21
+ "jsonwebtoken": "^9.0.3"
22
+ },
23
+ "devDependencies": {
24
+ "@types/jsonwebtoken": "^9.0.10"
17
25
  },
18
26
  "peerDependencies": {
19
- "@nestjs/axios": "^4.0.0",
20
27
  "@nestjs/common": "^11.0.0",
21
28
  "@nestjs/core": "^11.0.0",
22
- "@dismissible/nestjs-dismissible": "*",
23
- "@dismissible/nestjs-logger": "*"
29
+ "class-validator": "^0.14.0",
30
+ "class-transformer": "^0.5.0",
31
+ "rxjs": "^7.8.2"
24
32
  },
25
33
  "peerDependenciesMeta": {
26
- "@nestjs/axios": {
27
- "optional": false
28
- },
29
34
  "@nestjs/common": {
30
35
  "optional": false
31
36
  },
32
37
  "@nestjs/core": {
33
38
  "optional": false
34
39
  },
35
- "@dismissible/nestjs-dismissible": {
40
+ "class-validator": {
41
+ "optional": false
42
+ },
43
+ "class-transformer": {
36
44
  "optional": false
37
45
  },
38
- "@dismissible/nestjs-logger": {
46
+ "rxjs": {
39
47
  "optional": false
40
48
  }
41
49
  },
@@ -56,7 +56,6 @@ describe('JwtAuthHookConfig', () => {
56
56
  wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
57
57
  });
58
58
 
59
- // The transform converts string values: 'true' -> true, anything else -> false
60
59
  expect(config.enabled).toBe(false);
61
60
  });
62
61
  });
@@ -78,7 +77,6 @@ describe('JwtAuthHookConfig', () => {
78
77
  });
79
78
 
80
79
  const errors = await validate(config);
81
- // wellKnownUrl should not be required when enabled is false
82
80
  expect(errors.some((e) => e.property === 'wellKnownUrl')).toBe(false);
83
81
  });
84
82
 
@@ -4,7 +4,32 @@ import { JwtAuthHook } from './jwt-auth.hook';
4
4
  import { JwtAuthService, IJwtValidationResult } from './jwt-auth.service';
5
5
  import { JwtAuthHookConfig } from './jwt-auth-hook.config';
6
6
  import { IDismissibleLogger } from '@dismissible/nestjs-logger';
7
- import { IRequestContext } from '@dismissible/nestjs-dismissible';
7
+ import { IRequestContext } from '@dismissible/nestjs-dismissible-request';
8
+
9
+ function createMinimalContext(overrides: Partial<IRequestContext> = {}): IRequestContext {
10
+ return {
11
+ requestId: 'req-123',
12
+ headers: {},
13
+ query: {},
14
+ params: {},
15
+ body: {},
16
+ user: {},
17
+ ip: '127.0.0.1',
18
+ method: 'GET',
19
+ url: '/test',
20
+ protocol: 'http',
21
+ secure: false,
22
+ hostname: 'localhost',
23
+ port: 3000,
24
+ path: '/test',
25
+ search: '',
26
+ searchParams: {},
27
+ origin: 'http://localhost:3000',
28
+ referer: '',
29
+ userAgent: 'test-agent',
30
+ ...overrides,
31
+ };
32
+ }
8
33
 
9
34
  describe('JwtAuthHook', () => {
10
35
  let hook: JwtAuthHook;
@@ -44,7 +69,7 @@ describe('JwtAuthHook', () => {
44
69
  const disabledConfig = { ...mockConfig, enabled: false };
45
70
  const disabledHook = new JwtAuthHook(mockJwtAuthService, disabledConfig, mockLogger);
46
71
 
47
- const context: IRequestContext = { requestId: 'req-123' };
72
+ const context = createMinimalContext();
48
73
  const result = await disabledHook.onBeforeRequest(testItemId, testUserId, context);
49
74
 
50
75
  expect(result.proceed).toBe(true);
@@ -55,7 +80,7 @@ describe('JwtAuthHook', () => {
55
80
  it('should throw UnauthorizedException when no authorization header is present', async () => {
56
81
  mockJwtAuthService.extractBearerToken.mockReturnValue(null);
57
82
 
58
- const context: IRequestContext = { requestId: 'req-123' };
83
+ const context = createMinimalContext();
59
84
 
60
85
  await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
61
86
  UnauthorizedException,
@@ -69,10 +94,9 @@ describe('JwtAuthHook', () => {
69
94
  it('should throw UnauthorizedException when authorization header is malformed', async () => {
70
95
  mockJwtAuthService.extractBearerToken.mockReturnValue(null);
71
96
 
72
- const context: IRequestContext = {
73
- requestId: 'req-123',
74
- authorizationHeader: 'Basic abc123',
75
- };
97
+ const context = createMinimalContext({
98
+ headers: { authorization: 'Basic abc123' },
99
+ });
76
100
 
77
101
  await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
78
102
  UnauthorizedException,
@@ -90,10 +114,9 @@ describe('JwtAuthHook', () => {
90
114
  mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
91
115
  mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
92
116
 
93
- const context: IRequestContext = {
94
- requestId: 'req-123',
95
- authorizationHeader: 'Bearer valid-token',
96
- };
117
+ const context = createMinimalContext({
118
+ headers: { authorization: 'Bearer valid-token' },
119
+ });
97
120
 
98
121
  await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
99
122
  UnauthorizedException,
@@ -116,10 +139,9 @@ describe('JwtAuthHook', () => {
116
139
  mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
117
140
  mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
118
141
 
119
- const context: IRequestContext = {
120
- requestId: 'req-123',
121
- authorizationHeader: 'Bearer valid-token',
122
- };
142
+ const context = createMinimalContext({
143
+ headers: { authorization: 'Bearer valid-token' },
144
+ });
123
145
  const result = await hook.onBeforeRequest(testItemId, matchingUserId, context);
124
146
 
125
147
  expect(result.proceed).toBe(true);
@@ -144,10 +166,9 @@ describe('JwtAuthHook', () => {
144
166
  mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
145
167
  mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
146
168
 
147
- const context: IRequestContext = {
148
- requestId: 'req-123',
149
- authorizationHeader: 'Bearer valid-token',
150
- };
169
+ const context = createMinimalContext({
170
+ headers: { authorization: 'Bearer valid-token' },
171
+ });
151
172
  await hook.onBeforeRequest(testItemId, matchingUserId, context);
152
173
 
153
174
  expect(mockLogger.debug).toHaveBeenCalledWith(
@@ -172,10 +193,9 @@ describe('JwtAuthHook', () => {
172
193
  mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
173
194
  mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
174
195
 
175
- const context: IRequestContext = {
176
- requestId: 'req-123',
177
- authorizationHeader: 'Bearer valid-token',
178
- };
196
+ const context = createMinimalContext({
197
+ headers: { authorization: 'Bearer valid-token' },
198
+ });
179
199
 
180
200
  await expect(hook.onBeforeRequest(testItemId, testUserId, context)).rejects.toThrow(
181
201
  ForbiddenException,
@@ -206,10 +226,9 @@ describe('JwtAuthHook', () => {
206
226
  mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
207
227
  mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
208
228
 
209
- const context: IRequestContext = {
210
- requestId: 'req-123',
211
- authorizationHeader: 'Bearer valid-token',
212
- };
229
+ const context = createMinimalContext({
230
+ headers: { authorization: 'Bearer valid-token' },
231
+ });
213
232
  const result = await hook.onBeforeRequest(testItemId, testUserId, context);
214
233
 
215
234
  expect(result.proceed).toBe(true);
@@ -234,10 +253,9 @@ describe('JwtAuthHook', () => {
234
253
  mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
235
254
  mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
236
255
 
237
- const context: IRequestContext = {
238
- requestId: 'req-123',
239
- authorizationHeader: 'Bearer valid-token',
240
- };
256
+ const context = createMinimalContext({
257
+ headers: { authorization: 'Bearer valid-token' },
258
+ });
241
259
  const result = await disabledVerifyHook.onBeforeRequest(testItemId, testUserId, context);
242
260
 
243
261
  expect(result.proceed).toBe(true);
@@ -248,16 +266,14 @@ describe('JwtAuthHook', () => {
248
266
  valid: true,
249
267
  payload: {
250
268
  iss: 'https://auth.example.com',
251
- // No sub claim
252
269
  },
253
270
  };
254
271
  mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
255
272
  mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
256
273
 
257
- const context: IRequestContext = {
258
- requestId: 'req-123',
259
- authorizationHeader: 'Bearer valid-token',
260
- };
274
+ const context = createMinimalContext({
275
+ headers: { authorization: 'Bearer valid-token' },
276
+ });
261
277
  const result = await hook.onBeforeRequest(testItemId, testUserId, context);
262
278
 
263
279
  expect(result.proceed).toBe(true);
@@ -266,15 +282,13 @@ describe('JwtAuthHook', () => {
266
282
  it('should proceed when JWT payload is undefined and verifyUserIdMatch is enabled', async () => {
267
283
  const validationResult: IJwtValidationResult = {
268
284
  valid: true,
269
- // No payload
270
285
  };
271
286
  mockJwtAuthService.extractBearerToken.mockReturnValue('valid-token');
272
287
  mockJwtAuthService.validateToken.mockResolvedValue(validationResult);
273
288
 
274
- const context: IRequestContext = {
275
- requestId: 'req-123',
276
- authorizationHeader: 'Bearer valid-token',
277
- };
289
+ const context = createMinimalContext({
290
+ headers: { authorization: 'Bearer valid-token' },
291
+ });
278
292
  const result = await hook.onBeforeRequest(testItemId, testUserId, context);
279
293
 
280
294
  expect(result.proceed).toBe(true);
@@ -1,9 +1,6 @@
1
1
  import { Injectable, Inject, UnauthorizedException, ForbiddenException } from '@nestjs/common';
2
- import {
3
- IDismissibleLifecycleHook,
4
- IHookResult,
5
- IRequestContext,
6
- } from '@dismissible/nestjs-dismissible';
2
+ import { IDismissibleLifecycleHook, IHookResult } from '@dismissible/nestjs-dismissible-hooks';
3
+ import { IRequestContext } from '@dismissible/nestjs-dismissible-request';
7
4
  import { DISMISSIBLE_LOGGER, IDismissibleLogger } from '@dismissible/nestjs-logger';
8
5
  import { JwtAuthService } from './jwt-auth.service';
9
6
  import { JWT_AUTH_HOOK_CONFIG, JwtAuthHookConfig } from './jwt-auth-hook.config';
@@ -23,7 +20,6 @@ export class JwtAuthHook implements IDismissibleLifecycleHook {
23
20
  @Inject(DISMISSIBLE_LOGGER)
24
21
  private readonly logger: IDismissibleLogger,
25
22
  ) {
26
- // Default to -100 for authentication (runs early)
27
23
  this.priority = config.priority ?? -100;
28
24
  }
29
25
 
@@ -40,9 +36,8 @@ export class JwtAuthHook implements IDismissibleLifecycleHook {
40
36
  return { proceed: true };
41
37
  }
42
38
 
43
- const authorizationHeader = context?.authorizationHeader;
39
+ const authorizationHeader = context?.headers['authorization'];
44
40
 
45
- // Extract the bearer token
46
41
  const token = this.jwtAuthService.extractBearerToken(authorizationHeader);
47
42
 
48
43
  if (!token) {
@@ -55,7 +50,6 @@ export class JwtAuthHook implements IDismissibleLifecycleHook {
55
50
  throw new UnauthorizedException('Missing or invalid bearer token');
56
51
  }
57
52
 
58
- // Validate the token
59
53
  const result = await this.jwtAuthService.validateToken(token);
60
54
 
61
55
  if (!result.valid) {
@@ -69,8 +63,6 @@ export class JwtAuthHook implements IDismissibleLifecycleHook {
69
63
  throw new UnauthorizedException(result.error);
70
64
  }
71
65
 
72
- // Verify that the userId parameter matches the JWT subject (sub) claim
73
- // This prevents users from accessing other users' data by changing the userId in the URL
74
66
  const verifyUserIdMatch = this.config.verifyUserIdMatch ?? true;
75
67
  if (verifyUserIdMatch && result.payload?.sub) {
76
68
  if (result.payload.sub !== userId) {
@@ -90,7 +90,6 @@ describe('JwtAuthService', () => {
90
90
  });
91
91
 
92
92
  it('should return invalid for malformed token', async () => {
93
- // Mock HTTP response for well-known config
94
93
  const mockResponse: AxiosResponse = {
95
94
  data: {
96
95
  jwks_uri: 'https://auth.example.com/.well-known/jwks.json',
@@ -124,7 +123,6 @@ describe('JwtAuthService', () => {
124
123
 
125
124
  await service.initializeJwksClient();
126
125
 
127
- // Mock jwt.decode to return a string
128
126
  const jwt = require('jsonwebtoken');
129
127
  jest.spyOn(jwt, 'decode').mockReturnValue('string-token');
130
128
 
@@ -148,7 +146,6 @@ describe('JwtAuthService', () => {
148
146
 
149
147
  await service.initializeJwksClient();
150
148
 
151
- // Mock jwt.decode to return object without kid
152
149
  const jwt = require('jsonwebtoken');
153
150
  jest.spyOn(jwt, 'decode').mockReturnValue({
154
151
  header: {},
@@ -175,14 +172,12 @@ describe('JwtAuthService', () => {
175
172
 
176
173
  await service.initializeJwksClient();
177
174
 
178
- // Mock jwt.decode
179
175
  const jwt = require('jsonwebtoken');
180
176
  jest.spyOn(jwt, 'decode').mockReturnValue({
181
177
  header: { kid: 'key-id-123' },
182
178
  payload: {},
183
179
  });
184
180
 
185
- // Mock jwksClient.getSigningKey to throw
186
181
  const mockJwksClient = (service as any).jwksClient;
187
182
  jest.spyOn(mockJwksClient, 'getSigningKey').mockRejectedValue(new Error('Key not found'));
188
183
 
@@ -206,7 +201,6 @@ describe('JwtAuthService', () => {
206
201
 
207
202
  await service.initializeJwksClient();
208
203
 
209
- // Mock jwt operations
210
204
  const jwt = require('jsonwebtoken');
211
205
  const mockPayload = {
212
206
  sub: 'user-123',
@@ -283,7 +277,6 @@ describe('JwtAuthService', () => {
283
277
  const result = await serviceWithoutIssuerAudience.validateToken('valid-token');
284
278
 
285
279
  expect(result.valid).toBe(true);
286
- // Check the last call (this test's call) doesn't have issuer/audience
287
280
  const lastCall = verifySpy.mock.calls[verifySpy.mock.calls.length - 1];
288
281
  expect(lastCall[2]).not.toHaveProperty('issuer');
289
282
  expect(lastCall[2]).not.toHaveProperty('audience');
@@ -458,7 +451,6 @@ describe('JwtAuthService', () => {
458
451
  const mockResponse: AxiosResponse = {
459
452
  data: {
460
453
  issuer: 'https://auth.example.com',
461
- // no jwks_uri
462
454
  },
463
455
  status: 200,
464
456
  statusText: 'OK',
@@ -524,7 +516,6 @@ describe('JwtAuthService', () => {
524
516
 
525
517
  await serviceWithCacheDuration.initializeJwksClient();
526
518
 
527
- // Verify service initialized successfully with custom cache duration
528
519
  expect((serviceWithCacheDuration as any).jwksClient).toBeDefined();
529
520
  expect(mockLogger.info).toHaveBeenCalledWith(
530
521
  'JWKS client initialized successfully',
@@ -545,7 +536,6 @@ describe('JwtAuthService', () => {
545
536
  mockLogger,
546
537
  );
547
538
 
548
- // Service should be created without error
549
539
  expect(serviceWithDefaults).toBeDefined();
550
540
  });
551
541
 
@@ -120,7 +120,6 @@ export class JwtAuthService implements OnModuleInit {
120
120
  }
121
121
 
122
122
  try {
123
- // Decode the token header to get the key ID (kid)
124
123
  const decoded = jwt.decode(token, { complete: true });
125
124
 
126
125
  if (!decoded || typeof decoded === 'string') {
@@ -138,7 +137,6 @@ export class JwtAuthService implements OnModuleInit {
138
137
  };
139
138
  }
140
139
 
141
- // Get the signing key from JWKS
142
140
  let signingKey: SigningKey;
143
141
  try {
144
142
  signingKey = await this.jwksClient.getSigningKey(kid);
@@ -151,7 +149,6 @@ export class JwtAuthService implements OnModuleInit {
151
149
 
152
150
  const publicKey = signingKey.getPublicKey();
153
151
 
154
- // Verify the token
155
152
  const verifyOptions: jwt.VerifyOptions = {
156
153
  algorithms: (this.config.algorithms as jwt.Algorithm[]) ?? ['RS256'],
157
154
  };