@nauth-toolkit/core 0.1.0 → 0.1.5

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.
Files changed (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +9 -0
  3. package/package.json +8 -3
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. package/tsconfig.lint.json +0 -6
@@ -1,882 +0,0 @@
1
- import * as crypto from 'crypto';
2
- import { JwtService } from './jwt.service';
3
- import { JwtConfig } from '../interfaces/config.interface';
4
- import { NAuthException } from '../exceptions/nauth.exception';
5
-
6
- /**
7
- * JWT Service Unit Tests
8
- *
9
- * Covers:
10
- * - Token generation (access and refresh tokens)
11
- * - Token validation (access and refresh tokens)
12
- * - Multiple algorithms (HS256, HS384, HS512, RS256, RS384, RS512)
13
- * - Token expiration handling
14
- * - Token family tracking
15
- * - Token utilities (hash, decode, extract)
16
- * - Error handling
17
- * - Configuration edge cases
18
- */
19
- describe('JwtService', () => {
20
- let service: JwtService;
21
- let config: JwtConfig;
22
-
23
- // Generate RSA key pair for asymmetric algorithm testing
24
- const generateRSAKeyPair = (): { privateKey: string; publicKey: string } => {
25
- const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
26
- modulusLength: 2048,
27
- publicKeyEncoding: {
28
- type: 'spki',
29
- format: 'pem',
30
- },
31
- privateKeyEncoding: {
32
- type: 'pkcs8',
33
- format: 'pem',
34
- },
35
- });
36
- return { privateKey, publicKey };
37
- };
38
-
39
- const defaultConfig: JwtConfig = {
40
- algorithm: 'HS256',
41
- accessToken: {
42
- secret: 'test-access-secret-min-32-characters',
43
- expiresIn: '15m',
44
- },
45
- refreshToken: {
46
- secret: 'test-refresh-secret-min-32-characters',
47
- expiresIn: '30d',
48
- rotation: true,
49
- reuseDetection: true,
50
- },
51
- issuer: 'nauth-toolkit',
52
- audience: 'test-app',
53
- };
54
-
55
- beforeEach(() => {
56
- config = { ...defaultConfig };
57
- service = new JwtService(config);
58
- });
59
-
60
- // ============================================================================
61
- // Service Initialization
62
- // ============================================================================
63
-
64
- describe('constructor', () => {
65
- it('should initialize with valid config', () => {
66
- expect(service).toBeDefined();
67
- });
68
-
69
- it('should use default algorithm HS256 when not specified', () => {
70
- const configWithoutAlgorithm = {
71
- ...defaultConfig,
72
- algorithm: undefined,
73
- };
74
- const serviceDefault = new JwtService(configWithoutAlgorithm);
75
- expect(serviceDefault).toBeDefined();
76
- });
77
- });
78
-
79
- // ============================================================================
80
- // Token Generation
81
- // ============================================================================
82
-
83
- describe('generateTokenPair', () => {
84
- it('should generate access and refresh tokens', async () => {
85
- const tokens = await service.generateTokenPair({
86
- userId: 'user-123',
87
- email: 'test@example.com',
88
- sessionId: 'session-456',
89
- });
90
-
91
- expect(tokens.accessToken).toBeDefined();
92
- expect(tokens.refreshToken).toBeDefined();
93
- expect(tokens.expiresIn).toBe(900); // 15 minutes
94
- });
95
-
96
- it('should include token family in both tokens', async () => {
97
- const tokens = await service.generateTokenPair({
98
- userId: 'user-123',
99
- email: 'test@example.com',
100
- sessionId: 'session-456',
101
- });
102
-
103
- const accessDecoded = service.decodeToken(tokens.accessToken);
104
- const refreshDecoded = service.decodeToken(tokens.refreshToken);
105
-
106
- expect(accessDecoded?.tokenFamily).toBeDefined();
107
- expect(refreshDecoded?.tokenFamily).toBeDefined();
108
- expect(accessDecoded?.tokenFamily).toBe(refreshDecoded?.tokenFamily);
109
- });
110
-
111
- it('should reuse provided token family', async () => {
112
- const providedFamily = 'existing-family-id';
113
- const tokens = await service.generateTokenPair({
114
- userId: 'user-123',
115
- email: 'test@example.com',
116
- sessionId: 'session-456',
117
- tokenFamily: providedFamily,
118
- });
119
-
120
- const accessDecoded = service.decodeToken(tokens.accessToken);
121
- expect(accessDecoded?.tokenFamily).toBe(providedFamily);
122
- });
123
-
124
- it('should include all required fields in tokens', async () => {
125
- const tokens = await service.generateTokenPair({
126
- userId: 'user-123',
127
- email: 'test@example.com',
128
- sessionId: 'session-456',
129
- });
130
-
131
- const accessDecoded = service.decodeToken(tokens.accessToken);
132
- expect(accessDecoded?.sub).toBe('user-123');
133
- expect(accessDecoded?.email).toBe('test@example.com');
134
- expect(accessDecoded?.sessionId).toBe('session-456');
135
- expect(accessDecoded?.type).toBe('access');
136
- expect(accessDecoded?.iat).toBeDefined();
137
- expect(accessDecoded?.exp).toBeDefined();
138
- });
139
- });
140
-
141
- describe('generateAccessToken', () => {
142
- it('should generate access token with issuer and audience', async () => {
143
- const token = await service.generateAccessToken({
144
- userId: 'user-123',
145
- email: 'test@example.com',
146
- sessionId: 'session-456',
147
- tokenFamily: 'family-123',
148
- });
149
-
150
- expect(token).toBeDefined();
151
- const decoded = service.decodeToken(token);
152
- expect(decoded?.iss).toBe('nauth-toolkit');
153
- expect(decoded?.aud).toBe('test-app');
154
- });
155
-
156
- it('should generate access token without issuer and audience', async () => {
157
- const configWithoutIssuer = {
158
- ...defaultConfig,
159
- issuer: undefined,
160
- audience: undefined,
161
- };
162
- const serviceWithoutIssuer = new JwtService(configWithoutIssuer);
163
-
164
- const token = await serviceWithoutIssuer.generateAccessToken({
165
- userId: 'user-123',
166
- email: 'test@example.com',
167
- sessionId: 'session-456',
168
- tokenFamily: 'family-123',
169
- });
170
-
171
- expect(token).toBeDefined();
172
- const decoded = serviceWithoutIssuer.decodeToken(token);
173
- expect(decoded?.iss).toBeUndefined();
174
- expect(decoded?.aud).toBeUndefined();
175
- });
176
-
177
- it('should generate access token with array audience', async () => {
178
- const configWithArrayAudience = {
179
- ...defaultConfig,
180
- audience: ['app1', 'app2'],
181
- };
182
- const serviceWithArrayAudience = new JwtService(configWithArrayAudience);
183
-
184
- const token = await serviceWithArrayAudience.generateAccessToken({
185
- userId: 'user-123',
186
- email: 'test@example.com',
187
- sessionId: 'session-456',
188
- tokenFamily: 'family-123',
189
- });
190
-
191
- expect(token).toBeDefined();
192
- const decoded = serviceWithArrayAudience.decodeToken(token);
193
- expect(Array.isArray(decoded?.aud)).toBe(true);
194
- expect((decoded?.aud as string[]).length).toBe(2);
195
- });
196
-
197
- it('should throw error when access token key not configured', async () => {
198
- const configWithoutKey: JwtConfig = {
199
- ...defaultConfig,
200
- accessToken: {
201
- expiresIn: '15m',
202
- },
203
- };
204
- const serviceWithoutKey = new JwtService(configWithoutKey);
205
-
206
- try {
207
- await serviceWithoutKey.generateAccessToken({
208
- userId: 'user-123',
209
- email: 'test@example.com',
210
- sessionId: 'session-456',
211
- tokenFamily: 'family-123',
212
- });
213
- fail('Should have thrown NAuthException');
214
- } catch (error) {
215
- expect(error).toBeInstanceOf(NAuthException);
216
- }
217
- });
218
- });
219
-
220
- describe('generateRefreshToken', () => {
221
- it('should generate refresh token', async () => {
222
- const token = await service.generateRefreshToken({
223
- userId: 'user-123',
224
- email: 'test@example.com',
225
- sessionId: 'session-456',
226
- tokenFamily: 'family-123',
227
- });
228
-
229
- expect(token).toBeDefined();
230
- const decoded = service.decodeToken(token);
231
- expect(decoded?.type).toBe('refresh');
232
- });
233
-
234
- it('should throw error when refresh token secret not configured', async () => {
235
- const configWithoutSecret: JwtConfig = {
236
- ...defaultConfig,
237
- refreshToken: {
238
- secret: '', // Empty secret to trigger error
239
- expiresIn: '30d',
240
- },
241
- };
242
- // Create service that will fail during key preparation
243
- const serviceWithoutSecret = new JwtService(configWithoutSecret);
244
-
245
- try {
246
- await serviceWithoutSecret.generateRefreshToken({
247
- userId: 'user-123',
248
- email: 'test@example.com',
249
- sessionId: 'session-456',
250
- tokenFamily: 'family-123',
251
- });
252
- fail('Should have thrown NAuthException');
253
- } catch (error) {
254
- expect(error).toBeInstanceOf(NAuthException);
255
- }
256
- });
257
- });
258
-
259
- // ============================================================================
260
- // Algorithm Support
261
- // ============================================================================
262
-
263
- describe('algorithm support', () => {
264
- const symmetricAlgorithms: Array<'HS256' | 'HS384' | 'HS512'> = ['HS256', 'HS384', 'HS512'];
265
- const asymmetricAlgorithms: Array<'RS256' | 'RS384' | 'RS512'> = ['RS256', 'RS384', 'RS512'];
266
-
267
- symmetricAlgorithms.forEach((algorithm) => {
268
- it(`should generate and validate tokens with ${algorithm}`, async () => {
269
- const configWithAlgorithm = {
270
- ...defaultConfig,
271
- algorithm,
272
- };
273
- const serviceWithAlgorithm = new JwtService(configWithAlgorithm);
274
-
275
- const tokens = await serviceWithAlgorithm.generateTokenPair({
276
- userId: 'user-123',
277
- email: 'test@example.com',
278
- sessionId: 'session-456',
279
- });
280
-
281
- const result = await serviceWithAlgorithm.validateAccessToken(tokens.accessToken);
282
- expect(result.valid).toBe(true);
283
- expect(result.payload?.sub).toBe('user-123');
284
- });
285
- });
286
-
287
- asymmetricAlgorithms.forEach((algorithm) => {
288
- it(`should generate and validate tokens with ${algorithm}`, async () => {
289
- const { privateKey, publicKey } = generateRSAKeyPair();
290
- const configWithAlgorithm: JwtConfig = {
291
- ...defaultConfig,
292
- algorithm,
293
- accessToken: {
294
- privateKey,
295
- publicKey,
296
- expiresIn: '15m',
297
- },
298
- };
299
- const serviceWithAlgorithm = new JwtService(configWithAlgorithm);
300
-
301
- const tokens = await serviceWithAlgorithm.generateTokenPair({
302
- userId: 'user-123',
303
- email: 'test@example.com',
304
- sessionId: 'session-456',
305
- });
306
-
307
- const result = await serviceWithAlgorithm.validateAccessToken(tokens.accessToken);
308
- expect(result.valid).toBe(true);
309
- expect(result.payload?.sub).toBe('user-123');
310
- });
311
- });
312
-
313
- it('should use HS256 for refresh token when access token uses asymmetric algorithm', async () => {
314
- const { privateKey, publicKey } = generateRSAKeyPair();
315
- const configWithRS256: JwtConfig = {
316
- ...defaultConfig,
317
- algorithm: 'RS256',
318
- accessToken: {
319
- privateKey,
320
- publicKey,
321
- expiresIn: '15m',
322
- },
323
- };
324
- const serviceWithRS256 = new JwtService(configWithRS256);
325
-
326
- const tokens = await serviceWithRS256.generateTokenPair({
327
- userId: 'user-123',
328
- email: 'test@example.com',
329
- sessionId: 'session-456',
330
- });
331
-
332
- // Refresh token should still validate (uses HS256)
333
- const refreshResult = await serviceWithRS256.validateRefreshToken(tokens.refreshToken);
334
- expect(refreshResult.valid).toBe(true);
335
- });
336
- });
337
-
338
- // ============================================================================
339
- // Token Validation
340
- // ============================================================================
341
-
342
- describe('validateAccessToken', () => {
343
- it('should validate valid access token', async () => {
344
- const tokens = await service.generateTokenPair({
345
- userId: 'user-123',
346
- email: 'test@example.com',
347
- sessionId: 'session-456',
348
- });
349
-
350
- const result = await service.validateAccessToken(tokens.accessToken);
351
-
352
- expect(result.valid).toBe(true);
353
- expect(result.payload).toBeDefined();
354
- expect(result.payload?.sub).toBe('user-123');
355
- expect(result.payload?.email).toBe('test@example.com');
356
- expect(result.payload?.type).toBe('access');
357
- });
358
-
359
- it('should reject refresh token as access token', async () => {
360
- const tokens = await service.generateTokenPair({
361
- userId: 'user-123',
362
- email: 'test@example.com',
363
- sessionId: 'session-456',
364
- });
365
-
366
- const result = await service.validateAccessToken(tokens.refreshToken);
367
-
368
- expect(result.valid).toBe(false);
369
- expect(result.errorType).toBe('invalid');
370
- });
371
-
372
- it('should reject malformed token', async () => {
373
- const result = await service.validateAccessToken('invalid-token');
374
-
375
- expect(result.valid).toBe(false);
376
- // jose library may return 'invalid' or 'malformed' for malformed tokens
377
- expect(result.errorType).toBeDefined();
378
- expect(['invalid', 'malformed']).toContain(result.errorType!);
379
- });
380
-
381
- it('should reject expired token', async () => {
382
- // Create token with very short expiration (using real-world string format)
383
- const configWithShortExpiry = {
384
- ...defaultConfig,
385
- accessToken: {
386
- ...defaultConfig.accessToken,
387
- expiresIn: '1s', // 1 second - matches real-world config format ('15m', '30d', etc.)
388
- },
389
- };
390
- const serviceWithShortExpiry = new JwtService(configWithShortExpiry);
391
-
392
- const tokens = await serviceWithShortExpiry.generateTokenPair({
393
- userId: 'user-123',
394
- email: 'test@example.com',
395
- sessionId: 'session-456',
396
- });
397
-
398
- // Wait for token to expire (add buffer for clock skew and parsing)
399
- await new Promise((resolve) => setTimeout(resolve, 2100));
400
-
401
- const result = await serviceWithShortExpiry.validateAccessToken(tokens.accessToken);
402
-
403
- expect(result.valid).toBe(false);
404
- expect(result.errorType).toBe('expired');
405
- });
406
-
407
- it('should reject token with wrong signature', async () => {
408
- const tokens = await service.generateTokenPair({
409
- userId: 'user-123',
410
- email: 'test@example.com',
411
- sessionId: 'session-456',
412
- });
413
-
414
- // Create service with different secret
415
- const differentConfig = {
416
- ...defaultConfig,
417
- accessToken: {
418
- ...defaultConfig.accessToken,
419
- secret: 'different-secret-min-32-characters-long',
420
- },
421
- };
422
- const differentService = new JwtService(differentConfig);
423
-
424
- const result = await differentService.validateAccessToken(tokens.accessToken);
425
-
426
- expect(result.valid).toBe(false);
427
- // jose library may return 'invalid' or 'malformed' for wrong signature
428
- expect(result.errorType).toBeDefined();
429
- expect(['invalid', 'malformed']).toContain(result.errorType!);
430
- });
431
-
432
- it('should validate token with public key for asymmetric algorithm', async () => {
433
- const { privateKey, publicKey } = generateRSAKeyPair();
434
- const configWithRS256: JwtConfig = {
435
- ...defaultConfig,
436
- algorithm: 'RS256',
437
- accessToken: {
438
- privateKey,
439
- publicKey,
440
- expiresIn: '15m',
441
- },
442
- };
443
- const serviceWithRS256 = new JwtService(configWithRS256);
444
-
445
- const tokens = await serviceWithRS256.generateTokenPair({
446
- userId: 'user-123',
447
- email: 'test@example.com',
448
- sessionId: 'session-456',
449
- });
450
-
451
- const result = await serviceWithRS256.validateAccessToken(tokens.accessToken);
452
- expect(result.valid).toBe(true);
453
- expect(result.payload?.sub).toBe('user-123');
454
- });
455
-
456
- it('should reject token when issuer mismatch', async () => {
457
- const tokens = await service.generateTokenPair({
458
- userId: 'user-123',
459
- email: 'test@example.com',
460
- sessionId: 'session-456',
461
- });
462
-
463
- const configWithDifferentIssuer = {
464
- ...defaultConfig,
465
- issuer: 'different-issuer',
466
- };
467
- const serviceWithDifferentIssuer = new JwtService(configWithDifferentIssuer);
468
-
469
- const result = await serviceWithDifferentIssuer.validateAccessToken(tokens.accessToken);
470
-
471
- expect(result.valid).toBe(false);
472
- // jose library may return 'invalid' or 'malformed' for issuer mismatch
473
- expect(result.errorType).toBeDefined();
474
- expect(['invalid', 'malformed']).toContain(result.errorType!);
475
- });
476
-
477
- it('should reject token when audience mismatch', async () => {
478
- const tokens = await service.generateTokenPair({
479
- userId: 'user-123',
480
- email: 'test@example.com',
481
- sessionId: 'session-456',
482
- });
483
-
484
- const configWithDifferentAudience = {
485
- ...defaultConfig,
486
- audience: 'different-audience',
487
- };
488
- const serviceWithDifferentAudience = new JwtService(configWithDifferentAudience);
489
-
490
- const result = await serviceWithDifferentAudience.validateAccessToken(tokens.accessToken);
491
-
492
- expect(result.valid).toBe(false);
493
- // jose library may return 'invalid' or 'malformed' for audience mismatch
494
- expect(result.errorType).toBeDefined();
495
- expect(['invalid', 'malformed']).toContain(result.errorType!);
496
- });
497
-
498
- it('should handle missing public key gracefully for asymmetric algorithm', async () => {
499
- const { privateKey } = generateRSAKeyPair();
500
- const configWithoutPublicKey: JwtConfig = {
501
- ...defaultConfig,
502
- algorithm: 'RS256',
503
- accessToken: {
504
- privateKey,
505
- expiresIn: '15m',
506
- },
507
- };
508
- const serviceWithoutPublicKey = new JwtService(configWithoutPublicKey);
509
-
510
- const tokens = await serviceWithoutPublicKey.generateTokenPair({
511
- userId: 'user-123',
512
- email: 'test@example.com',
513
- sessionId: 'session-456',
514
- });
515
-
516
- // Validation should fail without public key
517
- const result = await serviceWithoutPublicKey.validateAccessToken(tokens.accessToken);
518
- expect(result.valid).toBe(false);
519
- });
520
- });
521
-
522
- describe('validateRefreshToken', () => {
523
- it('should validate valid refresh token', async () => {
524
- const tokens = await service.generateTokenPair({
525
- userId: 'user-123',
526
- email: 'test@example.com',
527
- sessionId: 'session-456',
528
- });
529
-
530
- const result = await service.validateRefreshToken(tokens.refreshToken);
531
-
532
- expect(result.valid).toBe(true);
533
- expect(result.payload).toBeDefined();
534
- expect(result.payload?.type).toBe('refresh');
535
- expect(result.payload?.sub).toBe('user-123');
536
- });
537
-
538
- it('should reject access token as refresh token', async () => {
539
- const tokens = await service.generateTokenPair({
540
- userId: 'user-123',
541
- email: 'test@example.com',
542
- sessionId: 'session-456',
543
- });
544
-
545
- const result = await service.validateRefreshToken(tokens.accessToken);
546
-
547
- expect(result.valid).toBe(false);
548
- expect(result.errorType).toBe('invalid');
549
- });
550
-
551
- it('should reject malformed refresh token', async () => {
552
- const result = await service.validateRefreshToken('invalid-token');
553
-
554
- expect(result.valid).toBe(false);
555
- // jose library may return 'invalid' or 'malformed' for malformed tokens
556
- expect(result.errorType).toBeDefined();
557
- expect(['invalid', 'malformed']).toContain(result.errorType!);
558
- });
559
-
560
- it('should reject expired refresh token', async () => {
561
- const configWithShortExpiry = {
562
- ...defaultConfig,
563
- refreshToken: {
564
- ...defaultConfig.refreshToken,
565
- expiresIn: '1s', // 1 second - matches real-world config format ('30d', '7d', etc.)
566
- },
567
- };
568
- const serviceWithShortExpiry = new JwtService(configWithShortExpiry);
569
-
570
- const tokens = await serviceWithShortExpiry.generateTokenPair({
571
- userId: 'user-123',
572
- email: 'test@example.com',
573
- sessionId: 'session-456',
574
- });
575
-
576
- // Wait for token to expire (add buffer for clock skew and parsing)
577
- await new Promise((resolve) => setTimeout(resolve, 2100));
578
-
579
- const result = await serviceWithShortExpiry.validateRefreshToken(tokens.refreshToken);
580
-
581
- expect(result.valid).toBe(false);
582
- expect(result.errorType).toBe('expired');
583
- });
584
-
585
- it('should handle missing refresh token key during validation', async () => {
586
- const configWithoutSecret: JwtConfig = {
587
- ...defaultConfig,
588
- refreshToken: {
589
- secret: '', // Empty secret
590
- expiresIn: '30d',
591
- },
592
- };
593
- const serviceWithoutSecret = new JwtService(configWithoutSecret);
594
-
595
- // Validation should fail without secret
596
- const result = await serviceWithoutSecret.validateRefreshToken('any-token');
597
- expect(result.valid).toBe(false);
598
- });
599
- });
600
-
601
- // ============================================================================
602
- // Token Utilities
603
- // ============================================================================
604
-
605
- describe('decodeToken', () => {
606
- it('should decode token without verification', async () => {
607
- const tokens = await service.generateTokenPair({
608
- userId: 'user-123',
609
- email: 'test@example.com',
610
- sessionId: 'session-456',
611
- });
612
-
613
- const decoded = service.decodeToken(tokens.accessToken);
614
-
615
- expect(decoded).toBeDefined();
616
- expect(decoded?.sub).toBe('user-123');
617
- expect(decoded?.email).toBe('test@example.com');
618
- expect(decoded?.sessionId).toBe('session-456');
619
- });
620
-
621
- it('should return null for malformed token', () => {
622
- const decoded = service.decodeToken('invalid-token');
623
- expect(decoded).toBeNull();
624
- });
625
-
626
- it('should decode expired token', async () => {
627
- const configWithShortExpiry = {
628
- ...defaultConfig,
629
- accessToken: {
630
- ...defaultConfig.accessToken,
631
- expiresIn: '1s', // 1 second - matches real-world config format
632
- },
633
- };
634
- const serviceWithShortExpiry = new JwtService(configWithShortExpiry);
635
-
636
- const tokens = await serviceWithShortExpiry.generateTokenPair({
637
- userId: 'user-123',
638
- email: 'test@example.com',
639
- sessionId: 'session-456',
640
- });
641
-
642
- // Wait for token to expire (add buffer for clock skew and parsing)
643
- await new Promise((resolve) => setTimeout(resolve, 2100));
644
-
645
- // Decode should still work (no verification)
646
- const decoded = serviceWithShortExpiry.decodeToken(tokens.accessToken);
647
- expect(decoded).toBeDefined();
648
- expect(decoded?.sub).toBe('user-123');
649
- });
650
- });
651
-
652
- describe('hashToken', () => {
653
- it('should generate consistent hash for same token', () => {
654
- const token = 'test-token';
655
- const hash1 = service.hashToken(token);
656
- const hash2 = service.hashToken(token);
657
-
658
- expect(hash1).toBe(hash2);
659
- expect(hash1.length).toBe(64); // SHA-256 hex length
660
- });
661
-
662
- it('should generate different hashes for different tokens', () => {
663
- const hash1 = service.hashToken('token1');
664
- const hash2 = service.hashToken('token2');
665
-
666
- expect(hash1).not.toBe(hash2);
667
- });
668
-
669
- it('should generate hash for empty token', () => {
670
- const hash = service.hashToken('');
671
- expect(hash).toBeDefined();
672
- expect(hash.length).toBe(64);
673
- });
674
- });
675
-
676
- describe('extractTokenFromHeader', () => {
677
- it('should extract token from Bearer header', () => {
678
- const token = service.extractTokenFromHeader('Bearer eyJhbGc...');
679
- expect(token).toBe('eyJhbGc...');
680
- });
681
-
682
- it('should return null for missing header', () => {
683
- const token = service.extractTokenFromHeader(undefined);
684
- expect(token).toBeNull();
685
- });
686
-
687
- it('should return null for invalid format', () => {
688
- const token = service.extractTokenFromHeader('Basic abc123');
689
- expect(token).toBeNull();
690
- });
691
-
692
- it('should return null for Bearer without token', () => {
693
- const token = service.extractTokenFromHeader('Bearer ');
694
- expect(token).toBeNull();
695
- });
696
-
697
- it('should return null for Bearer with only whitespace', () => {
698
- const token = service.extractTokenFromHeader('Bearer ');
699
- expect(token).toBeNull();
700
- });
701
-
702
- it('should extract token with multiple spaces (first token after split)', () => {
703
- // Note: split(' ') splits on single space, so 'Bearer token' becomes ['Bearer', '', '', 'token']
704
- // The implementation returns the second element, which would be empty string
705
- // This test verifies the actual behavior (may need implementation fix)
706
- const token = service.extractTokenFromHeader('Bearer eyJhbGc...');
707
- // Implementation returns first token after split, which is empty string for multiple spaces
708
- expect(token).toBeNull(); // Current implementation returns empty string which becomes null
709
- });
710
-
711
- it('should return null for empty string', () => {
712
- const token = service.extractTokenFromHeader('');
713
- expect(token).toBeNull();
714
- });
715
- });
716
-
717
- describe('generateTokenFamily', () => {
718
- it('should generate unique token families', () => {
719
- const family1 = service.generateTokenFamily();
720
- const family2 = service.generateTokenFamily();
721
-
722
- expect(family1).toBeDefined();
723
- expect(family2).toBeDefined();
724
- expect(family1).not.toBe(family2);
725
- expect(family1.length).toBe(64); // 32 bytes hex (256 bits - SECURITY FIX #10)
726
- });
727
-
728
- it('should generate token families with correct format', () => {
729
- const family = service.generateTokenFamily();
730
- expect(family).toMatch(/^[a-f0-9]{64}$/); // Hex string, 64 characters
731
- });
732
-
733
- it('should generate many unique token families', () => {
734
- const families = new Set<string>();
735
- for (let i = 0; i < 100; i++) {
736
- families.add(service.generateTokenFamily());
737
- }
738
- expect(families.size).toBe(100); // All unique
739
- });
740
- });
741
-
742
- // ============================================================================
743
- // Expiration Time Utilities
744
- // ============================================================================
745
-
746
- describe('getAccessTokenExpiry', () => {
747
- it('should return expiry time in seconds', () => {
748
- const expiry = service.getAccessTokenExpiry();
749
- expect(expiry).toBe(900); // 15 minutes
750
- });
751
-
752
- it('should handle different expiry formats', () => {
753
- const configWithNumberExpiry = {
754
- ...defaultConfig,
755
- accessToken: {
756
- ...defaultConfig.accessToken,
757
- expiresIn: 3600, // 1 hour in seconds
758
- },
759
- };
760
- const serviceWithNumberExpiry = new JwtService(configWithNumberExpiry);
761
- expect(serviceWithNumberExpiry.getAccessTokenExpiry()).toBe(3600);
762
- });
763
- });
764
-
765
- describe('getRefreshTokenTTL', () => {
766
- it('should return TTL in seconds', () => {
767
- const ttl = service.getRefreshTokenTTL();
768
- expect(ttl).toBe(2592000); // 30 days
769
- });
770
-
771
- it('should handle different TTL formats', () => {
772
- const configWithNumberTTL = {
773
- ...defaultConfig,
774
- refreshToken: {
775
- ...defaultConfig.refreshToken,
776
- expiresIn: 604800, // 7 days in seconds
777
- },
778
- };
779
- const serviceWithNumberTTL = new JwtService(configWithNumberTTL);
780
- expect(serviceWithNumberTTL.getRefreshTokenTTL()).toBe(604800);
781
- });
782
- });
783
-
784
- // ============================================================================
785
- // Expiration Time Parsing
786
- // ============================================================================
787
-
788
- describe('parseExpiresIn edge cases', () => {
789
- it('should parse numeric expiresIn', async () => {
790
- const configWithNumber = {
791
- ...defaultConfig,
792
- accessToken: {
793
- ...defaultConfig.accessToken,
794
- expiresIn: 3600,
795
- },
796
- };
797
- const serviceWithNumber = new JwtService(configWithNumber);
798
- expect(serviceWithNumber.getAccessTokenExpiry()).toBe(3600);
799
- });
800
-
801
- it('should parse expiresIn with different units', async () => {
802
- const testCases = [
803
- { value: '60s', expected: 60 },
804
- { value: '1m', expected: 60 },
805
- { value: '1h', expected: 3600 },
806
- { value: '1d', expected: 86400 },
807
- ];
808
-
809
- for (const testCase of testCases) {
810
- const config = {
811
- ...defaultConfig,
812
- accessToken: {
813
- ...defaultConfig.accessToken,
814
- expiresIn: testCase.value,
815
- },
816
- };
817
- const service = new JwtService(config);
818
- expect(service.getAccessTokenExpiry()).toBe(testCase.expected);
819
- }
820
- });
821
-
822
- it('should throw error for invalid expiresIn format', async () => {
823
- const configWithInvalidFormat: JwtConfig = {
824
- ...defaultConfig,
825
- accessToken: {
826
- ...defaultConfig.accessToken,
827
- expiresIn: 'invalid' as any,
828
- },
829
- };
830
-
831
- // parseExpiresIn is called during token generation, not construction
832
- const serviceWithInvalidFormat = new JwtService(configWithInvalidFormat);
833
-
834
- try {
835
- await serviceWithInvalidFormat.generateTokenPair({
836
- userId: 'user-123',
837
- email: 'test@example.com',
838
- sessionId: 'session-456',
839
- });
840
- fail('Should have thrown an error');
841
- } catch (error) {
842
- // May throw NAuthException or TypeError from jose library
843
- expect(error).toBeDefined();
844
- expect(error instanceof NAuthException || error instanceof Error).toBe(true);
845
- }
846
- });
847
- });
848
-
849
- // ============================================================================
850
- // Error Handling
851
- // ============================================================================
852
-
853
- describe('error handling', () => {
854
- it('should handle validation errors gracefully', async () => {
855
- const result = await service.validateAccessToken('not.a.valid.jwt.token');
856
- expect(result.valid).toBe(false);
857
- expect(result.errorType).toBeDefined();
858
- });
859
-
860
- it('should handle missing keys error', async () => {
861
- const configWithoutKeys: JwtConfig = {
862
- ...defaultConfig,
863
- accessToken: {
864
- expiresIn: '15m',
865
- },
866
- };
867
- const serviceWithoutKeys = new JwtService(configWithoutKeys);
868
-
869
- try {
870
- await serviceWithoutKeys.generateAccessToken({
871
- userId: 'user-123',
872
- email: 'test@example.com',
873
- sessionId: 'session-456',
874
- tokenFamily: 'family-123',
875
- });
876
- fail('Should have thrown NAuthException');
877
- } catch (error) {
878
- expect(error).toBeInstanceOf(NAuthException);
879
- }
880
- });
881
- });
882
- });