@soapjs/soap-auth 0.3.1 → 0.3.3

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 (121) hide show
  1. package/build/__tests__/soap-auth.test.d.ts +1 -0
  2. package/build/__tests__/soap-auth.test.js +42 -0
  3. package/build/errors.d.ts +14 -3
  4. package/build/errors.js +29 -8
  5. package/build/index.d.ts +1 -1
  6. package/build/index.js +1 -1
  7. package/build/services/__tests__/account-lock.service.test.d.ts +1 -0
  8. package/build/services/__tests__/account-lock.service.test.js +55 -0
  9. package/build/services/__tests__/auth-throttle.service.test.d.ts +1 -0
  10. package/build/services/__tests__/auth-throttle.service.test.js +48 -0
  11. package/build/services/__tests__/jwks.service.test.d.ts +1 -0
  12. package/build/services/__tests__/jwks.service.test.js +39 -0
  13. package/build/services/__tests__/mfa.service.test.d.ts +1 -0
  14. package/build/services/__tests__/mfa.service.test.js +66 -0
  15. package/build/services/__tests__/password.service.test.d.ts +1 -0
  16. package/build/services/__tests__/password.service.test.js +66 -0
  17. package/build/services/__tests__/pkce.service.test.d.ts +1 -0
  18. package/build/services/__tests__/pkce.service.test.js +77 -0
  19. package/build/services/__tests__/rate-limit.service.test.d.ts +1 -0
  20. package/build/services/__tests__/rate-limit.service.test.js +37 -0
  21. package/build/services/__tests__/role.service.test.d.ts +1 -0
  22. package/build/services/__tests__/role.service.test.js +31 -0
  23. package/build/services/account-lock.service.d.ts +12 -0
  24. package/build/services/account-lock.service.js +39 -0
  25. package/build/services/auth-throttle.service.d.ts +10 -0
  26. package/build/services/auth-throttle.service.js +43 -0
  27. package/build/services/index.d.ts +8 -0
  28. package/build/{factories → services}/index.js +8 -3
  29. package/build/services/jwks.service.d.ts +7 -0
  30. package/build/services/jwks.service.js +41 -0
  31. package/build/services/mfa.service.d.ts +12 -0
  32. package/build/services/mfa.service.js +74 -0
  33. package/build/services/password.service.d.ts +14 -0
  34. package/build/services/password.service.js +78 -0
  35. package/build/services/pkce.service.d.ts +14 -0
  36. package/build/services/pkce.service.js +81 -0
  37. package/build/services/rate-limit.service.d.ts +9 -0
  38. package/build/services/rate-limit.service.js +26 -0
  39. package/build/services/role.service.d.ts +9 -0
  40. package/build/services/role.service.js +26 -0
  41. package/build/session/__tests__/file.session-store.test.d.ts +1 -0
  42. package/build/session/__tests__/file.session-store.test.js +117 -0
  43. package/build/session/__tests__/memory.session-store.test.d.ts +1 -0
  44. package/build/session/__tests__/memory.session-store.test.js +77 -0
  45. package/build/session/__tests__/session-handler.test.d.ts +1 -0
  46. package/build/session/__tests__/session-handler.test.js +337 -0
  47. package/build/session/file.session-store.d.ts +1 -0
  48. package/build/session/file.session-store.js +7 -0
  49. package/build/session/memory.session-store.d.ts +4 -1
  50. package/build/session/memory.session-store.js +11 -5
  51. package/build/session/session-handler.d.ts +12 -7
  52. package/build/session/session-handler.js +46 -13
  53. package/build/session/session.errors.d.ts +6 -0
  54. package/build/session/session.errors.js +15 -0
  55. package/build/soap-auth.d.ts +9 -8
  56. package/build/soap-auth.js +42 -29
  57. package/build/strategies/__tests__/base-auth.strategy.test.d.ts +14 -0
  58. package/build/strategies/__tests__/base-auth.strategy.test.js +137 -0
  59. package/build/strategies/__tests__/credential-auth.strategy.test.d.ts +14 -0
  60. package/build/strategies/__tests__/credential-auth.strategy.test.js +265 -0
  61. package/build/strategies/__tests__/token-auth.strategy.test.d.ts +28 -0
  62. package/build/strategies/__tests__/token-auth.strategy.test.js +298 -0
  63. package/build/strategies/api-key/__tests__/api-key.strategy.test.d.ts +1 -0
  64. package/build/strategies/api-key/__tests__/api-key.strategy.test.js +103 -0
  65. package/build/strategies/api-key/api-key.strategy.d.ts +5 -2
  66. package/build/strategies/api-key/api-key.strategy.js +43 -35
  67. package/build/strategies/api-key/api-key.tools.d.ts +2 -0
  68. package/build/strategies/api-key/api-key.tools.js +39 -0
  69. package/build/strategies/api-key/api-key.types.d.ts +10 -2
  70. package/build/strategies/base-auth.strategy.d.ts +11 -5
  71. package/build/strategies/base-auth.strategy.js +45 -52
  72. package/build/strategies/basic/__tests__/basic.strategy.test.d.ts +1 -0
  73. package/build/strategies/basic/__tests__/basic.strategy.test.js +104 -0
  74. package/build/strategies/basic/basic.strategy.d.ts +5 -7
  75. package/build/strategies/basic/basic.strategy.js +6 -6
  76. package/build/strategies/basic/basic.tools.d.ts +2 -0
  77. package/build/strategies/basic/basic.tools.js +44 -0
  78. package/build/strategies/credential-auth.strategy.d.ts +7 -17
  79. package/build/strategies/credential-auth.strategy.js +116 -181
  80. package/build/strategies/jwt/__tests__/jwt.strategy.test.d.ts +1 -0
  81. package/build/strategies/jwt/__tests__/jwt.strategy.test.js +156 -0
  82. package/build/strategies/jwt/__tests__/jwt.tools.test.d.ts +1 -0
  83. package/build/strategies/jwt/__tests__/jwt.tools.test.js +98 -0
  84. package/build/strategies/jwt/jwt.strategy.d.ts +13 -14
  85. package/build/strategies/jwt/jwt.strategy.js +57 -44
  86. package/build/strategies/jwt/jwt.tools.d.ts +20 -7
  87. package/build/strategies/jwt/jwt.tools.js +180 -81
  88. package/build/strategies/local/__tests__/local.strategy.test.d.ts +1 -0
  89. package/build/strategies/local/__tests__/local.strategy.test.js +115 -0
  90. package/build/strategies/local/local.strategy.d.ts +4 -3
  91. package/build/strategies/local/local.strategy.js +7 -6
  92. package/build/strategies/local/local.tools.d.ts +2 -0
  93. package/build/strategies/local/local.tools.js +44 -0
  94. package/build/strategies/oauth2/hybrid.oauth2.strategy.d.ts +5 -0
  95. package/build/strategies/oauth2/hybrid.oauth2.strategy.js +92 -0
  96. package/build/strategies/oauth2/oauth2.errors.d.ts +12 -0
  97. package/build/strategies/oauth2/oauth2.errors.js +24 -0
  98. package/build/strategies/oauth2/oauth2.strategy.d.ts +25 -15
  99. package/build/strategies/oauth2/oauth2.strategy.js +131 -141
  100. package/build/strategies/oauth2/oauth2.tools.d.ts +7 -2
  101. package/build/strategies/oauth2/oauth2.tools.js +119 -14
  102. package/build/strategies/oauth2/oauth2.types.d.ts +32 -1
  103. package/build/strategies/token-auth.strategy.d.ts +14 -8
  104. package/build/strategies/token-auth.strategy.js +162 -38
  105. package/build/tools/index.d.ts +0 -2
  106. package/build/tools/index.js +0 -2
  107. package/build/tools/tools.d.ts +2 -1
  108. package/build/tools/tools.js +9 -12
  109. package/build/types.d.ts +88 -57
  110. package/package.json +1 -1
  111. package/build/factories/auth-strategy.factory.d.ts +0 -9
  112. package/build/factories/auth-strategy.factory.js +0 -16
  113. package/build/factories/http-auth-strategy.factory.d.ts +0 -5
  114. package/build/factories/http-auth-strategy.factory.js +0 -41
  115. package/build/factories/index.d.ts +0 -3
  116. package/build/factories/socket-auth-strategy.factory.d.ts +0 -5
  117. package/build/factories/socket-auth-strategy.factory.js +0 -27
  118. package/build/tools/session.tools.d.ts +0 -6
  119. package/build/tools/session.tools.js +0 -15
  120. package/build/tools/token.tools.d.ts +0 -7
  121. package/build/tools/token.tools.js +0 -32
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TestTokenAuthStrategy = void 0;
4
+ const token_auth_strategy_1 = require("../token-auth.strategy");
5
+ const errors_1 = require("../../../src/errors");
6
+ class TestTokenAuthStrategy extends token_auth_strategy_1.TokenAuthStrategy {
7
+ async invalidateAccessToken(token, context) {
8
+ context.accessToken = undefined;
9
+ }
10
+ async invalidateRefreshToken(token, context) {
11
+ context.refreshToken = undefined;
12
+ }
13
+ constructor(config, session, logger) {
14
+ super(config, session, logger);
15
+ }
16
+ async fetchUser(payload) {
17
+ return { id: "testUserId", name: "Test User" };
18
+ }
19
+ extractAccessToken(context) {
20
+ return context.accessToken;
21
+ }
22
+ extractRefreshToken(context) {
23
+ return context.refreshToken;
24
+ }
25
+ async verifyAccessToken(token) {
26
+ return { userId: "testUserId" };
27
+ }
28
+ async verifyRefreshToken(token) {
29
+ return { userId: "testUserId" };
30
+ }
31
+ async generateAccessToken(user, context) {
32
+ return "newAccessToken";
33
+ }
34
+ async generateRefreshToken(user, context) {
35
+ return "newRefreshToken";
36
+ }
37
+ async storeAccessToken(token) {
38
+ }
39
+ async storeRefreshToken(token) {
40
+ }
41
+ embedAccessToken(token, context) {
42
+ context.accessToken = token;
43
+ }
44
+ embedRefreshToken(token, context) {
45
+ context.refreshToken = token;
46
+ }
47
+ }
48
+ exports.TestTokenAuthStrategy = TestTokenAuthStrategy;
49
+ const jsonwebtoken_1 = require("jsonwebtoken");
50
+ describe("TokenAuthStrategy", () => {
51
+ let config;
52
+ let strategy;
53
+ let mockSession;
54
+ let mockLogger;
55
+ let mockRateLimitService;
56
+ let mockRoleService;
57
+ let mockThrottleService;
58
+ let mockAccountLockService;
59
+ let mockMfaService;
60
+ let context;
61
+ beforeEach(() => {
62
+ config = {
63
+ refreshToken: {
64
+ enabled: true,
65
+ },
66
+ };
67
+ mockSession = {
68
+ logoutSession: jest.fn().mockResolvedValue(undefined),
69
+ };
70
+ mockLogger = {
71
+ warn: jest.fn(),
72
+ error: jest.fn(),
73
+ info: jest.fn(),
74
+ };
75
+ mockRateLimitService = {
76
+ checkRateLimit: jest.fn().mockResolvedValue(undefined),
77
+ };
78
+ mockRoleService = {
79
+ isAuthorized: jest.fn().mockResolvedValue(true),
80
+ };
81
+ mockThrottleService = {};
82
+ mockAccountLockService = {};
83
+ mockMfaService = {};
84
+ context = {
85
+ accessToken: undefined,
86
+ refreshToken: undefined,
87
+ };
88
+ strategy = new TestTokenAuthStrategy(config, mockSession, mockLogger);
89
+ strategy.rateLimit = mockRateLimitService;
90
+ strategy.role = mockRoleService;
91
+ strategy.throttle = mockThrottleService;
92
+ strategy.accountLock = mockAccountLockService;
93
+ strategy.mfa = mockMfaService;
94
+ });
95
+ describe("authenticate", () => {
96
+ it("should throw MissingTokenError if no access token and refreshToken is disabled in config", async () => {
97
+ config.refreshToken = undefined;
98
+ await expect(strategy.authenticate(context)).rejects.toThrowError(errors_1.MissingTokenError);
99
+ expect(mockRateLimitService.checkRateLimit).toHaveBeenCalledWith(context);
100
+ });
101
+ it("should verify access token if present and return user + tokens if valid", async () => {
102
+ context.accessToken = "validAccessToken";
103
+ const result = await strategy.authenticate(context);
104
+ expect(result.user).toEqual({ id: "testUserId", name: "Test User" });
105
+ expect(result.tokens).toEqual({ accessToken: "validAccessToken" });
106
+ expect(mockRoleService.isAuthorized).toHaveBeenCalledWith({
107
+ id: "testUserId",
108
+ name: "Test User",
109
+ });
110
+ expect(mockLogger.warn).not.toHaveBeenCalled();
111
+ });
112
+ it("should fallback to refresh token if access token is invalid or expired", async () => {
113
+ context.accessToken = "invalidAccessToken";
114
+ context.refreshToken = "validRefreshToken";
115
+ jest
116
+ .spyOn(strategy, "verifyAccessToken")
117
+ .mockRejectedValue(new jsonwebtoken_1.TokenExpiredError("expired", new Date()));
118
+ const refreshSpy = jest
119
+ .spyOn(strategy, "refreshTokens")
120
+ .mockResolvedValue({
121
+ user: { id: "testUserId", name: "Refreshed User" },
122
+ tokens: {
123
+ accessToken: "refreshedAccess",
124
+ refreshToken: "refreshedRefresh",
125
+ },
126
+ });
127
+ const result = await strategy.authenticate(context);
128
+ expect(mockLogger.warn).toHaveBeenCalledWith("Access token expired, attempting refresh...");
129
+ expect(refreshSpy).toHaveBeenCalledWith(context);
130
+ expect(result).toEqual({
131
+ user: { id: "testUserId", name: "Refreshed User" },
132
+ tokens: {
133
+ accessToken: "refreshedAccess",
134
+ refreshToken: "refreshedRefresh",
135
+ },
136
+ });
137
+ });
138
+ it("should throw MissingTokenError if no access token and no refresh token provided", async () => {
139
+ context.accessToken = undefined;
140
+ context.refreshToken = undefined;
141
+ await expect(strategy.authenticate(context)).rejects.toThrow(errors_1.MissingTokenError);
142
+ });
143
+ });
144
+ describe("refreshTokens", () => {
145
+ it("should throw an error if refresh tokens are not enabled in config", async () => {
146
+ config.refreshToken = undefined;
147
+ await expect(strategy.refreshTokens(context)).rejects.toThrowError("Refresh tokens are not enabled.");
148
+ });
149
+ it("should throw MissingTokenError if no refresh token is provided", async () => {
150
+ config.refreshToken = {};
151
+ await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.MissingTokenError);
152
+ });
153
+ it("should throw InvalidTokenError if refresh token is invalid", async () => {
154
+ context.refreshToken = "invalidRefreshToken";
155
+ jest
156
+ .spyOn(strategy, "verifyRefreshToken")
157
+ .mockRejectedValue(new errors_1.InvalidTokenError("Refresh"));
158
+ await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.InvalidTokenError);
159
+ });
160
+ it("should throw UserNotFoundError if user retrieval returns null", async () => {
161
+ context.refreshToken = "validRefresh";
162
+ jest.spyOn(strategy, "fetchUser").mockResolvedValueOnce(null);
163
+ await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.UserNotFoundError);
164
+ });
165
+ it("should return new access token (and refresh token) on success", async () => {
166
+ context.refreshToken = "validRefresh";
167
+ jest
168
+ .spyOn(strategy, "verifyRefreshToken")
169
+ .mockResolvedValueOnce({ userId: "testUserId" });
170
+ jest
171
+ .spyOn(strategy, "generateAccessToken")
172
+ .mockResolvedValueOnce("newAccessToken");
173
+ jest
174
+ .spyOn(strategy, "generateRefreshToken")
175
+ .mockResolvedValueOnce("newRefreshToken");
176
+ const storeAccessSpy = jest.spyOn(strategy, "storeAccessToken");
177
+ const storeRefreshSpy = jest.spyOn(strategy, "storeRefreshToken");
178
+ const result = await strategy.refreshTokens(context);
179
+ expect(storeAccessSpy).toHaveBeenCalledWith("newAccessToken");
180
+ expect(storeRefreshSpy).toHaveBeenCalledWith("newRefreshToken");
181
+ expect(result).toEqual({
182
+ user: { id: "testUserId", name: "Test User" },
183
+ tokens: {
184
+ accessToken: "newAccessToken",
185
+ refreshToken: "newRefreshToken",
186
+ },
187
+ });
188
+ });
189
+ describe("absoluteExpiry", () => {
190
+ it("should log a warning and throw InvalidTokenError if absolute expiry is exceeded (onExpiry=error)", async () => {
191
+ config.refreshToken = {
192
+ absoluteExpiry: {
193
+ payloadField: "absoluteExp",
194
+ onExpiry: "error",
195
+ },
196
+ };
197
+ context.refreshToken = "validRefresh";
198
+ jest
199
+ .spyOn(strategy, "verifyRefreshToken")
200
+ .mockResolvedValueOnce({
201
+ userId: "testUserId",
202
+ absoluteExp: Math.floor(Date.now() / 1000) - 1000,
203
+ });
204
+ await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.InvalidTokenError);
205
+ expect(mockLogger.warn).toHaveBeenCalledWith("Absolute expiry exceeded for refresh token.");
206
+ });
207
+ it("should call logoutSession and throw if onExpiry=logout", async () => {
208
+ config.refreshToken = {
209
+ absoluteExpiry: {
210
+ payloadField: "absoluteExp",
211
+ onExpiry: "logout",
212
+ },
213
+ };
214
+ context.refreshToken = "validRefresh";
215
+ jest
216
+ .spyOn(strategy, "verifyRefreshToken")
217
+ .mockResolvedValueOnce({
218
+ userId: "testUserId",
219
+ absoluteExp: Math.floor(Date.now() / 1000) - 1000,
220
+ });
221
+ await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.InvalidTokenError);
222
+ expect(mockSession.logoutSession).toHaveBeenCalledWith(context);
223
+ });
224
+ });
225
+ describe("rotation", () => {
226
+ beforeEach(() => {
227
+ config.refreshToken = {
228
+ enabled: true,
229
+ rotation: {
230
+ maxRotations: 3,
231
+ getRotationCount: jest.fn().mockResolvedValue(1),
232
+ isLimitReached: jest
233
+ .fn()
234
+ .mockImplementation((count, max) => count >= max),
235
+ rotateToken: jest.fn().mockResolvedValue({
236
+ newToken: "rotatedRefreshToken",
237
+ }),
238
+ afterRotation: jest.fn(),
239
+ },
240
+ };
241
+ context.refreshToken = "someOldRefreshToken";
242
+ });
243
+ it("should throw TokenRotationLimitReachedError if limit is reached", async () => {
244
+ config.refreshToken.rotation.getRotationCount.mockResolvedValueOnce(3);
245
+ await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.TokenRotationLimitReachedError);
246
+ });
247
+ it("should rotate the refresh token and call afterRotation if limit not reached", async () => {
248
+ config.refreshToken.rotation.getRotationCount.mockResolvedValueOnce(2);
249
+ jest
250
+ .spyOn(strategy, "verifyRefreshToken")
251
+ .mockResolvedValueOnce({ userId: "testUserId" });
252
+ jest
253
+ .spyOn(strategy, "generateAccessToken")
254
+ .mockResolvedValueOnce("newAccessToken");
255
+ const afterRotationSpy = config.refreshToken.rotation
256
+ .afterRotation;
257
+ const result = await strategy.refreshTokens(context);
258
+ expect(config.refreshToken.rotation.rotateToken).toHaveBeenCalledWith("someOldRefreshToken", { id: "testUserId", name: "Test User" }, context);
259
+ expect(afterRotationSpy).toHaveBeenCalledWith("someOldRefreshToken", "rotatedRefreshToken", { id: "testUserId", name: "Test User" }, context, 3);
260
+ expect(result).toEqual({
261
+ user: { id: "testUserId", name: "Test User" },
262
+ tokens: {
263
+ accessToken: "newAccessToken",
264
+ refreshToken: "rotatedRefreshToken",
265
+ },
266
+ });
267
+ });
268
+ it("should warn if rotation is enabled but rotateToken is not provided", async () => {
269
+ config.refreshToken.rotation.rotateToken = undefined;
270
+ jest
271
+ .spyOn(strategy, "verifyRefreshToken")
272
+ .mockResolvedValueOnce({ userId: "testUserId" });
273
+ jest
274
+ .spyOn(strategy, "generateAccessToken")
275
+ .mockResolvedValueOnce("newAccessToken");
276
+ const result = await strategy.refreshTokens(context);
277
+ expect(mockLogger.warn).toHaveBeenCalledWith("Rotation enabled but rotateToken not provided.");
278
+ expect(result).toEqual({
279
+ user: { id: "testUserId", name: "Test User" },
280
+ tokens: { accessToken: "newAccessToken" },
281
+ });
282
+ });
283
+ });
284
+ });
285
+ describe("error handling", () => {
286
+ it("should call onFailure", async () => {
287
+ const onFailureSpy = jest.spyOn(strategy, "onFailure");
288
+ jest
289
+ .spyOn(strategy, "verifyAccessToken")
290
+ .mockRejectedValue(new Error());
291
+ context.accessToken = "someToken";
292
+ await expect(strategy.authenticate(context)).rejects.toThrow(errors_1.InvalidTokenError);
293
+ expect(onFailureSpy).toHaveBeenCalledWith("authenticate", {
294
+ error: expect.any(Error),
295
+ });
296
+ });
297
+ });
298
+ });
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const api_key_strategy_1 = require("../api-key.strategy");
4
+ const errors_1 = require("../../../errors");
5
+ const api_key_errors_1 = require("../api-key.errors");
6
+ describe("ApiKeyStrategy", () => {
7
+ let strategy;
8
+ let mockConfig;
9
+ let mockLogger;
10
+ beforeEach(() => {
11
+ mockLogger = {
12
+ error: jest.fn(),
13
+ warn: jest.fn(),
14
+ info: jest.fn(),
15
+ };
16
+ mockConfig = {
17
+ extractApiKey: jest.fn(),
18
+ retrieveUserByApiKey: jest.fn(),
19
+ lock: {
20
+ isAccountLocked: jest.fn(),
21
+ logFailedAttempt: jest.fn(),
22
+ },
23
+ isApiKeyExpired: jest.fn(),
24
+ rateLimit: {
25
+ checkRateLimit: jest.fn(),
26
+ incrementRequestCount: jest.fn(),
27
+ },
28
+ role: { authorizeByRoles: jest.fn(), roles: [] },
29
+ revokeApiKey: jest.fn(),
30
+ trackApiKeyUsage: jest.fn(),
31
+ keyType: "long-term",
32
+ };
33
+ strategy = new api_key_strategy_1.ApiKeyStrategy(mockConfig, mockLogger);
34
+ });
35
+ it("should authenticate a user with a valid API key", async () => {
36
+ mockConfig.extractApiKey.mockReturnValue("valid-api-key");
37
+ mockConfig.retrieveUserByApiKey.mockResolvedValue({
38
+ id: 1,
39
+ name: "John Doe",
40
+ });
41
+ const result = await strategy.authenticate({});
42
+ expect(result).toEqual({ user: { id: 1, name: "John Doe" } });
43
+ expect(mockConfig.trackApiKeyUsage).toHaveBeenCalledWith("valid-api-key");
44
+ expect(mockConfig.rateLimit.incrementRequestCount).toHaveBeenCalledWith("valid-api-key");
45
+ });
46
+ it("should throw MissingApiKeyError if no API key is provided", async () => {
47
+ mockConfig.extractApiKey.mockReturnValue(null);
48
+ await expect(strategy.authenticate({})).rejects.toThrow(api_key_errors_1.MissingApiKeyError);
49
+ expect(mockConfig.lock.logFailedAttempt).toHaveBeenCalled();
50
+ });
51
+ it("should throw InvalidApiKeyError if the API key is invalid", async () => {
52
+ mockConfig.extractApiKey.mockReturnValue("invalid-api-key");
53
+ mockConfig.retrieveUserByApiKey.mockResolvedValue(null);
54
+ await expect(strategy.authenticate({})).rejects.toThrow(api_key_errors_1.InvalidApiKeyError);
55
+ expect(mockConfig.lock.logFailedAttempt).toHaveBeenCalledWith("invalid-api-key", {});
56
+ });
57
+ it("should throw AccountLockedError if account is locked", async () => {
58
+ mockConfig.extractApiKey.mockReturnValue("valid-api-key");
59
+ mockConfig.lock.isAccountLocked.mockResolvedValue(true);
60
+ await expect(strategy.authenticate({})).rejects.toThrow(errors_1.AccountLockedError);
61
+ });
62
+ it("should throw ExpiredApiKeyError if API key is expired", async () => {
63
+ mockConfig.extractApiKey.mockReturnValue("valid-api-key");
64
+ mockConfig.isApiKeyExpired.mockResolvedValue(true);
65
+ await expect(strategy.authenticate({})).rejects.toThrow(api_key_errors_1.ExpiredApiKeyError);
66
+ });
67
+ it("should throw RateLimitExceededError if API key exceeds rate limit", async () => {
68
+ mockConfig.extractApiKey.mockReturnValue("valid-api-key");
69
+ mockConfig.rateLimit.checkRateLimit.mockResolvedValue(true);
70
+ await expect(strategy.authenticate({})).rejects.toThrow(errors_1.RateLimitExceededError);
71
+ });
72
+ it("should throw UnauthorizedRoleError if user has no access", async () => {
73
+ mockConfig.extractApiKey.mockReturnValue("valid-api-key");
74
+ mockConfig.retrieveUserByApiKey.mockResolvedValue({ id: 1, role: "guest" });
75
+ mockConfig.role.authorizeByRoles.mockResolvedValue(false);
76
+ strategy.role.roles = ["admin", "user"];
77
+ await expect(strategy.authenticate({})).rejects.toThrow(errors_1.UnauthorizedRoleError);
78
+ });
79
+ it("should revoke one-time API keys after authentication", async () => {
80
+ mockConfig.keyType = "one-time";
81
+ mockConfig.extractApiKey.mockReturnValue("one-time-key");
82
+ mockConfig.retrieveUserByApiKey.mockResolvedValue({ id: 1 });
83
+ await strategy.authenticate({});
84
+ expect(mockConfig.revokeApiKey).toHaveBeenCalledWith("one-time-key");
85
+ });
86
+ it("should retry retrieving user on failure", async () => {
87
+ mockConfig.retrieveUserMaxRetries = 1;
88
+ mockConfig.extractApiKey.mockReturnValue("valid-api-key");
89
+ mockConfig.retrieveUserByApiKey
90
+ .mockRejectedValueOnce(new Error("Temporary failure"))
91
+ .mockResolvedValueOnce({ id: 1, name: "John Doe" });
92
+ const result = await strategy.authenticate({});
93
+ expect(result).toEqual({ user: { id: 1, name: "John Doe" } });
94
+ expect(mockConfig.retrieveUserByApiKey).toHaveBeenCalledTimes(2);
95
+ });
96
+ it("should log failed authentication attempts", async () => {
97
+ mockConfig.extractApiKey.mockReturnValue("invalid-api-key");
98
+ mockConfig.retrieveUserByApiKey.mockResolvedValue(null);
99
+ const ctx = {};
100
+ await expect(strategy.authenticate(ctx)).rejects.toThrow(api_key_errors_1.InvalidApiKeyError);
101
+ expect(mockConfig.lock.logFailedAttempt).toHaveBeenCalledWith("invalid-api-key", ctx);
102
+ });
103
+ });
@@ -4,12 +4,15 @@ import { ApiKeyStrategyConfig } from "./api-key.types";
4
4
  import { BaseAuthStrategy } from "../base-auth.strategy";
