@soapjs/soap-auth 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +261 -128
  2. package/build/index.d.ts +1 -0
  3. package/build/index.js +1 -0
  4. package/build/recipes/auth-config.recipes.d.ts +40 -0
  5. package/build/recipes/auth-config.recipes.js +135 -0
  6. package/build/recipes/http-context.helpers.d.ts +13 -0
  7. package/build/recipes/http-context.helpers.js +64 -0
  8. package/build/recipes/index.d.ts +3 -0
  9. package/build/recipes/index.js +19 -0
  10. package/build/recipes/oauth2-presets.d.ts +20 -0
  11. package/build/recipes/oauth2-presets.js +74 -0
  12. package/build/soap-auth.js +62 -0
  13. package/build/strategies/jwt/jwt.strategy.js +6 -3
  14. package/build/strategies/jwt/jwt.tools.js +8 -6
  15. package/build/strategies/local/local.strategy.d.ts +2 -2
  16. package/build/strategies/local/local.strategy.js +7 -7
  17. package/build/strategies/oauth2/hybrid.oauth2.strategy.js +1 -1
  18. package/build/strategies/oauth2/oauth2.strategy.js +2 -2
  19. package/build/strategies/oauth2/oauth2.types.d.ts +5 -0
  20. package/build/strategies/oauth2/providers/configurable-hybrid-oauth2.strategy.d.ts +19 -0
  21. package/build/strategies/oauth2/providers/configurable-hybrid-oauth2.strategy.js +85 -0
  22. package/build/strategies/oauth2/providers/configurable-oauth2.strategy.d.ts +11 -0
  23. package/build/strategies/oauth2/providers/configurable-oauth2.strategy.js +46 -0
  24. package/build/strategies/oauth2/providers/index.d.ts +2 -0
  25. package/build/strategies/oauth2/providers/index.js +2 -0
  26. package/build/strategies/oauth2/providers/provider.types.d.ts +3 -0
  27. package/build/types.d.ts +5 -2
  28. package/package.json +91 -13
  29. package/.claude/settings.local.json +0 -20
  30. package/build/__tests__/soap-auth.test.d.ts +0 -1
  31. package/build/__tests__/soap-auth.test.js +0 -136
  32. package/build/services/__tests__/account-lock.service.test.d.ts +0 -1
  33. package/build/services/__tests__/account-lock.service.test.js +0 -55
  34. package/build/services/__tests__/auth-throttle.service.test.d.ts +0 -1
  35. package/build/services/__tests__/auth-throttle.service.test.js +0 -48
  36. package/build/services/__tests__/jwks.service.test.d.ts +0 -1
  37. package/build/services/__tests__/jwks.service.test.js +0 -39
  38. package/build/services/__tests__/mfa.service.test.d.ts +0 -1
  39. package/build/services/__tests__/mfa.service.test.js +0 -66
  40. package/build/services/__tests__/password.service.test.d.ts +0 -1
  41. package/build/services/__tests__/password.service.test.js +0 -73
  42. package/build/services/__tests__/pkce.service.test.d.ts +0 -1
  43. package/build/services/__tests__/pkce.service.test.js +0 -77
  44. package/build/services/__tests__/rate-limit.service.test.d.ts +0 -1
  45. package/build/services/__tests__/rate-limit.service.test.js +0 -37
  46. package/build/services/__tests__/role.service.test.d.ts +0 -1
  47. package/build/services/__tests__/role.service.test.js +0 -31
  48. package/build/services/__tests__/totp.service.test.d.ts +0 -1
  49. package/build/services/__tests__/totp.service.test.js +0 -120
  50. package/build/session/__tests__/file.session-store.test.d.ts +0 -1
  51. package/build/session/__tests__/file.session-store.test.js +0 -117
  52. package/build/session/__tests__/memory.session-store.test.d.ts +0 -1
  53. package/build/session/__tests__/memory.session-store.test.js +0 -77
  54. package/build/session/__tests__/session-handler.test.d.ts +0 -1
  55. package/build/session/__tests__/session-handler.test.js +0 -345
  56. package/build/strategies/__tests__/base-auth.strategy.test.d.ts +0 -15
  57. package/build/strategies/__tests__/base-auth.strategy.test.js +0 -138
  58. package/build/strategies/__tests__/credential-auth.strategy.test.d.ts +0 -15
  59. package/build/strategies/__tests__/credential-auth.strategy.test.js +0 -266
  60. package/build/strategies/__tests__/token-auth.strategy.test.d.ts +0 -29
  61. package/build/strategies/__tests__/token-auth.strategy.test.js +0 -299
  62. package/build/strategies/api-key/__tests__/api-key.strategy.test.d.ts +0 -1
  63. package/build/strategies/api-key/__tests__/api-key.strategy.test.js +0 -103
  64. package/build/strategies/basic/__tests__/basic.strategy.test.d.ts +0 -1
  65. package/build/strategies/basic/__tests__/basic.strategy.test.js +0 -104
  66. package/build/strategies/jwt/__tests__/jwt.strategy.test.d.ts +0 -1
  67. package/build/strategies/jwt/__tests__/jwt.strategy.test.js +0 -156
  68. package/build/strategies/jwt/__tests__/jwt.tools.test.d.ts +0 -1
  69. package/build/strategies/jwt/__tests__/jwt.tools.test.js +0 -98
  70. package/build/strategies/local/__tests__/local.strategy.test.d.ts +0 -1
  71. package/build/strategies/local/__tests__/local.strategy.test.js +0 -121
  72. package/build/strategies/oauth2/__tests__/oauth2.strategy.test.d.ts +0 -1
  73. package/build/strategies/oauth2/__tests__/oauth2.strategy.test.js +0 -239
  74. package/build/strategies/oauth2/providers/__tests__/social-providers.test.d.ts +0 -1
  75. package/build/strategies/oauth2/providers/__tests__/social-providers.test.js +0 -201
  76. package/build/utils/__tests__/validation.test.d.ts +0 -1
  77. package/build/utils/__tests__/validation.test.js +0 -181
  78. package/jest.config.unit.json +0 -10
