@soapjs/soap-auth 0.3.3 → 0.4.4

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 (98) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/build/errors.d.ts +1 -1
  3. package/build/errors.js +2 -2
  4. package/build/index.d.ts +1 -0
  5. package/build/index.js +1 -0
  6. package/build/services/auth-throttle.service.d.ts +2 -2
  7. package/build/services/auth-throttle.service.js +2 -2
  8. package/build/services/index.d.ts +1 -0
  9. package/build/services/index.js +1 -0
  10. package/build/services/password.service.d.ts +7 -5
  11. package/build/services/password.service.js +76 -18
  12. package/build/services/totp.service.d.ts +16 -0
  13. package/build/services/totp.service.js +96 -0
  14. package/build/session/session-handler.d.ts +1 -0
  15. package/build/session/session-handler.js +39 -6
  16. package/build/soap-auth.d.ts +13 -6
  17. package/build/soap-auth.js +132 -5
  18. package/build/strategies/api-key/api-key.strategy.d.ts +4 -4
  19. package/build/strategies/api-key/api-key.strategy.js +3 -2
  20. package/build/strategies/base-auth.strategy.d.ts +5 -4
  21. package/build/strategies/basic/basic.strategy.d.ts +2 -1
  22. package/build/strategies/basic/basic.strategy.js +1 -0
  23. package/build/strategies/credential-auth.strategy.d.ts +7 -7
  24. package/build/strategies/credential-auth.strategy.js +9 -14
  25. package/build/strategies/index.d.ts +1 -0
  26. package/build/strategies/index.js +1 -0
  27. package/build/strategies/jwt/jwt.strategy.d.ts +3 -1
  28. package/build/strategies/jwt/jwt.strategy.js +41 -9
  29. package/build/strategies/jwt/jwt.tools.js +16 -14
  30. package/build/strategies/local/local.strategy.d.ts +6 -3
  31. package/build/strategies/local/local.strategy.js +83 -2
  32. package/build/strategies/oauth2/hybrid.oauth2.strategy.d.ts +3 -3
  33. package/build/strategies/oauth2/hybrid.oauth2.strategy.js +1 -6
  34. package/build/strategies/oauth2/oauth2.errors.d.ts +1 -0
  35. package/build/strategies/oauth2/oauth2.errors.js +4 -0
  36. package/build/strategies/oauth2/oauth2.strategy.d.ts +6 -4
  37. package/build/strategies/oauth2/oauth2.strategy.js +114 -46
  38. package/build/strategies/oauth2/oauth2.tools.js +2 -2
  39. package/build/strategies/oauth2/oauth2.types.d.ts +2 -2
  40. package/build/strategies/oauth2/providers/facebook.strategy.d.ts +11 -0
  41. package/build/strategies/oauth2/providers/facebook.strategy.js +58 -0
  42. package/build/strategies/oauth2/providers/github.strategy.d.ts +11 -0
  43. package/build/strategies/oauth2/providers/github.strategy.js +56 -0
  44. package/build/strategies/oauth2/providers/google.strategy.d.ts +11 -0
  45. package/build/strategies/oauth2/providers/google.strategy.js +52 -0
  46. package/build/strategies/oauth2/providers/http-oauth2.strategy.d.ts +16 -0
  47. package/build/strategies/oauth2/providers/http-oauth2.strategy.js +49 -0
  48. package/build/strategies/oauth2/providers/index.d.ts +5 -0
  49. package/build/strategies/oauth2/providers/index.js +21 -0
  50. package/build/strategies/oauth2/providers/provider.types.d.ts +7 -0
  51. package/build/strategies/oauth2/providers/provider.types.js +2 -0
  52. package/build/strategies/token-auth.strategy.d.ts +4 -4
  53. package/build/strategies/token-auth.strategy.js +2 -3
  54. package/build/tools/tools.js +1 -2
  55. package/build/types.d.ts +31 -32
  56. package/build/utils/validation.d.ts +23 -0
  57. package/build/utils/validation.js +139 -0
  58. package/package.json +8 -7
  59. package/build/__tests__/soap-auth.test.d.ts +0 -1
  60. package/build/__tests__/soap-auth.test.js +0 -42
  61. package/build/services/__tests__/account-lock.service.test.d.ts +0 -1
  62. package/build/services/__tests__/account-lock.service.test.js +0 -55
  63. package/build/services/__tests__/auth-throttle.service.test.d.ts +0 -1
  64. package/build/services/__tests__/auth-throttle.service.test.js +0 -48
  65. package/build/services/__tests__/jwks.service.test.d.ts +0 -1
  66. package/build/services/__tests__/jwks.service.test.js +0 -39
  67. package/build/services/__tests__/mfa.service.test.d.ts +0 -1
  68. package/build/services/__tests__/mfa.service.test.js +0 -66
  69. package/build/services/__tests__/password.service.test.d.ts +0 -1
  70. package/build/services/__tests__/password.service.test.js +0 -66
  71. package/build/services/__tests__/pkce.service.test.d.ts +0 -1
  72. package/build/services/__tests__/pkce.service.test.js +0 -77
  73. package/build/services/__tests__/rate-limit.service.test.d.ts +0 -1
  74. package/build/services/__tests__/rate-limit.service.test.js +0 -37
  75. package/build/services/__tests__/role.service.test.d.ts +0 -1
  76. package/build/services/__tests__/role.service.test.js +0 -31
  77. package/build/session/__tests__/file.session-store.test.d.ts +0 -1
  78. package/build/session/__tests__/file.session-store.test.js +0 -117
  79. package/build/session/__tests__/memory.session-store.test.d.ts +0 -1
  80. package/build/session/__tests__/memory.session-store.test.js +0 -77
  81. package/build/session/__tests__/session-handler.test.d.ts +0 -1
  82. package/build/session/__tests__/session-handler.test.js +0 -337
  83. package/build/strategies/__tests__/base-auth.strategy.test.d.ts +0 -14
  84. package/build/strategies/__tests__/base-auth.strategy.test.js +0 -137
  85. package/build/strategies/__tests__/credential-auth.strategy.test.d.ts +0 -14
  86. package/build/strategies/__tests__/credential-auth.strategy.test.js +0 -265
  87. package/build/strategies/__tests__/token-auth.strategy.test.d.ts +0 -28
  88. package/build/strategies/__tests__/token-auth.strategy.test.js +0 -298
  89. package/build/strategies/api-key/__tests__/api-key.strategy.test.d.ts +0 -1
  90. package/build/strategies/api-key/__tests__/api-key.strategy.test.js +0 -103
  91. package/build/strategies/basic/__tests__/basic.strategy.test.d.ts +0 -1
  92. package/build/strategies/basic/__tests__/basic.strategy.test.js +0 -104
  93. package/build/strategies/jwt/__tests__/jwt.strategy.test.d.ts +0 -1
  94. package/build/strategies/jwt/__tests__/jwt.strategy.test.js +0 -156
  95. package/build/strategies/jwt/__tests__/jwt.tools.test.d.ts +0 -1
  96. package/build/strategies/jwt/__tests__/jwt.tools.test.js +0 -98
  97. package/build/strategies/local/__tests__/local.strategy.test.d.ts +0 -1
  98. package/build/strategies/local/__tests__/local.strategy.test.js +0 -115