5
5
  export declare class ApiKeyStrategy<TContext = unknown, TUser = unknown> extends BaseAuthStrategy<TContext, TUser> implements AuthStrategy<TContext, TUser> {
6
6
  protected config: ApiKeyStrategyConfig<TContext, TUser>;
7
+ protected apiKeyValidity: {
8
+ sessionDuration: number;
9
+ longTermDuration: number;
10
+ };
7
11
  constructor(config: ApiKeyStrategyConfig<TContext, TUser>, logger: Soap.Logger);
12
+ protected fetchUser(apiKey: string, context: TContext): Promise<TUser | null>;
8
13
  init(): Promise<void>;
9
14
  authenticate(context?: TContext): Promise<AuthResult<TUser>>;
10
15
  authorize(user: TUser, action: string, resource?: string): Promise<boolean>;
11
16
  revoke(apiKey: string): Promise<void>;
12
17
  private trackApiKeyUsage;
13
- private incrementRequestCount;
14
- logout(context: TContext): Promise<void>;
15
18
  }
@@ -4,58 +4,79 @@ exports.ApiKeyStrategy = void 0;
4
4
  const api_key_errors_1 = require("./api-key.errors");
5
5
  const errors_1 = require("../../errors");
6
6
  const base_auth_strategy_1 = require("../base-auth.strategy");