@@ -1,266 +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
- name = "test-credential";
34
- constructor(config, session, jwt, logger) {
35
- super(config, session, jwt, logger);
36
- }
37
- async verifyCredentials(identifier, password) {
38
- return true;
39
- }
40
- async extractCredentials(context) {
41
- return { identifier: "testUser", password: "testPass" };
42
- }
43
- async fetchUser(credentials) {
44
- return { id: "1", name: "Test User" };
45
- }
46
- }
47
- exports.TestCredentialAuthStrategy = TestCredentialAuthStrategy;
48
- describe("CredentialAuthStrategy", () => {
49
- let strategy;
50
- let config;
51
- let mockSession;
52
- let mockJwt;
53
- let mockLogger;
54
- const mockUser = { id: "user123", username: "testuser" };
55
- const context = { headers: {}, body: {} };
56
- beforeEach(() => {
57
- mockLogger = {
58
- info: jest.fn(),
59
- warn: jest.fn(),
60
- error: jest.fn(),
61
- };
62
- mockSession = {
63
- logoutSession: jest.fn().mockResolvedValue(undefined),
64
- issueSession: jest.fn().mockResolvedValue("session-id"),
65
- getSessionId: jest.fn().mockReturnValue("session-id"),
66
- getSessionData: jest
67
- .fn()
68
- .mockResolvedValue({ user: { id: "1", name: "Test" } }),
69
- };
70
- mockJwt = {
71
- authenticate: jest.fn(),
72
- issueTokens: jest.fn().mockResolvedValue({
73
- accessToken: "fakeAccess",
74
- refreshToken: "fakeRefresh",
75
- }),
76
- };
77
- config = {
78
- failedAttempts: {
79
- incrementFailedAttempts: jest.fn(),
80
- resetFailedAttempts: jest.fn(),
81
- getFailedAttempts: jest.fn(),
82
- },
83
- role: {
84
- authorizeByRoles: jest.fn().mockResolvedValue(true),
85
- roles: ["user"],
86
- },
87
- rateLimit: {
88
- checkRateLimit: jest.fn().mockResolvedValue(false),
89
- },
90
- mfa: {
91
- isMfaRequired: jest.fn().mockReturnValue(false),
92
- },
93
- passwordPolicy: {
94
- updatePassword: jest.fn(),
95
- generateResetToken: jest.fn(),
96
- validatePasswordResetToken: jest.fn(),
97
- sendPasswordResetEmail: jest.fn(),
98
- },
99
- security: {
100
- maxFailedLoginAttempts: 3,
101
- lockoutDuration: 10,
102
- notifyOnLockout: jest.fn(),
103
- },
104
- lock: {
105
- isAccountLocked: jest.fn(),
106
- },
107
- user: {
108
- fetchUser: jest.fn(),
109
- },
110
- };
111
- strategy = new TestCredentialAuthStrategy(config, mockSession, mockJwt, mockLogger);
112
- });
113
- describe("authenticate", () => {
114
- it("should use JWT if available and return its result", async () => {
115
- mockJwt.authenticate.mockResolvedValue({
116
- user: { id: "1", name: "JWT User" },
117
- });
118
- const result = await strategy.authenticate(context);
119
- expect(mockJwt.authenticate).toHaveBeenCalledWith(context);
120
- expect(result).toEqual({
121
- user: { id: "1", name: "JWT User" },
122
- });
123
- });
124
- it("should fall back to authenticateWithSession if JWT throws an error", async () => {
125
- mockJwt.authenticate.mockRejectedValue(new Error("JWT error"));
126
- const result = await strategy.authenticate(context);
127
- expect(mockLogger.warn).toHaveBeenCalledWith("JWT authentication failed, falling back to session.");
128
- expect(result.user).toEqual({ id: "1", name: "Test" });
129
- });
130
- it("should use session if JWT is not defined but session is available", async () => {
131
- strategy = new TestCredentialAuthStrategy(config, mockSession, undefined, mockLogger);
132
- const result = await strategy.authenticate(context);
133
- expect(result.user).toEqual({ id: "1", name: "Test" });
134
- });
135
- it("should return user=null if no JWT/session and allowGuest = true", async () => {
136
- config.allowGuest = true;
137
- strategy = new TestCredentialAuthStrategy(config, undefined, undefined, mockLogger);
138
- const result = await strategy.authenticate(context);
139
- expect(mockLogger.warn).toHaveBeenCalledWith("No authentication method found. Proceeding as guest.");
140
- expect(result.user).toBeNull();
141
- });
142
- it("should throw UserNotFoundError if no JWT/session and allowGuest = false", async () => {
143
- config.allowGuest = false;
144
- strategy = new TestCredentialAuthStrategy(config, undefined, undefined, mockLogger);
145
- await expect(strategy.authenticate(context)).rejects.toThrow(errors_1.UserNotFoundError);
146
- });
147
- });
148
- describe("login", () => {
149
- it("should throw MissingCredentialsError if credentials are missing", async () => {
150
- jest
151
- .spyOn(strategy, "extractCredentials")
152
- .mockResolvedValueOnce(undefined);
153
- await expect(strategy.login(context)).rejects.toThrow(errors_1.MissingCredentialsError);
154
- });
155
- it("should throw InvalidCredentialsError if verifyCredentials returns false", async () => {
156
- jest
157
- .spyOn(strategy, "verifyCredentials")
158
- .mockResolvedValueOnce(false);
159
- await expect(strategy.login(context)).rejects.toThrow(errors_1.InvalidCredentialsError);
160
- });
161
- it("should throw ExpiredPasswordError if the password is expired", async () => {
162
- jest
163
- .spyOn(strategy.password, "isPasswordChangeRequired")
164
- .mockResolvedValueOnce(true);
165
- await expect(strategy.login(context)).rejects.toThrow(errors_1.ExpiredPasswordError);
166
- });
167
- it("should return user, tokens, and session on success", async () => {
168
- const result = await strategy.login(context);
169
- expect(result.user).toBeDefined();
170
- expect(result.tokens).toBeDefined();
171
- expect(result.session).toBeDefined();
172
- });
173
- });
174
- describe("logout", () => {
175
- it("should call session.logoutSession and onSuccess", async () => {
176
- const onSuccessSpy = jest.spyOn(strategy, "onSuccess");
177
- await strategy.logout(context);
178
- expect(mockSession.logoutSession).toHaveBeenCalledWith(context);
179
- expect(onSuccessSpy).toHaveBeenCalledWith("logout", context);
180
- });
181
- it("should call onFailure and rethrow the error if session.logoutSession fails", async () => {
182
- const onFailureSpy = jest.spyOn(strategy, "onFailure");
183
- mockSession.logoutSession.mockRejectedValueOnce(new Error("Session error"));
184
- await expect(strategy.logout(context)).rejects.toThrow("Session error");
185
- expect(onFailureSpy).toHaveBeenCalledWith("logout", {
186
- error: expect.any(Error),
187
- });
188
- });
189
- });
190
- describe("requestPasswordReset", () => {
191
- it("should throw NotImplementedError if generateResetToken is not configured", async () => {
192
- config.passwordPolicy.generateResetToken = undefined;
193
- await expect(strategy.requestPasswordResetLink("testUser")).rejects.toThrow(Soap.NotImplementedError);
194
- });
195
- it("should call generateResetToken and sendResetEmail (if email is provided)", async () => {
196
- config.passwordPolicy.generateResetToken = jest
197
- .fn()
198
- .mockResolvedValue("mockToken");
199
- config.passwordPolicy.sendPasswordResetEmail = jest
200
- .fn()
201
- .mockResolvedValue(undefined);
202
- await strategy.requestPasswordResetLink("testUser", "test@example.com");
203
- expect(config.passwordPolicy.generateResetToken).toHaveBeenCalledWith("testUser");
204
- expect(config.passwordPolicy.sendPasswordResetEmail).toHaveBeenCalledWith("test@example.com", "mockToken");
205
- });
206
- it("should call onSuccess with the correct data", async () => {
207
- config.passwordPolicy.generateResetToken = jest
208
- .fn()
209
- .mockResolvedValue("mockToken");
210
- const onSuccessSpy = jest.spyOn(strategy, "onSuccess");
211
- await strategy.requestPasswordResetLink("testUser");
212
- expect(onSuccessSpy).toHaveBeenCalledWith("request_password_reset", {
213
- identifier: "testUser",
214
- tokens: { reset: "mockToken" },
215
- });
216
- });
217
- });
218
- describe("resetPassword", () => {
219
- it("should throw NotImplementedError if validateResetToken or updatePassword are not configured", async () => {
220
- config.passwordPolicy.updatePassword = undefined;
221
- await expect(strategy.resetPasswordWithToken("testUser", "token", "newPass")).rejects.toThrow(Soap.NotImplementedError);
222
- });
223
- it("should throw ExpiredResetTokenError if validateResetToken returns false", async () => {
224
- config.passwordPolicy.validatePasswordResetToken = jest
225
- .fn()
226
- .mockResolvedValue(false);
227
- config.passwordPolicy.updatePassword = jest.fn();
228
- await expect(strategy.resetPasswordWithToken("testUser", "token", "newPass")).rejects.toThrow(errors_1.ExpiredResetTokenError);
229
- });
230
- it("should call updatePassword if the token is valid", async () => {
231
- config.passwordPolicy.validatePasswordResetToken = jest
232
- .fn()
233
- .mockResolvedValue(true);
234
- config.passwordPolicy.updatePassword = jest
235
- .fn()
236
- .mockResolvedValue(undefined);
237
- await strategy.resetPasswordWithToken("testUser", "validToken", "newPass");
238
- expect(config.passwordPolicy.updatePassword).toHaveBeenCalledWith("testUser", "newPass", undefined);
239
- });
240
- });
241
- describe("changePassword", () => {
242
- it("should throw NotImplementedError if updatePassword is not configured", async () => {
243
- config.passwordPolicy.updatePassword = undefined;
244
- await expect(strategy.replacePassword("testUser", "oldPass", "newPass")).rejects.toThrow(Soap.NotImplementedError);
245
- });
246
- it("should throw InvalidCredentialsError if verifyCredentials returns false", async () => {
247
- config.passwordPolicy.updatePassword = jest
248
- .fn()
249
- .mockResolvedValue(undefined);
250
- jest
251
- .spyOn(strategy, "verifyCredentials")
252
- .mockResolvedValueOnce(false);
253
- await expect(strategy.replacePassword("testUser", "oldPass", "newPass")).rejects.toThrow(errors_1.InvalidCredentialsError);
254
- });
255
- it("should call updatePassword with new password if old credentials are correct", async () => {
256
- config.passwordPolicy.updatePassword = jest
257
- .fn()
258
- .mockResolvedValue(undefined);
259
- jest
260
- .spyOn(strategy, "verifyCredentials")
261
- .mockResolvedValueOnce(true);
262
- await strategy.replacePassword("testUser", "oldPass", "newPass");
263
- expect(config.passwordPolicy.updatePassword).toHaveBeenCalledWith("testUser", "newPass", undefined);
264
- });
265
- });
266
- });
@@ -1,29 +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
- readonly name = "test-token";
15
- protected invalidateAccessToken(token: string, context?: MockContext): Promise<void>;
16
- protected invalidateRefreshToken(token: string, context?: MockContext): Promise<void>;
17
- constructor(config: TokenAuthStrategyConfig<MockContext, MockUser>, session?: SessionHandler, logger?: Soap.Logger);
18
- protected fetchUser(payload: any): Promise<MockUser | null>;
19
- protected extractAccessToken(context: MockContext): string | undefined;
20
- protected extractRefreshToken(context: MockContext): string | undefined;
21
- protected verifyAccessToken(token: string): Promise<any>;
22
- protected verifyRefreshToken(token: string): Promise<any>;
23
- protected generateAccessToken(user: MockUser, context: MockContext): Promise<string>;
24
- protected generateRefreshToken(user: MockUser, context: MockContext): Promise<string>;
25
- protected storeAccessToken(token: string): Promise<void>;
26
- protected storeRefreshToken(token: string): Promise<void>;
27
- protected embedAccessToken(token: string, context: MockContext): void;
28
- protected embedRefreshToken(token: string, context: MockContext): void;
29
- }
@@ -1,299 +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
- name = "test-token";
8
- async invalidateAccessToken(token, context) {
9
- context.accessToken = undefined;
10
- }
11
- async invalidateRefreshToken(token, context) {
12
- context.refreshToken = undefined;
13
- }
14
- constructor(config, session, logger) {
15
- super(config, session, logger);
16
- }
17
- async fetchUser(payload) {
18
- return { id: "testUserId", name: "Test User" };
19
- }
20
- extractAccessToken(context) {
21
- return context.accessToken;
22
- }
23
- extractRefreshToken(context) {
24
- return context.refreshToken;
25
- }
26
- async verifyAccessToken(token) {
27
- return { userId: "testUserId" };
28
- }
29
- async verifyRefreshToken(token) {
30
- return { userId: "testUserId" };
31
- }
32
- async generateAccessToken(user, context) {
33
- return "newAccessToken";
34
- }
35
- async generateRefreshToken(user, context) {
36
- return "newRefreshToken";
37
- }
38
- async storeAccessToken(token) {
39
- }
40
- async storeRefreshToken(token) {
41
- }
42
- embedAccessToken(token, context) {
43
- context.accessToken = token;
44
- }
45
- embedRefreshToken(token, context) {
46
- context.refreshToken = token;
47
- }
48
- }
49
- exports.TestTokenAuthStrategy = TestTokenAuthStrategy;
50
- const jsonwebtoken_1 = require("jsonwebtoken");
51
- describe("TokenAuthStrategy", () => {
52
- let config;
53
- let strategy;
54
- let mockSession;
55
- let mockLogger;
56
- let mockRateLimitService;
57
- let mockRoleService;
58
- let mockThrottleService;
59
- let mockAccountLockService;
60
- let mockMfaService;
61
- let context;
62
- beforeEach(() => {
63
- config = {
64
- refreshToken: {
65
- enabled: true,
66
- },
67
- };
68
- mockSession = {
69
- logoutSession: jest.fn().mockResolvedValue(undefined),
70
- };
71
- mockLogger = {
72
- warn: jest.fn(),
73
- error: jest.fn(),
74
- info: jest.fn(),
75
- };
76
- mockRateLimitService = {
77
- checkRateLimit: jest.fn().mockResolvedValue(undefined),
78
- };
79
- mockRoleService = {
80
- isAuthorized: jest.fn().mockResolvedValue(true),
81
- };
82
- mockThrottleService = {};
83
- mockAccountLockService = {};
84
- mockMfaService = {};
85
- context = {
86
- accessToken: undefined,
87
- refreshToken: undefined,
88
- };
89
- strategy = new TestTokenAuthStrategy(config, mockSession, mockLogger);
90
- strategy.rateLimit = mockRateLimitService;
91
- strategy.role = mockRoleService;
92
- strategy.throttle = mockThrottleService;
93
- strategy.accountLock = mockAccountLockService;
94
- strategy.mfa = mockMfaService;
95
- });
96
- describe("authenticate", () => {
97
- it("should throw MissingTokenError if no access token and refreshToken is disabled in config", async () => {
98
- config.refreshToken = undefined;
99
- await expect(strategy.authenticate(context)).rejects.toThrowError(errors_1.MissingTokenError);
100
- expect(mockRateLimitService.checkRateLimit).toHaveBeenCalledWith(context);
101
- });
102
- it("should verify access token if present and return user + tokens if valid", async () => {
103
- context.accessToken = "validAccessToken";
104
- const result = await strategy.authenticate(context);
105
- expect(result.user).toEqual({ id: "testUserId", name: "Test User" });
106
- expect(result.tokens).toEqual({ accessToken: "validAccessToken" });
107
- expect(mockRoleService.isAuthorized).toHaveBeenCalledWith({
108
- id: "testUserId",
109
- name: "Test User",
110
- });
111
- expect(mockLogger.warn).not.toHaveBeenCalled();
112
- });
113
- it("should fallback to refresh token if access token is invalid or expired", async () => {
114
- context.accessToken = "invalidAccessToken";
115
- context.refreshToken = "validRefreshToken";
116
- jest
117
- .spyOn(strategy, "verifyAccessToken")
118
- .mockRejectedValue(new jsonwebtoken_1.TokenExpiredError("expired", new Date()));
119
- const refreshSpy = jest
120
- .spyOn(strategy, "refreshTokens")
121
- .mockResolvedValue({
122
- user: { id: "testUserId", name: "Refreshed User" },
123
- tokens: {
124
- accessToken: "refreshedAccess",
125
- refreshToken: "refreshedRefresh",
126
- },
127
- });
128
- const result = await strategy.authenticate(context);
129
- expect(mockLogger.warn).toHaveBeenCalledWith("Access token expired, attempting refresh...");
130
- expect(refreshSpy).toHaveBeenCalledWith(context);
131
- expect(result).toEqual({
132
- user: { id: "testUserId", name: "Refreshed User" },
133
- tokens: {
134
- accessToken: "refreshedAccess",
135
- refreshToken: "refreshedRefresh",
136
- },
137
- });
138
- });
139
- it("should throw MissingTokenError if no access token and no refresh token provided", async () => {
140
- context.accessToken = undefined;
141
- context.refreshToken = undefined;
142
- await expect(strategy.authenticate(context)).rejects.toThrow(errors_1.MissingTokenError);
143
- });
144
- });
145
- describe("refreshTokens", () => {
146
- it("should throw an error if refresh tokens are not enabled in config", async () => {
147
- config.refreshToken = undefined;
148
- await expect(strategy.refreshTokens(context)).rejects.toThrowError("Refresh tokens are not enabled.");
149
- });
150
- it("should throw MissingTokenError if no refresh token is provided", async () => {
151
- config.refreshToken = {};
152
- await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.MissingTokenError);
153
- });
154
- it("should throw InvalidTokenError if refresh token is invalid", async () => {
155
- context.refreshToken = "invalidRefreshToken";
156
- jest
157
- .spyOn(strategy, "verifyRefreshToken")
158
- .mockRejectedValue(new errors_1.InvalidTokenError("Refresh"));
159
- await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.InvalidTokenError);
160
- });
161
- it("should throw UserNotFoundError if user retrieval returns null", async () => {
162
- context.refreshToken = "validRefresh";
163
- jest.spyOn(strategy, "fetchUser").mockResolvedValueOnce(null);
164
- await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.UserNotFoundError);
165
- });
166
- it("should return new access token (and refresh token) on success", async () => {
167
- context.refreshToken = "validRefresh";
168
- jest
169
- .spyOn(strategy, "verifyRefreshToken")
170
- .mockResolvedValueOnce({ userId: "testUserId" });
171
- jest
172
- .spyOn(strategy, "generateAccessToken")
173
- .mockResolvedValueOnce("newAccessToken");
174
- jest
175
- .spyOn(strategy, "generateRefreshToken")
176
- .mockResolvedValueOnce("newRefreshToken");
177
- const storeAccessSpy = jest.spyOn(strategy, "storeAccessToken");
178
- const storeRefreshSpy = jest.spyOn(strategy, "storeRefreshToken");
179
- const result = await strategy.refreshTokens(context);
180
- expect(storeAccessSpy).toHaveBeenCalledWith("newAccessToken");
181
- expect(storeRefreshSpy).toHaveBeenCalledWith("newRefreshToken");
182
- expect(result).toEqual({
183
- user: { id: "testUserId", name: "Test User" },
184
- tokens: {
185
- accessToken: "newAccessToken",
186
- refreshToken: "newRefreshToken",
187
- },
188
- });
189
- });
190
- describe("absoluteExpiry", () => {
191
- it("should log a warning and throw InvalidTokenError if absolute expiry is exceeded (onExpiry=error)", async () => {
192
- config.refreshToken = {
193
- absoluteExpiry: {
194
- payloadField: "absoluteExp",
195
- onExpiry: "error",
196
- },
197
- };
198
- context.refreshToken = "validRefresh";
199
- jest
200
- .spyOn(strategy, "verifyRefreshToken")
201
- .mockResolvedValueOnce({
202
- userId: "testUserId",
203
- absoluteExp: Math.floor(Date.now() / 1000) - 1000,
204
- });
205
- await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.InvalidTokenError);
206
- expect(mockLogger.warn).toHaveBeenCalledWith("Absolute expiry exceeded for refresh token.");
207
- });
208
- it("should call logoutSession and throw if onExpiry=logout", async () => {
209
- config.refreshToken = {
210
- absoluteExpiry: {
211
- payloadField: "absoluteExp",
212
- onExpiry: "logout",
213
- },
214
- };
215
- context.refreshToken = "validRefresh";
216
- jest
217
- .spyOn(strategy, "verifyRefreshToken")
218
- .mockResolvedValueOnce({
219
- userId: "testUserId",
220
- absoluteExp: Math.floor(Date.now() / 1000) - 1000,
221
- });
222
- await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.InvalidTokenError);
223
- expect(mockSession.logoutSession).toHaveBeenCalledWith(context);
224
- });
225
- });
226
- describe("rotation", () => {
227
- beforeEach(() => {
228
- config.refreshToken = {
229
- enabled: true,
230
- rotation: {
231
- maxRotations: 3,
232
- getRotationCount: jest.fn().mockResolvedValue(1),
233
- isLimitReached: jest
234
- .fn()
235
- .mockImplementation((count, max) => count >= max),
236
- rotateToken: jest.fn().mockResolvedValue({
237
- newToken: "rotatedRefreshToken",
238
- }),
239
- afterRotation: jest.fn(),
240
- },
241
- };
242
- context.refreshToken = "someOldRefreshToken";
243
- });
244
- it("should throw TokenRotationLimitReachedError if limit is reached", async () => {
245
- config.refreshToken.rotation.getRotationCount.mockResolvedValueOnce(3);
246
- await expect(strategy.refreshTokens(context)).rejects.toThrow(errors_1.TokenRotationLimitReachedError);
247
- });
248
- it("should rotate the refresh token and call afterRotation if limit not reached", async () => {
249
- config.refreshToken.rotation.getRotationCount.mockResolvedValueOnce(2);
250
- jest
251
- .spyOn(strategy, "verifyRefreshToken")
252
- .mockResolvedValueOnce({ userId: "testUserId" });
253
- jest
254
- .spyOn(strategy, "generateAccessToken")
255
- .mockResolvedValueOnce("newAccessToken");
256
- const afterRotationSpy = config.refreshToken.rotation
257
- .afterRotation;
258
- const result = await strategy.refreshTokens(context);
259
- expect(config.refreshToken.rotation.rotateToken).toHaveBeenCalledWith("someOldRefreshToken", { id: "testUserId", name: "Test User" }, context);
260
- expect(afterRotationSpy).toHaveBeenCalledWith("someOldRefreshToken", "rotatedRefreshToken", { id: "testUserId", name: "Test User" }, context, 3);
261
- expect(result).toEqual({
262
- user: { id: "testUserId", name: "Test User" },
263
- tokens: {
264
- accessToken: "newAccessToken",
265
- refreshToken: "rotatedRefreshToken",
266
- },
267
- });
268
- });
269
- it("should warn if rotation is enabled but rotateToken is not provided", async () => {
270
- config.refreshToken.rotation.rotateToken = undefined;
271
- jest
272
- .spyOn(strategy, "verifyRefreshToken")
273
- .mockResolvedValueOnce({ userId: "testUserId" });
274
- jest
275
- .spyOn(strategy, "generateAccessToken")
276
- .mockResolvedValueOnce("newAccessToken");
277
- const result = await strategy.refreshTokens(context);
278
- expect(mockLogger.warn).toHaveBeenCalledWith("Rotation enabled but rotateToken not provided.");
279
- expect(result).toEqual({
280
- user: { id: "testUserId", name: "Test User" },
281
- tokens: { accessToken: "newAccessToken" },
282
- });
283
- });
284
- });
285
- });
286
- describe("error handling", () => {
287
- it("should call onFailure", async () => {
288
- const onFailureSpy = jest.spyOn(strategy, "onFailure");
289
- jest
290
- .spyOn(strategy, "verifyAccessToken")
291
- .mockRejectedValue(new Error());
292
- context.accessToken = "someToken";
293
- await expect(strategy.authenticate(context)).rejects.toThrow(errors_1.InvalidTokenError);
294
- expect(onFailureSpy).toHaveBeenCalledWith("authenticate", {
295
- error: expect.any(Error),
296
- });
297
- });
298
- });
299
- });