@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.
- package/.claude/settings.local.json +20 -0
- package/build/errors.d.ts +1 -1
- package/build/errors.js +2 -2
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/services/auth-throttle.service.d.ts +2 -2
- package/build/services/auth-throttle.service.js +2 -2
- package/build/services/index.d.ts +1 -0
- package/build/services/index.js +1 -0
- package/build/services/password.service.d.ts +7 -5
- package/build/services/password.service.js +76 -18
- package/build/services/totp.service.d.ts +16 -0
- package/build/services/totp.service.js +96 -0
- package/build/session/session-handler.d.ts +1 -0
- package/build/session/session-handler.js +39 -6
- package/build/soap-auth.d.ts +13 -6
- package/build/soap-auth.js +132 -5
- package/build/strategies/api-key/api-key.strategy.d.ts +4 -4
- package/build/strategies/api-key/api-key.strategy.js +3 -2
- package/build/strategies/base-auth.strategy.d.ts +5 -4
- package/build/strategies/basic/basic.strategy.d.ts +2 -1
- package/build/strategies/basic/basic.strategy.js +1 -0
- package/build/strategies/credential-auth.strategy.d.ts +7 -7
- package/build/strategies/credential-auth.strategy.js +9 -14
- package/build/strategies/index.d.ts +1 -0
- package/build/strategies/index.js +1 -0
- package/build/strategies/jwt/jwt.strategy.d.ts +3 -1
- package/build/strategies/jwt/jwt.strategy.js +41 -9
- package/build/strategies/jwt/jwt.tools.js +16 -14
- package/build/strategies/local/local.strategy.d.ts +6 -3
- package/build/strategies/local/local.strategy.js +83 -2
- package/build/strategies/oauth2/hybrid.oauth2.strategy.d.ts +3 -3
- package/build/strategies/oauth2/hybrid.oauth2.strategy.js +1 -6
- package/build/strategies/oauth2/oauth2.errors.d.ts +1 -0
- package/build/strategies/oauth2/oauth2.errors.js +4 -0
- package/build/strategies/oauth2/oauth2.strategy.d.ts +6 -4
- package/build/strategies/oauth2/oauth2.strategy.js +114 -46
- package/build/strategies/oauth2/oauth2.tools.js +2 -2
- package/build/strategies/oauth2/oauth2.types.d.ts +2 -2
- package/build/strategies/oauth2/providers/facebook.strategy.d.ts +11 -0
- package/build/strategies/oauth2/providers/facebook.strategy.js +58 -0
- package/build/strategies/oauth2/providers/github.strategy.d.ts +11 -0
- package/build/strategies/oauth2/providers/github.strategy.js +56 -0
- package/build/strategies/oauth2/providers/google.strategy.d.ts +11 -0
- package/build/strategies/oauth2/providers/google.strategy.js +52 -0
- package/build/strategies/oauth2/providers/http-oauth2.strategy.d.ts +16 -0
- package/build/strategies/oauth2/providers/http-oauth2.strategy.js +49 -0
- package/build/strategies/oauth2/providers/index.d.ts +5 -0
- package/build/strategies/oauth2/providers/index.js +21 -0
- package/build/strategies/oauth2/providers/provider.types.d.ts +7 -0
- package/build/strategies/oauth2/providers/provider.types.js +2 -0
- package/build/strategies/token-auth.strategy.d.ts +4 -4
- package/build/strategies/token-auth.strategy.js +2 -3
- package/build/tools/tools.js +1 -2
- package/build/types.d.ts +31 -32
- package/build/utils/validation.d.ts +23 -0
- package/build/utils/validation.js +139 -0
- package/package.json +8 -7
- package/build/__tests__/soap-auth.test.d.ts +0 -1
- package/build/__tests__/soap-auth.test.js +0 -42
- package/build/services/__tests__/account-lock.service.test.d.ts +0 -1
- package/build/services/__tests__/account-lock.service.test.js +0 -55
- package/build/services/__tests__/auth-throttle.service.test.d.ts +0 -1
- package/build/services/__tests__/auth-throttle.service.test.js +0 -48
- package/build/services/__tests__/jwks.service.test.d.ts +0 -1
- package/build/services/__tests__/jwks.service.test.js +0 -39
- package/build/services/__tests__/mfa.service.test.d.ts +0 -1
- package/build/services/__tests__/mfa.service.test.js +0 -66
- package/build/services/__tests__/password.service.test.d.ts +0 -1
- package/build/services/__tests__/password.service.test.js +0 -66
- package/build/services/__tests__/pkce.service.test.d.ts +0 -1
- package/build/services/__tests__/pkce.service.test.js +0 -77
- package/build/services/__tests__/rate-limit.service.test.d.ts +0 -1
- package/build/services/__tests__/rate-limit.service.test.js +0 -37
- package/build/services/__tests__/role.service.test.d.ts +0 -1
- package/build/services/__tests__/role.service.test.js +0 -31
- package/build/session/__tests__/file.session-store.test.d.ts +0 -1
- package/build/session/__tests__/file.session-store.test.js +0 -117
- package/build/session/__tests__/memory.session-store.test.d.ts +0 -1
- package/build/session/__tests__/memory.session-store.test.js +0 -77
- package/build/session/__tests__/session-handler.test.d.ts +0 -1
- package/build/session/__tests__/session-handler.test.js +0 -337
- package/build/strategies/__tests__/base-auth.strategy.test.d.ts +0 -14
- package/build/strategies/__tests__/base-auth.strategy.test.js +0 -137
- package/build/strategies/__tests__/credential-auth.strategy.test.d.ts +0 -14
- package/build/strategies/__tests__/credential-auth.strategy.test.js +0 -265
- package/build/strategies/__tests__/token-auth.strategy.test.d.ts +0 -28
- package/build/strategies/__tests__/token-auth.strategy.test.js +0 -298
- package/build/strategies/api-key/__tests__/api-key.strategy.test.d.ts +0 -1
- package/build/strategies/api-key/__tests__/api-key.strategy.test.js +0 -103
- package/build/strategies/basic/__tests__/basic.strategy.test.d.ts +0 -1
- package/build/strategies/basic/__tests__/basic.strategy.test.js +0 -104
- package/build/strategies/jwt/__tests__/jwt.strategy.test.d.ts +0 -1
- package/build/strategies/jwt/__tests__/jwt.strategy.test.js +0 -156
- package/build/strategies/jwt/__tests__/jwt.tools.test.d.ts +0 -1
- package/build/strategies/jwt/__tests__/jwt.tools.test.js +0 -98
- package/build/strategies/local/__tests__/local.strategy.test.d.ts +0 -1
- 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
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|