7
+ const api_key_tools_1 = require("./api-key.tools");
7
8
  class ApiKeyStrategy extends base_auth_strategy_1.BaseAuthStrategy {
8
9
  config;
10
+ apiKeyValidity;
9
11
  constructor(config, logger) {
10
- super(config, null, logger);
12
+ super((0, api_key_tools_1.prepareApiKeyConfig)(config), null, logger);
11
13
  this.config = config;
12
14
  if (!this.config.extractApiKey || !this.config.retrieveUserByApiKey) {
13
15
  throw new Error("ApiKeyStrategy requires extractApiKey and retrieveUserByApiKey functions.");
14
16
  }
17
+ this.apiKeyValidity = {
18
+ sessionDuration: config.sessionDuration || 15 * 60 * 1000,
19
+ longTermDuration: config.longTermDuration || 90 * 24 * 60 * 60 * 1000,
20
+ };
21
+ }
22
+ async fetchUser(apiKey, context) {
23
+ const maxRetries = this.config.retrieveUserMaxRetries ?? 0;
24
+ const retryDelay = this.config.retrieveUserRetryDelay ?? 100;
25
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
26
+ try {
27
+ const user = await this.config.retrieveUserByApiKey(apiKey, context);
28
+ return user;
29
+ }
30
+ catch (error) {
31
+ this.logger.warn(`Attempt ${attempt + 1} failed: ${error}`);
32
+ if (attempt < maxRetries) {
33
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
34
+ }
35
+ else {
36
+ throw error;
37
+ }
38
+ }
39
+ }
40
+ return null;
15
41
  }