@@ -1,265 +0,0 @@
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.TestCredentialAuthStrategy = void 0;
27
- const Soap = __importStar(require("@soapjs/soap"));
28
- const errors_1 = require("../../../src/errors");
29
- const credential_auth_strategy_1 = require("../credential-auth.strategy");
30
- jest.mock("../../session/session-handler");
31
- jest.mock("../jwt/jwt.strategy");
32
- class TestCredentialAuthStrategy extends credential_auth_strategy_1.CredentialAuthStrategy {
33
- constructor(config, session, jwt, logger) {
34
- super(config, session, jwt, logger);
35
- }
36
- async verifyCredentials(identifier, password) {
37
- return true;
38
- }
39
- async extractCredentials(context) {
40
- return { identifier: "testUser", password: "testPass" };
41
- }
42
- async fetchUser(credentials) {
43
- return { id: "1", name: "Test User" };
44
- }
45
- }
46
- exports.TestCredentialAuthStrategy = TestCredentialAuthStrategy;
47
- describe("CredentialAuthStrategy", () => {
48
- let strategy;
49
- let config;
50
- let mockSession;
51
- let mockJwt;
52
- let mockLogger;
53
- const mockUser = { id: "user123", username: "testuser" };
54
- const context = { headers: {}, body: {} };
55
- beforeEach(() => {
56
- mockLogger = {
57
- info: jest.fn(),
58
- warn: jest.fn(),
59
- error: jest.fn(),
60
- };
61
- mockSession = {
62
- logoutSession: jest.fn().mockResolvedValue(undefined),
63
- issueSession: jest.fn().mockResolvedValue("session-id"),
64
- getSessionId: jest.fn().mockReturnValue("session-id"),
65
- getSessionData: jest
66
- .fn()
67
- .mockResolvedValue({ user: { id: "1", name: "Test" } }),
68
- };
69
- mockJwt = {
70
- authenticate: jest.fn(),
71
- issueTokens: jest.fn().mockResolvedValue({
72
- accessToken: "fakeAccess",
73
- refreshToken: "fakeRefresh",
74
- }),
75
- };
76
- config = {
77
- failedAttempts: {
78
- incrementFailedAttempts: jest.fn(),
79
- resetFailedAttempts: jest.fn(),
80
- getFailedAttempts: jest.fn(),
81
- },
82
- role: {
83
- authorizeByRoles: jest.fn().mockResolvedValue(true),
84
- roles: ["user"],
85
- },
86
- rateLimit: {
87
- checkRateLimit: jest.fn().mockResolvedValue(false),
88
- },
89
- mfa: {
90
- isMfaRequired: jest.fn().mockReturnValue(false),
91
- },
92
- passwordPolicy: {
93
- updatePassword: jest.fn(),
94
- generateResetToken: jest.fn(),
95
- validateResetToken: jest.fn(),
96
- sendResetEmail: jest.fn(),
97
- },
98
- security: {
99
- maxFailedLoginAttempts: 3,
100
- lockoutDuration: 10,
101
- notifyOnLockout: jest.fn(),
102
- },
103
- lock: {
104
- isAccountLocked: jest.fn(),
105
- },
106
- user: {
107
- fetchUser: jest.fn(),
108
- },
109
- };
110
- strategy = new TestCredentialAuthStrategy(config, mockSession, mockJwt, mockLogger);
111
- });
112
- describe("authenticate", () => {
113
- it("should use JWT if available and return its result", async () => {
114
- mockJwt.authenticate.mockResolvedValue({
115
- user: { id: "1", name: "JWT User" },
116
- });
117
- const result = await strategy.authenticate(context);
118
- expect(mockJwt.authenticate).toHaveBeenCalledWith(context);
119
- expect(result).toEqual({
120
- user: { id: "1", name: "JWT User" },
121
- });
122
- });
123
- it("should fall back to authenticateWithSession if JWT throws an error", async () => {
124
- mockJwt.authenticate.mockRejectedValue(new Error("JWT error"));
125
- const result = await strategy.authenticate(context);
126
- expect(mockLogger.warn).toHaveBeenCalledWith("JWT authentication failed, falling back to session.");
127
- expect(result.user).toEqual({ id: "1", name: "Test" });
128
- });
129
- it("should use session if JWT is not defined but session is available", async () => {
130
- strategy = new TestCredentialAuthStrategy(config, mockSession, undefined, mockLogger);
131
- const result = await strategy.authenticate(context);
132
- expect(result.user).toEqual({ id: "1", name: "Test" });
133
- });
134
- it("should return user=null if no JWT/session and allowGuest = true", async () => {
135
- config.allowGuest = true;
136
- strategy = new TestCredentialAuthStrategy(config, undefined, undefined, mockLogger);
137
- const result = await strategy.authenticate(context);
138
- expect(mockLogger.warn).toHaveBeenCalledWith("No authentication method found. Proceeding as guest.");
139
- expect(result.user).toBeNull();
140
- });
141
- it("should throw UserNotFoundError if no JWT/session and allowGuest = false", async () => {
142
- config.allowGuest = false;
143
- strategy = new TestCredentialAuthStrategy(config, undefined, undefined, mockLogger);
144
- await expect(strategy.authenticate(context)).rejects.toThrow(errors_1.UserNotFoundError);
145
- });
146
- });
147
- describe("login", () => {
148
- it("should throw MissingCredentialsError if credentials are missing", async () => {
149
- jest
150
- .spyOn(strategy, "extractCredentials")
151
- .mockResolvedValueOnce(undefined);
152
- await expect(strategy.login(context)).rejects.toThrow(errors_1.MissingCredentialsError);
153
- });
154
- it("should throw InvalidCredentialsError if verifyCredentials returns false", async () => {
155
- jest
156
- .spyOn(strategy, "verifyCredentials")
157
- .mockResolvedValueOnce(false);
158
- await expect(strategy.login(context)).rejects.toThrow(errors_1.InvalidCredentialsError);
159
- });
160
- it("should throw ExpiredPasswordError if the password is expired", async () => {
161
- jest
162
- .spyOn(strategy.password, "isPasswordChangeRequired")
163
- .mockResolvedValueOnce(true);
164
- await expect(strategy.login(context)).rejects.toThrow(errors_1.ExpiredPasswordError);
165
- });
166
- it("should return user, tokens, and session on success", async () => {
167
- const result = await strategy.login(context);
168
- expect(result.user).toBeDefined();
169
- expect(result.tokens).toBeDefined();
170
- expect(result.session).toBeDefined();
171
- });
172
- });
173
- describe("logout", () => {
174
- it("should call session.logoutSession and onSuccess", async () => {
175
- const onSuccessSpy = jest.spyOn(strategy, "onSuccess");
176
- await strategy.logout(context);
177
- expect(mockSession.logoutSession).toHaveBeenCalledWith(context);
178
- expect(onSuccessSpy).toHaveBeenCalledWith("logout", context);
179
- });
180
- it("should call onFailure and rethrow the error if session.logoutSession fails", async () => {
181
- const onFailureSpy = jest.spyOn(strategy, "onFailure");
182
- mockSession.logoutSession.mockRejectedValueOnce(new Error("Session error"));
183
- await expect(strategy.logout(context)).rejects.toThrow("Session error");
184
- expect(onFailureSpy).toHaveBeenCalledWith("logout", {
185
- error: expect.any(Error),
186
- });
187
- });
188
- });
189
- describe("requestPasswordReset", () => {
190
- it("should throw NotImplementedError if generateResetToken is not configured", async () => {
191
- config.passwordPolicy.generateResetToken = undefined;
192
- await expect(strategy.requestPasswordReset("testUser")).rejects.toThrow(Soap.NotImplementedError);
193
- });
194
- it("should call generateResetToken and sendResetEmail (if email is provided)", async () => {
195
- config.passwordPolicy.generateResetToken = jest
196
- .fn()
197
- .mockResolvedValue("mockToken");
198
- config.passwordPolicy.sendResetEmail = jest
199
- .fn()
200
- .mockResolvedValue(undefined);
201
- await strategy.requestPasswordReset("testUser", "test@example.com");
202
- expect(config.passwordPolicy.generateResetToken).toHaveBeenCalledWith("testUser");
203
- expect(config.passwordPolicy.sendResetEmail).toHaveBeenCalledWith("test@example.com", "mockToken");
204
- });
205
- it("should call onSuccess with the correct data", async () => {
206
- config.passwordPolicy.generateResetToken = jest
207
- .fn()
208
- .mockResolvedValue("mockToken");
209
- const onSuccessSpy = jest.spyOn(strategy, "onSuccess");
210
- await strategy.requestPasswordReset("testUser");
211
- expect(onSuccessSpy).toHaveBeenCalledWith("request_password_reset", {
212
- identifier: "testUser",
213
- tokens: { reset: "mockToken" },
214
- });
215
- });
216
- });
217
- describe("resetPassword", () => {
218
- it("should throw NotImplementedError if validateResetToken or updatePassword are not configured", async () => {
219
- config.passwordPolicy.updatePassword = undefined;
220
- await expect(strategy.resetPassword("testUser", "token", "newPass")).rejects.toThrow(Soap.NotImplementedError);
221
- });
222
- it("should throw ExpiredResetTokenError if validateResetToken returns false", async () => {
223
- config.passwordPolicy.validateResetToken = jest
224
- .fn()
225
- .mockResolvedValue(false);
226
- config.passwordPolicy.updatePassword = jest.fn();
227
- await expect(strategy.resetPassword("testUser", "token", "newPass")).rejects.toThrow(errors_1.ExpiredResetTokenError);
228
- });
229
- it("should call updatePassword if the token is valid", async () => {
230
- config.passwordPolicy.validateResetToken = jest
231
- .fn()
232
- .mockResolvedValue(true);
233
- config.passwordPolicy.updatePassword = jest
234
- .fn()
235
- .mockResolvedValue(undefined);
236
- await strategy.resetPassword("testUser", "validToken", "newPass");
237
- expect(config.passwordPolicy.updatePassword).toHaveBeenCalledWith("testUser", "newPass");
238
- });
239
- });
240
- describe("changePassword", () => {
241
- it("should throw NotImplementedError if updatePassword is not configured", async () => {
242
- config.passwordPolicy.updatePassword = undefined;
243
- await expect(strategy.changePassword("testUser", "oldPass", "newPass")).rejects.toThrow(Soap.NotImplementedError);
244
- });
245
- it("should throw InvalidCredentialsError if verifyCredentials returns false", async () => {
246
- config.passwordPolicy.updatePassword = jest
247
- .fn()
248
- .mockResolvedValue(undefined);
249
- jest
250
- .spyOn(strategy, "verifyCredentials")
251
- .mockResolvedValueOnce(false);
252
- await expect(strategy.changePassword("testUser", "oldPass", "newPass")).rejects.toThrow(errors_1.InvalidCredentialsError);
253
- });
254
- it("should call updatePassword with new password if old credentials are correct", async () => {
255
- config.passwordPolicy.updatePassword = jest
256
- .fn()
257
- .mockResolvedValue(undefined);
258
- jest
259
- .spyOn(strategy, "verifyCredentials")
260
- .mockResolvedValueOnce(true);
261
- await strategy.changePassword("testUser", "oldPass", "newPass");
262
- expect(config.passwordPolicy.updatePassword).toHaveBeenCalledWith("testUser", "newPass");
263
- });
264
- });
265
- });
@@ -1,28 +0,0 @@
1
- import * as Soap from "@soapjs/soap";
2
- import { TokenAuthStrategy } from "../token-auth.strategy";
3
- import { SessionHandler } from "../../session";
4
- import { TokenAuthStrategyConfig } from "../../../src/types";
5
- export interface MockUser {
6
- id: string;
7
- name: string;
8
- }
9
- export interface MockContext {
10
- accessToken?: string;
11
- refreshToken?: string;
12
- }
13
- export declare class TestTokenAuthStrategy extends TokenAuthStrategy<MockContext, MockUser> {
14
- protected invalidateAccessToken(token: string, context?: MockContext): Promise<void>;
15
- protected invalidateRefreshToken(token: string, context?: MockContext): Promise<void>;
16
- constructor(config: TokenAuthStrategyConfig<MockContext, MockUser>, session?: SessionHandler, logger?: Soap.Logger);
17
- protected fetchUser(payload: any): Promise<MockUser | null>;
18
- protected extractAccessToken(context: MockContext): string | undefined;
19
- protected extractRefreshToken(context: MockContext): string | undefined;
20
- protected verifyAccessToken(token: string): Promise<any>;
21
- protected verifyRefreshToken(token: string): Promise<any>;
22
- protected generateAccessToken(user: MockUser, context: MockContext): Promise<string>;
23
- protected generateRefreshToken(user: MockUser, context: MockContext): Promise<string>;
24
- protected storeAccessToken(token: string): Promise<void>;
25
- protected storeRefreshToken(token: string): Promise<void>;
26
- protected embedAccessToken(token: string, context: MockContext): void;
27
- protected embedRefreshToken(token: string, context: MockContext): void;
28
- }
@@ -1,298 +0,0 @@
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
- });