16
42
  async init() {
17
43
  return Promise.resolve();
18
44
  }
19
45
  async authenticate(context) {
46
+ let apiKey;
20
47
  try {
21
- const apiKey = this.config.extractApiKey(context);
48
+ apiKey = this.config.extractApiKey(context);
22
49
  if (!apiKey) {
23
50
  throw new api_key_errors_1.MissingApiKeyError();
24
51
  }
25
- if (await this.config.isAccountLocked?.(apiKey, context)) {
52
+ if (await this.accountLock?.isAccountLocked(apiKey)) {
26
53
  throw new errors_1.AccountLockedError();
27
54
  }
55
+ await this.rateLimit?.checkRateLimit(apiKey);
28
56
  if (await this.config.isApiKeyExpired?.(apiKey)) {
29
57
  throw new api_key_errors_1.ExpiredApiKeyError();
30
58
  }
31
- if (this.config.checkRateLimit &&
32
- (await this.config.checkRateLimit(apiKey))) {
33
- throw new errors_1.RateLimitExceededError();
34
- }
35
- const user = await this.config.retrieveUserByApiKey(apiKey);
59
+ const user = await this.fetchUser(apiKey, context);
36
60
  if (!user) {
37
- await this.config.logFailedAttempt?.(apiKey, context);
38
61
  throw new api_key_errors_1.InvalidApiKeyError();
39
62
  }
40
- if (this.config.authorizeByRoles) {
41
- const hasAccess = await this.config.authorizeByRoles(user, this.config.roles || []);
42
- if (!hasAccess) {
43
- throw new errors_1.UnauthorizedRoleError();
44
- }
63
+ await this.role?.isAuthorized(user);
64
+ if (this.config.keyType === "one-time") {
65
+ await this.config.revokeApiKey?.(apiKey);
45
66
  }
67
+ await this.rateLimit?.incrementRequestCount(apiKey);
46
68
  await this.trackApiKeyUsage(apiKey);
47
- await this.incrementRequestCount(apiKey);
48
69
  await this.onSuccess("authenticate", { user, context });
49
70
  return { user };
50
71
  }
51
72
  catch (error) {
52
- this.logger.error("API Key authentication error:", error);
53
- try {
54
- await this.onFailure("authenticate", { error, context });
55
- }
56
- catch (callbackError) {
57
- this.logger.error("onFailure callback error during authentication:", callbackError);
73
+ if (this.accountLock) {
74
+ this.accountLock.logFailedAttempt(apiKey, context);
58
75
  }
76
+ await this.onFailure("authenticate", {
77
+ context,
78
+ error,
79
+ });
59
80
  throw error;
60
81
  }
61
82
  }
@@ -75,24 +96,11 @@ class ApiKeyStrategy extends base_auth_strategy_1.BaseAuthStrategy {
75
96
  }
76
97
  async trackApiKeyUsage(apiKey) {
77
98
  try {
78
- if (this.config.trackApiKeyUsage) {
79
- await this.config.trackApiKeyUsage(apiKey);
80
- }
99
+ await this.config.trackApiKeyUsage?.(apiKey);
81
100
  }
82
101
  catch (error) {
83
102
  this.logger.warn("Failed to track API key usage:", error);
84
103
  }
85
104
  }
86
- async incrementRequestCount(apiKey) {
87
- try {
88
- await this.config.incrementRequestCount?.(apiKey);
89
- }
90
- catch (error) {
91
- this.logger.warn("Failed to increment request count:", error);
92
- }
93
- }
94
- logout(context) {
95
- return;
96
- }
97
105
  }
98
106
  exports.ApiKeyStrategy = ApiKeyStrategy;
@@ -0,0 +1,2 @@
1
+ import { ApiKeyStrategyConfig } from "./api-key.types";
2
+ export declare const prepareApiKeyConfig: <TContext = any, TUser = any>(config: Partial<ApiKeyStrategyConfig<TContext, TUser>>) => ApiKeyStrategyConfig<TContext, TUser>;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.prepareApiKeyConfig = void 0;
27
+ const Soap = __importStar(require("@soapjs/soap"));
28
+ const prepareApiKeyConfig = (config) => {
29
+ return Soap.removeUndefinedProperties({
30
+ ...config,
31
+ keyType: config.keyType || "long-term",
32
+ retrieveUserMaxRetries: config.retrieveUserMaxRetries ?? 3,
33
+ retrieveUserRetryDelay: config.retrieveUserRetryDelay ?? 1000,
34
+ sessionDuration: config.sessionDuration ?? 3600,
35
+ longTermDuration: config.longTermDuration ?? 86400,
36
+ trackApiKeyUsage: config.trackApiKeyUsage || (() => Promise.resolve()),
37
+ });
38
+ };
39
+ exports.prepareApiKeyConfig = prepareApiKeyConfig;
@@ -1,7 +1,15 @@
1
1
  import { RateLimitConfig, RoleAuthorizationConfig, AccountLockConfig, AuthResultConfig } from "../../types";
2
- export interface ApiKeyStrategyConfig<TContext = unknown, TUser = unknown> extends AuthResultConfig<TContext, TUser>, ApiKeyTrackingConfig, RateLimitConfig, RoleAuthorizationConfig<TUser>, AccountLockConfig<TContext> {
2
+ export interface ApiKeyStrategyConfig<TContext = unknown, TUser = unknown> extends AuthResultConfig<TContext, TUser>, ApiKeyTrackingConfig {
3
+ role?: RoleAuthorizationConfig<TUser>;
4
+ rateLimit?: RateLimitConfig;
5
+ lock?: AccountLockConfig<TContext>;
6
+ keyType: "one-time" | "long-term" | "session";
7
+ retrieveUserMaxRetries?: number;
8
+ retrieveUserRetryDelay?: number;
9
+ sessionDuration?: number;
10
+ longTermDuration?: number;
3
11
  extractApiKey: (context: TContext) => string | null;
4
- retrieveUserByApiKey: (apiKey: string) => Promise<TUser | null>;
12
+ retrieveUserByApiKey: (apiKey: string, context?: TContext) => Promise<TUser | null>;
5
13
  authorize?: (user: TUser, action: string, resource?: string) => Promise<boolean>;
6
14
  revokeApiKey?: (apiKey: string) => Promise<void>;
7
15
  }
@@ -1,18 +1,24 @@
1
1
  import * as Soap from "@soapjs/soap";
2
2
  import { AuthFailureContext, AuthResult, AuthStrategy, AuthSuccessContext, BaseAuthStrategyConfig } from "../types";
3
3
  import { SessionHandler } from "../session/session-handler";
4
+ import { AccountLockService } from "../services/account-lock.service";
5
+ import { MfaService } from "../services/mfa.service";
6
+ import { RateLimitService } from "../services/rate-limit.service";
7
+ import { RoleService } from "../services/role.service";
8
+ import { AuthThrottleService } from "../services/auth-throttle.service";
4
9
  export declare abstract class BaseAuthStrategy<TContext = unknown, TUser = unknown> implements AuthStrategy<TContext, TUser> {
5
10
  protected config: BaseAuthStrategyConfig<TContext, TUser>;
6
11
  protected session?: SessionHandler;
7
12
  protected logger?: Soap.Logger;
13
+ protected accountLock: AccountLockService<TContext>;
14
+ protected mfa: MfaService<TContext, TUser>;
15
+ protected rateLimit: RateLimitService;
16
+ protected role: RoleService<TUser>;
17
+ protected throttle: AuthThrottleService;
8
18
  abstract authenticate(context?: TContext): Promise<AuthResult<TUser>>;
9
- abstract logout(context: TContext): Promise<void>;
10
19
  constructor(config: BaseAuthStrategyConfig<TContext, TUser>, session?: SessionHandler, logger?: Soap.Logger);
11
20
  init(): Promise<void>;
12
- protected isAccountLocked(account: any): Promise<boolean>;
13
- protected isAuthorized(user: TUser): Promise<boolean>;
14
- protected checkRateLimit(data: unknown): Promise<void>;
15
- protected checkMfa(user: TUser, context: TContext): Promise<void>;
16
21
  protected onSuccess(action: string, context: AuthSuccessContext<TUser, TContext>): Promise<void>;
17
22
  protected onFailure(action: string, context: AuthFailureContext<TContext>): Promise<void>;
23
+ protected authenticateWithSession(context: TContext): Promise<AuthResult<TUser>>;
18
24
  }