@soapjs/soap-auth 0.3.1 → 0.3.2
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/build/__tests__/soap-auth.test.d.ts +1 -0
- package/build/__tests__/soap-auth.test.js +42 -0
- package/build/errors.d.ts +14 -3
- package/build/errors.js +29 -8
- package/build/index.d.ts +1 -1
- package/build/index.js +1 -1
- package/build/services/__tests__/account-lock.service.test.d.ts +1 -0
- package/build/services/__tests__/account-lock.service.test.js +55 -0
- package/build/services/__tests__/auth-throttle.service.test.d.ts +1 -0
- package/build/services/__tests__/auth-throttle.service.test.js +48 -0
- package/build/services/__tests__/jwks.service.test.d.ts +1 -0
- package/build/services/__tests__/jwks.service.test.js +39 -0
- package/build/services/__tests__/mfa.service.test.d.ts +1 -0
- package/build/services/__tests__/mfa.service.test.js +66 -0
- package/build/services/__tests__/password.service.test.d.ts +1 -0
- package/build/services/__tests__/password.service.test.js +66 -0
- package/build/services/__tests__/pkce.service.test.d.ts +1 -0
- package/build/services/__tests__/pkce.service.test.js +77 -0
- package/build/services/__tests__/rate-limit.service.test.d.ts +1 -0
- package/build/services/__tests__/rate-limit.service.test.js +37 -0
- package/build/services/__tests__/role.service.test.d.ts +1 -0
- package/build/services/__tests__/role.service.test.js +31 -0
- package/build/services/account-lock.service.d.ts +12 -0
- package/build/services/account-lock.service.js +39 -0
- package/build/services/auth-throttle.service.d.ts +10 -0
- package/build/services/auth-throttle.service.js +43 -0
- package/build/services/index.d.ts +8 -0
- package/build/{factories → services}/index.js +8 -3
- package/build/services/jwks.service.d.ts +7 -0
- package/build/services/jwks.service.js +41 -0
- package/build/services/mfa.service.d.ts +12 -0
- package/build/services/mfa.service.js +74 -0
- package/build/services/password.service.d.ts +14 -0
- package/build/services/password.service.js +78 -0
- package/build/services/pkce.service.d.ts +14 -0
- package/build/services/pkce.service.js +81 -0
- package/build/services/rate-limit.service.d.ts +9 -0
- package/build/services/rate-limit.service.js +26 -0
- package/build/services/role.service.d.ts +9 -0
- package/build/services/role.service.js +26 -0
- package/build/session/__tests__/file.session-store.test.d.ts +1 -0
- package/build/session/__tests__/file.session-store.test.js +117 -0
- package/build/session/__tests__/memory.session-store.test.d.ts +1 -0
- package/build/session/__tests__/memory.session-store.test.js +77 -0
- package/build/session/__tests__/session-handler.test.d.ts +1 -0
- package/build/session/__tests__/session-handler.test.js +337 -0
- package/build/session/file.session-store.d.ts +1 -0
- package/build/session/file.session-store.js +7 -0
- package/build/session/memory.session-store.d.ts +4 -1
- package/build/session/memory.session-store.js +11 -5
- package/build/session/session-handler.d.ts +12 -7
- package/build/session/session-handler.js +46 -13
- package/build/session/session.errors.d.ts +6 -0
- package/build/session/session.errors.js +15 -0
- package/build/soap-auth.d.ts +9 -8
- package/build/soap-auth.js +42 -29
- package/build/strategies/__tests__/base-auth.strategy.test.d.ts +14 -0
- package/build/strategies/__tests__/base-auth.strategy.test.js +137 -0
- package/build/strategies/__tests__/credential-auth.strategy.test.d.ts +14 -0
- package/build/strategies/__tests__/credential-auth.strategy.test.js +265 -0
- package/build/strategies/__tests__/token-auth.strategy.test.d.ts +28 -0
- package/build/strategies/__tests__/token-auth.strategy.test.js +298 -0
- package/build/strategies/api-key/__tests__/api-key.strategy.test.d.ts +1 -0
- package/build/strategies/api-key/__tests__/api-key.strategy.test.js +103 -0
- package/build/strategies/api-key/api-key.strategy.d.ts +5 -2
- package/build/strategies/api-key/api-key.strategy.js +43 -35
- package/build/strategies/api-key/api-key.tools.d.ts +2 -0
- package/build/strategies/api-key/api-key.tools.js +39 -0
- package/build/strategies/api-key/api-key.types.d.ts +10 -2
- package/build/strategies/base-auth.strategy.d.ts +11 -5
- package/build/strategies/base-auth.strategy.js +45 -52
- package/build/strategies/basic/__tests__/basic.strategy.test.d.ts +1 -0
- package/build/strategies/basic/__tests__/basic.strategy.test.js +104 -0
- package/build/strategies/basic/basic.strategy.d.ts +5 -7
- package/build/strategies/basic/basic.strategy.js +6 -6
- package/build/strategies/basic/basic.tools.d.ts +2 -0
- package/build/strategies/basic/basic.tools.js +44 -0
- package/build/strategies/credential-auth.strategy.d.ts +7 -17
- package/build/strategies/credential-auth.strategy.js +116 -181
- package/build/strategies/jwt/__tests__/jwt.strategy.test.d.ts +1 -0
- package/build/strategies/jwt/__tests__/jwt.strategy.test.js +156 -0
- package/build/strategies/jwt/__tests__/jwt.tools.test.d.ts +1 -0
- package/build/strategies/jwt/__tests__/jwt.tools.test.js +98 -0
- package/build/strategies/jwt/jwt.strategy.d.ts +13 -14
- package/build/strategies/jwt/jwt.strategy.js +57 -44
- package/build/strategies/jwt/jwt.tools.d.ts +20 -7
- package/build/strategies/jwt/jwt.tools.js +180 -81
- package/build/strategies/local/__tests__/local.strategy.test.d.ts +1 -0
- package/build/strategies/local/__tests__/local.strategy.test.js +115 -0
- package/build/strategies/local/local.strategy.d.ts +4 -3
- package/build/strategies/local/local.strategy.js +7 -6
- package/build/strategies/local/local.tools.d.ts +2 -0
- package/build/strategies/local/local.tools.js +44 -0
- package/build/strategies/oauth2/hybrid.oauth2.strategy.d.ts +5 -0
- package/build/strategies/oauth2/hybrid.oauth2.strategy.js +92 -0
- package/build/strategies/oauth2/oauth2.errors.d.ts +12 -0
- package/build/strategies/oauth2/oauth2.errors.js +24 -0
- package/build/strategies/oauth2/oauth2.strategy.d.ts +25 -15
- package/build/strategies/oauth2/oauth2.strategy.js +131 -141
- package/build/strategies/oauth2/oauth2.tools.d.ts +7 -2
- package/build/strategies/oauth2/oauth2.tools.js +119 -14
- package/build/strategies/oauth2/oauth2.types.d.ts +32 -1
- package/build/strategies/token-auth.strategy.d.ts +14 -8
- package/build/strategies/token-auth.strategy.js +162 -38
- package/build/tools/index.d.ts +0 -2
- package/build/tools/index.js +0 -2
- package/build/tools/tools.d.ts +2 -1
- package/build/tools/tools.js +9 -12
- package/build/types.d.ts +88 -57
- package/package.json +1 -1
- package/build/factories/auth-strategy.factory.d.ts +0 -9
- package/build/factories/auth-strategy.factory.js +0 -16
- package/build/factories/http-auth-strategy.factory.d.ts +0 -5
- package/build/factories/http-auth-strategy.factory.js +0 -41
- package/build/factories/index.d.ts +0 -3
- package/build/factories/socket-auth-strategy.factory.d.ts +0 -5
- package/build/factories/socket-auth-strategy.factory.js +0 -27
- package/build/tools/session.tools.d.ts +0 -6
- package/build/tools/session.tools.js +0 -15
- package/build/tools/token.tools.d.ts +0 -7
- package/build/tools/token.tools.js +0 -32
|
@@ -1,254 +1,189 @@
|
|
|
1
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
|
+
};
|
|
2
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
26
|
exports.CredentialAuthStrategy = void 0;
|
|
27
|
+
const Soap = __importStar(require("@soapjs/soap"));
|
|
4
28
|
const errors_1 = require("../errors");
|
|
5
29
|
const base_auth_strategy_1 = require("./base-auth.strategy");
|
|
30
|
+
const password_service_1 = require("../services/password.service");
|
|
6
31
|
class CredentialAuthStrategy extends base_auth_strategy_1.BaseAuthStrategy {
|
|
7
32
|
config;
|
|
8
33
|
session;
|
|
34
|
+
jwt;
|
|
9
35
|
logger;
|
|
10
|
-
|
|
36
|
+
password;
|
|
37
|
+
constructor(config, session, jwt, logger) {
|
|
11
38
|
super(config, session, logger);
|
|
12
39
|
this.config = config;
|
|
13
40
|
this.session = session;
|
|
41
|
+
this.jwt = jwt;
|
|
14
42
|
this.logger = logger;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!this.session || !this.config.session) {
|
|
18
|
-
this.logger?.info("Session management is not configured. Skipping session storage.");
|
|
19
|
-
return;
|
|
43
|
+
if (config.passwordPolicy) {
|
|
44
|
+
this.password = new password_service_1.PasswordService(config.passwordPolicy, logger);
|
|
20
45
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
: `sid-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
46
|
+
}
|
|
47
|
+
async fetchUser(payload) {
|
|
48
|
+
if (this.config?.user?.fetchUser) {
|
|
49
|
+
return this.config.user.fetchUser(payload);
|
|
26
50
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
await this.
|
|
51
|
+
throw new Soap.NotImplementedError("fetchUser");
|
|
52
|
+
}
|
|
53
|
+
async authenticate(context) {
|
|
54
|
+
try {
|
|
55
|
+
await this.rateLimit?.checkRateLimit(context);
|
|
56
|
+
if (this.jwt) {
|
|
57
|
+
try {
|
|
58
|
+
return await this.jwt.authenticate(context);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
this.logger?.warn("JWT authentication failed, falling back to session.");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (this.session) {
|
|
65
|
+
return await this.authenticateWithSession(context);
|
|
66
|
+
}
|
|
67
|
+
this.logger?.warn("No authentication method found. Proceeding as guest.");
|
|
68
|
+
if (this.config.allowGuest) {
|
|
69
|
+
return { user: null };
|
|
70
|
+
}
|
|
71
|
+
throw new errors_1.UserNotFoundError();
|
|
32
72
|
}
|
|
33
|
-
|
|
34
|
-
await this.
|
|
73
|
+
catch (error) {
|
|
74
|
+
await this.onFailure("authenticate", {
|
|
75
|
+
context,
|
|
76
|
+
error,
|
|
77
|
+
});
|
|
78
|
+
throw error;
|
|
35
79
|
}
|
|
36
|
-
this.config.session.embedSessionId?.(context, sessionId);
|
|
37
|
-
this.logger?.info(`Stored user session with ID: ${sessionId}`);
|
|
38
|
-
}
|
|
39
|
-
async handleAuthenticationError(error, context) {
|
|
40
|
-
this.logger?.error("Authentication failed:", error);
|
|
41
|
-
await this.onFailure("login", { context, error });
|
|
42
|
-
throw new errors_1.AuthError(error, "Authentication failed.");
|
|
43
|
-
}
|
|
44
|
-
async preAuthChecks(identifier) {
|
|
45
|
-
await this.isAccountLocked(identifier);
|
|
46
|
-
await this.checkFailedAttempts(identifier);
|
|
47
|
-
await this.checkRateLimit(identifier);
|
|
48
|
-
await this.checkPasswordExpiry(identifier);
|
|
49
80
|
}
|
|
50
|
-
async
|
|
51
|
-
await this.config.failedAttempts.incrementFailedAttempts?.(identifier);
|
|
52
|
-
}
|
|
53
|
-
async handleSuccessfulLogin(identifier) {
|
|
54
|
-
await this.config.failedAttempts.resetFailedAttempts?.(identifier);
|
|
55
|
-
}
|
|
56
|
-
async finalizeAuthentication(user, context) {
|
|
57
|
-
await this.checkMfa(user, context);
|
|
58
|
-
await this.isAuthorized(user);
|
|
59
|
-
await this.storeUserSession(user, context);
|
|
60
|
-
}
|
|
61
|
-
async authenticate(context) {
|
|
81
|
+
async login(context) {
|
|
62
82
|
try {
|
|
83
|
+
await this.rateLimit?.checkRateLimit(context);
|
|
63
84
|
const credentials = await this.extractCredentials(context);
|
|
64
|
-
if (!credentials)
|
|
85
|
+
if (!credentials) {
|
|
65
86
|
throw new errors_1.MissingCredentialsError();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
}
|
|
88
|
+
await this.accountLock?.isAccountLocked(credentials.identifier);
|
|
89
|
+
await this.throttle?.checkFailedAttempts(credentials.identifier);
|
|
90
|
+
if ((await this.verifyCredentials(credentials.identifier, credentials.password)) === false) {
|
|
91
|
+
await this.throttle?.incrementFailedAttempts(credentials.identifier);
|
|
70
92
|
throw new errors_1.InvalidCredentialsError();
|
|
71
93
|
}
|
|
72
|
-
await this.
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
const isPasswordChangeRequired = await this.password?.isPasswordChangeRequired(credentials.identifier);
|
|
95
|
+
if (isPasswordChangeRequired) {
|
|
96
|
+
throw new errors_1.ExpiredPasswordError();
|
|
97
|
+
}
|
|
98
|
+
await this.throttle?.resetFailedAttempts(credentials.identifier);
|
|
99
|
+
const user = await this.fetchUser(credentials.identifier);
|
|
100
|
+
if (!user) {
|
|
75
101
|
throw new errors_1.UserNotFoundError();
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
}
|
|
103
|
+
await this.mfa?.checkMfa(user, context);
|
|
104
|
+
await this.role?.isAuthorized(user);
|
|
105
|
+
const tokens = await this.jwt?.issueTokens(user, context);
|
|
106
|
+
const session = await this.session?.issueSession(user, context);
|
|
107
|
+
return { user, session, tokens };
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
await this.onFailure("login", {
|
|
111
|
+
context,
|
|
112
|
+
error,
|
|
113
|
+
});
|
|
114
|
+
throw error;
|
|
89
115
|
}
|
|
90
116
|
}
|
|
91
117
|
async logout(context) {
|
|
92
118
|
try {
|
|
93
|
-
|
|
94
|
-
const sessionId = this.session.getSessionId?.(context);
|
|
95
|
-
if (!sessionId)
|
|
96
|
-
throw new Error("Session ID is missing in the context.");
|
|
97
|
-
await this.session.destroy(sessionId);
|
|
98
|
-
this.logger?.info(`Session destroyed: ${sessionId}`);
|
|
99
|
-
}
|
|
119
|
+
await this.session?.logoutSession(context);
|
|
100
120
|
await this.onSuccess("logout", context);
|
|
101
121
|
}
|
|
102
|
-
catch (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
122
|
+
catch (error) {
|
|
123
|
+
await this.onFailure("logout", {
|
|
124
|
+
error,
|
|
125
|
+
});
|
|
106
126
|
throw error;
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
async requestPasswordReset(identifier, email) {
|
|
110
130
|
try {
|
|
111
|
-
|
|
112
|
-
throw new Error("Password reset token generation is not configured.");
|
|
113
|
-
}
|
|
114
|
-
const token = await this.config.passwordPolicy.generateResetToken(identifier);
|
|
131
|
+
const token = await this.password.generateResetToken(identifier);
|
|
115
132
|
if (email) {
|
|
116
|
-
await this.
|
|
133
|
+
await this.password.sendResetEmail(email, token);
|
|
117
134
|
}
|
|
118
|
-
await this.onSuccess("request_password_reset", {
|
|
135
|
+
await this.onSuccess("request_password_reset", {
|
|
136
|
+
identifier,
|
|
137
|
+
tokens: { reset: token },
|
|
138
|
+
});
|
|
119
139
|
this.logger.info(`Password reset requested for identifier: ${identifier}`);
|
|
120
140
|
}
|
|
121
|
-
catch (
|
|
122
|
-
const error = new errors_1.AuthError(e, "Password reset request error.");
|
|
123
|
-
this.logger.error("Password reset request error:", e);
|
|
141
|
+
catch (error) {
|
|
124
142
|
await this.onFailure("request_password_reset", {
|
|
125
143
|
identifier,
|
|
126
|
-
|
|
144
|
+
email,
|
|
145
|
+
error,
|
|
127
146
|
});
|
|
128
147
|
throw error;
|
|
129
148
|
}
|
|
130
149
|
}
|
|
131
150
|
async resetPassword(identifier, token, newPassword) {
|
|
132
151
|
try {
|
|
133
|
-
if (!this.
|
|
134
|
-
throw new
|
|
152
|
+
if (!this.password?.validateResetToken) {
|
|
153
|
+
throw new Soap.NotImplementedError("validateResetToken");
|
|
135
154
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
throw new Error("Invalid or expired reset token.");
|
|
155
|
+
if ((await this.password.validateResetToken(token)) === false) {
|
|
156
|
+
throw new errors_1.ExpiredResetTokenError();
|
|
139
157
|
}
|
|
140
|
-
await this.
|
|
158
|
+
await this.password.updatePassword(identifier, newPassword);
|
|
141
159
|
await this.onSuccess("password_reset", { identifier });
|
|
142
160
|
this.logger.info(`Password successfully reset for identifier: ${identifier}`);
|
|
143
|
-
this.auditPasswordChange(identifier);
|
|
144
161
|
}
|
|
145
|
-
catch (
|
|
146
|
-
const error = new errors_1.AuthError(e, "Password reset error");
|
|
147
|
-
this.logger.error("Password reset error:", e);
|
|
162
|
+
catch (error) {
|
|
148
163
|
await this.onFailure("password_reset", {
|
|
149
164
|
identifier,
|
|
150
|
-
|
|
165
|
+
additional: { token },
|
|
166
|
+
error,
|
|
151
167
|
});
|
|
152
168
|
throw error;
|
|
153
169
|
}
|
|
154
170
|
}
|
|
155
171
|
async changePassword(identifier, oldPassword, newPassword) {
|
|
156
172
|
try {
|
|
157
|
-
if (!this.
|
|
158
|
-
throw new Error("Credential verification is not configured.");
|
|
159
|
-
}
|
|
160
|
-
const isAuthenticated = await this.config.credentials.verifyCredentials(identifier, oldPassword);
|
|
161
|
-
if (!isAuthenticated) {
|
|
173
|
+
if (!(await this.verifyCredentials(identifier, oldPassword))) {
|
|
162
174
|
throw new errors_1.InvalidCredentialsError();
|
|
163
175
|
}
|
|
164
|
-
await this.
|
|
176
|
+
await this.password.updatePassword(identifier, newPassword);
|
|
165
177
|
await this.onSuccess("change_password", { identifier });
|
|
166
178
|
this.logger.info(`Password changed successfully for identifier: ${identifier}`);
|
|
167
|
-
this.auditPasswordChange(identifier);
|
|
168
179
|
}
|
|
169
|
-
catch (
|
|
170
|
-
const error = new errors_1.AuthError(e, "Change password error");
|
|
171
|
-
this.logger.error("Change password error:", e);
|
|
180
|
+
catch (error) {
|
|
172
181
|
await this.onFailure("change_password", {
|
|
173
182
|
identifier,
|
|
174
|
-
error
|
|
183
|
+
error,
|
|
175
184
|
});
|
|
176
185
|
throw error;
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
|
-
async auditLoginAttempt(identifier, success, context) {
|
|
180
|
-
await this.config.audit?.logAttempt?.(identifier, success, context);
|
|
181
|
-
}
|
|
182
|
-
async auditPasswordChange(identifier, context) {
|
|
183
|
-
await this.config.audit?.logPasswordChange?.(identifier, context);
|
|
184
|
-
}
|
|
185
|
-
validatePasswordPolicy(password) {
|
|
186
|
-
return this.config.passwordPolicy.validatePassword?.(password) ?? true;
|
|
187
|
-
}
|
|
188
|
-
async checkFailedAttempts(identifier) {
|
|
189
|
-
try {
|
|
190
|
-
if (this.config.security?.maxFailedLoginAttempts) {
|
|
191
|
-
const failedAttempts = (await this.config.failedAttempts.getFailedAttempts?.(identifier)) ||
|
|
192
|
-
0;
|
|
193
|
-
if (failedAttempts >= this.config.security.maxFailedLoginAttempts) {
|
|
194
|
-
this.logger.warn(`User ${identifier} is temporarily locked out.`);
|
|
195
|
-
throw new errors_1.AccountLockedError();
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
catch (e) {
|
|
200
|
-
this.logger.error("Check failed attempts:", e);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async isAccountLocked(account) {
|
|
204
|
-
if (await this.config.lock.isAccountLocked?.(account)) {
|
|
205
|
-
throw new errors_1.AccountLockedError();
|
|
206
|
-
}
|
|
207
|
-
if (typeof account === "string" && this.config.security?.lockoutDuration) {
|
|
208
|
-
const lockoutKey = `lockout:${account}`;
|
|
209
|
-
const lockoutSession = await this.session?.get(lockoutKey);
|
|
210
|
-
if (lockoutSession) {
|
|
211
|
-
const elapsed = Date.now() - Number(lockoutSession.date);
|
|
212
|
-
if (elapsed < this.config.security.lockoutDuration * 60 * 1000) {
|
|
213
|
-
this.logger.warn(`Account ${account} is temporarily locked out.`);
|
|
214
|
-
return true;
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
await this.session?.destroy(lockoutKey);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
async incrementFailedAttempts(account) {
|
|
224
|
-
if (this.config.failedAttempts.incrementFailedAttempts) {
|
|
225
|
-
await this.config.failedAttempts.incrementFailedAttempts(account);
|
|
226
|
-
const failedAttempts = (await this.config.failedAttempts.getFailedAttempts?.(account)) || 0;
|
|
227
|
-
if (this.config.security?.maxFailedLoginAttempts &&
|
|
228
|
-
failedAttempts >= this.config.security.maxFailedLoginAttempts) {
|
|
229
|
-
const lockoutKey = `lockout:${account}`;
|
|
230
|
-
await this.session?.set(lockoutKey, {
|
|
231
|
-
date: Date.now(),
|
|
232
|
-
});
|
|
233
|
-
this.logger.warn(`Account ${account} has been locked due to failed attempts.`);
|
|
234
|
-
this.notifyAccountLocked(account);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
async notifyAccountLocked(identifier) {
|
|
239
|
-
if (this.config.security?.notifyOnLockout) {
|
|
240
|
-
await this.config.security.notifyOnLockout(identifier);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
async checkPasswordExpiry(identifier) {
|
|
244
|
-
if (this.config.passwordPolicy?.passwordExpirationDays) {
|
|
245
|
-
const lastChanged = await this.config.passwordPolicy.getLastPasswordChange?.(identifier);
|
|
246
|
-
if (lastChanged &&
|
|
247
|
-
Date.now() - Number(lastChanged) >
|
|
248
|
-
this.config.passwordPolicy.passwordExpirationDays * 86400000) {
|
|
249
|
-
throw new Error("Password expired, please reset your password.");
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
188
|
}
|
|
254
189
|
exports.CredentialAuthStrategy = CredentialAuthStrategy;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
4
|
+
const jwt_strategy_1 = require("../jwt.strategy");
|
|
5
|
+
const errors_1 = require("../../../errors");
|
|
6
|
+
describe("JWTStrategy", () => {
|
|
7
|
+
let strategy;
|
|
8
|
+
let mockLogger;
|
|
9
|
+
let mockConfig;
|
|
10
|
+
let mockUser;
|
|
11
|
+
let mockContext;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
mockLogger = { error: jest.fn(), warn: jest.fn(), info: jest.fn() };
|
|
14
|
+
mockUser = { id: "123", email: "test@example.com" };
|
|
15
|
+
mockContext = {
|
|
16
|
+
req: {
|
|
17
|
+
headers: { authorization: "Bearer mock-access-token" },
|
|
18
|
+
cookies: { refreshToken: "mock-refresh-token" },
|
|
19
|
+
},
|
|
20
|
+
res: { setHeader: jest.fn(), cookie: jest.fn(), clearCookie: jest.fn() },
|
|
21
|
+
};
|
|
22
|
+
mockConfig = {
|
|
23
|
+
accessToken: {
|
|
24
|
+
issuer: { secretKey: "access-secret", options: { expiresIn: "1h" } },
|
|
25
|
+
verifier: { options: {} },
|
|
26
|
+
persistence: { store: jest.fn() },
|
|
27
|
+
extract: jest.fn(),
|
|
28
|
+
embed: jest.fn(),
|
|
29
|
+
},
|
|
30
|
+
refreshToken: {
|
|
31
|
+
issuer: { secretKey: "refresh-secret", options: { expiresIn: "7d" } },
|
|
32
|
+
verifier: { options: {} },
|
|
33
|
+
persistence: { store: jest.fn(), remove: jest.fn() },
|
|
34
|
+
extract: jest.fn(),
|
|
35
|
+
embed: jest.fn(),
|
|
36
|
+
},
|
|
37
|
+
user: { fetchUser: jest.fn().mockResolvedValue(mockUser) },
|
|
38
|
+
routes: {},
|
|
39
|
+
};
|
|
40
|
+
strategy = new jwt_strategy_1.JwtStrategy(mockConfig, mockLogger);
|
|
41
|
+
});
|
|
42
|
+
it("should authenticate user with valid access token", async () => {
|
|
43
|
+
const mockAccessToken = "mock-access-token";
|
|
44
|
+
jest
|
|
45
|
+
.spyOn(mockConfig.accessToken, "extract")
|
|
46
|
+
.mockReturnValue(mockAccessToken);
|
|
47
|
+
jest.spyOn(strategy, "verifyAccessToken").mockResolvedValue(mockUser);
|
|
48
|
+
const result = await strategy.authenticate(mockContext);
|
|
49
|
+
expect(result.user).toEqual(mockUser);
|
|
50
|
+
expect(result.tokens.accessToken).toEqual("mock-access-token");
|
|
51
|
+
});
|
|
52
|
+
it("should refresh tokens when access token is invalid", async () => {
|
|
53
|
+
const mockAccessToken = "mock-access-token";
|
|
54
|
+
const mockRefreshToken = "mock-refresh-token";
|
|
55
|
+
jest
|
|
56
|
+
.spyOn(mockConfig.accessToken, "extract")
|
|
57
|
+
.mockReturnValue(mockAccessToken);
|
|
58
|
+
jest
|
|
59
|
+
.spyOn(mockConfig.refreshToken, "extract")
|
|
60
|
+
.mockReturnValue(mockRefreshToken);
|
|
61
|
+
jest
|
|
62
|
+
.spyOn(strategy, "verifyAccessToken")
|
|
63
|
+
.mockRejectedValue(new jsonwebtoken_1.TokenExpiredError("Access", new Date()));
|
|
64
|
+
jest.spyOn(strategy, "verifyRefreshToken").mockResolvedValue(mockUser);
|
|
65
|
+
jest
|
|
66
|
+
.spyOn(strategy, "generateAccessToken")
|
|
67
|
+
.mockResolvedValue("new-access-token");
|
|
68
|
+
jest
|
|
69
|
+
.spyOn(strategy, "generateRefreshToken")
|
|
70
|
+
.mockResolvedValue("new-refresh-token");
|
|
71
|
+
jest.spyOn(strategy, "storeAccessToken").mockResolvedValue(null);
|
|
72
|
+
jest.spyOn(strategy, "storeRefreshToken").mockResolvedValue(null);
|
|
73
|
+
const result = await strategy.authenticate(mockContext);
|
|
74
|
+
expect(result.user).toEqual(mockUser);
|
|
75
|
+
expect(result.tokens.accessToken).toEqual("new-access-token");
|
|
76
|
+
expect(result.tokens.refreshToken).toEqual("new-refresh-token");
|
|
77
|
+
});
|
|
78
|
+
it("should throw MissingTokenError when no tokens are provided", async () => {
|
|
79
|
+
jest.spyOn(mockConfig.accessToken, "extract").mockReturnValue(undefined);
|
|
80
|
+
jest.spyOn(mockConfig.refreshToken, "extract").mockReturnValue(undefined);
|
|
81
|
+
await expect(strategy.authenticate(mockContext)).rejects.toThrow(errors_1.MissingTokenError);
|
|
82
|
+
});
|
|
83
|
+
it("should throw InvalidTokenError if user does not exist", async () => {
|
|
84
|
+
const mockAccessToken = "mock-access-token";
|
|
85
|
+
const mockRefreshToken = null;
|
|
86
|
+
jest
|
|
87
|
+
.spyOn(mockConfig.accessToken, "extract")
|
|
88
|
+
.mockReturnValue(mockAccessToken);
|
|
89
|
+
jest
|
|
90
|
+
.spyOn(mockConfig.refreshToken, "extract")
|
|
91
|
+
.mockReturnValue(mockRefreshToken);
|
|
92
|
+
jest.spyOn(strategy, "verifyAccessToken").mockResolvedValue(mockUser);
|
|
93
|
+
mockConfig.user.fetchUser = jest.fn().mockResolvedValue(null);
|
|
94
|
+
await expect(strategy.authenticate(mockContext)).rejects.toThrow(errors_1.InvalidTokenError);
|
|
95
|
+
});
|
|
96
|
+
it("should throw InvalidTokenError when refresh token is invalid", async () => {
|
|
97
|
+
const mockAccessToken = "mock-access-token";
|
|
98
|
+
const mockRefreshToken = "mock-refresh-token";
|
|
99
|
+
jest
|
|
100
|
+
.spyOn(mockConfig.accessToken, "extract")
|
|
101
|
+
.mockReturnValue(mockAccessToken);
|
|
102
|
+
jest
|
|
103
|
+
.spyOn(mockConfig.refreshToken, "extract")
|
|
104
|
+
.mockReturnValue(mockRefreshToken);
|
|
105
|
+
jest
|
|
106
|
+
.spyOn(strategy, "verifyAccessToken")
|
|
107
|
+
.mockRejectedValue(new errors_1.InvalidTokenError("Access"));
|
|
108
|
+
jest
|
|
109
|
+
.spyOn(strategy, "verifyRefreshToken")
|
|
110
|
+
.mockRejectedValue(new errors_1.InvalidTokenError("Refresh"));
|
|
111
|
+
await expect(strategy.authenticate(mockContext)).rejects.toThrow(errors_1.InvalidTokenError);
|
|
112
|
+
});
|
|
113
|
+
it("should generate and store access and refresh tokens", async () => {
|
|
114
|
+
jest
|
|
115
|
+
.spyOn(strategy, "generateAccessToken")
|
|
116
|
+
.mockResolvedValue("new-access-token");
|
|
117
|
+
jest
|
|
118
|
+
.spyOn(strategy, "generateRefreshToken")
|
|
119
|
+
.mockResolvedValue("new-refresh-token");
|
|
120
|
+
jest.spyOn(strategy, "storeAccessToken").mockResolvedValue(null);
|
|
121
|
+
jest.spyOn(strategy, "storeRefreshToken").mockResolvedValue(null);
|
|
122
|
+
const tokens = await strategy.issueTokens(mockUser, mockContext);
|
|
123
|
+
expect(tokens).toHaveProperty("accessToken", "new-access-token");
|
|
124
|
+
expect(tokens).toHaveProperty("refreshToken", "new-refresh-token");
|
|
125
|
+
expect(strategy.storeAccessToken).toHaveBeenCalledWith("new-access-token");
|
|
126
|
+
expect(strategy.storeRefreshToken).toHaveBeenCalledWith("new-refresh-token");
|
|
127
|
+
});
|
|
128
|
+
it("should invalidate refresh token", async () => {
|
|
129
|
+
jest
|
|
130
|
+
.spyOn(strategy, "extractRefreshToken")
|
|
131
|
+
.mockResolvedValue("mock-refresh-token");
|
|
132
|
+
jest.spyOn(strategy, "invalidateRefreshToken").mockResolvedValue(null);
|
|
133
|
+
await strategy.invalidateRefreshToken(mockContext);
|
|
134
|
+
expect(strategy.invalidateRefreshToken).toHaveBeenCalledWith(mockContext);
|
|
135
|
+
});
|
|
136
|
+
it("should extract access token from context", async () => {
|
|
137
|
+
const mockAccessToken = "mock-access-token";
|
|
138
|
+
jest
|
|
139
|
+
.spyOn(mockConfig.accessToken, "extract")
|
|
140
|
+
.mockReturnValue(mockAccessToken);
|
|
141
|
+
const token = await strategy.extractAccessToken(mockContext);
|
|
142
|
+
expect(token).toBe("mock-access-token");
|
|
143
|
+
});
|
|
144
|
+
it("should extract refresh token from context", async () => {
|
|
145
|
+
const mockRefreshToken = "mock-refresh-token";
|
|
146
|
+
jest
|
|
147
|
+
.spyOn(mockConfig.refreshToken, "extract")
|
|
148
|
+
.mockReturnValueOnce(mockRefreshToken);
|
|
149
|
+
const token = await strategy.extractRefreshToken(mockContext);
|
|
150
|
+
expect(token).toBe("mock-refresh-token");
|
|
151
|
+
});
|
|
152
|
+
it("should throw UndefinedTokenSecretError if access secret key is missing", async () => {
|
|
153
|
+
mockConfig.accessToken.issuer.secretKey = undefined;
|
|
154
|
+
expect(() => new jwt_strategy_1.JwtStrategy(mockConfig, mockLogger)).toThrow(errors_1.UndefinedTokenSecretError);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const globals_1 = require("@jest/globals");
|
|
4
|
+
const jwt_tools_1 = require("../jwt.tools");
|
|
5
|
+
const errors_1 = require("../../../errors");
|
|
6
|
+
(0, globals_1.describe)("JwtTools", () => {
|
|
7
|
+
const secretKey = "test-secret";
|
|
8
|
+
const payload = { id: "user123", email: "test@example.com" };
|
|
9
|
+
const config = {
|
|
10
|
+
accessToken: {
|
|
11
|
+
issuer: { secretKey, options: { expiresIn: "1h" } },
|
|
12
|
+
verifier: { options: {} },
|
|
13
|
+
},
|
|
14
|
+
refreshToken: {
|
|
15
|
+
issuer: { secretKey, options: { expiresIn: "7d" } },
|
|
16
|
+
verifier: { options: {} },
|
|
17
|
+
},
|
|
18
|
+
routes: {},
|
|
19
|
+
};
|
|
20
|
+
(0, globals_1.it)("should generate an access token", () => {
|
|
21
|
+
const token = jwt_tools_1.JwtTools.generateAccessToken(payload, config.accessToken);
|
|
22
|
+
(0, globals_1.expect)(typeof token).toBe("string");
|
|
23
|
+
});
|
|
24
|
+
(0, globals_1.it)("should throw error when generating access token without secret key", () => {
|
|
25
|
+
const invalidConfig = {
|
|
26
|
+
...config,
|
|
27
|
+
accessToken: { issuer: { secretKey: "" } },
|
|
28
|
+
};
|
|
29
|
+
(0, globals_1.expect)(() => jwt_tools_1.JwtTools.generateAccessToken(payload, invalidConfig.accessToken)).toThrow(errors_1.UndefinedTokenSecretError);
|
|
30
|
+
});
|
|
31
|
+
(0, globals_1.it)("should generate a refresh token", () => {
|
|
32
|
+
const token = jwt_tools_1.JwtTools.generateRefreshToken(payload, config.refreshToken);
|
|
33
|
+
(0, globals_1.expect)(typeof token).toBe("string");
|
|
34
|
+
});
|
|
35
|
+
(0, globals_1.it)("should throw error when generating refresh token without secret key", () => {
|
|
36
|
+
const invalidConfig = {
|
|
37
|
+
...config,
|
|
38
|
+
refreshToken: { issuer: { secretKey: "" } },
|
|
39
|
+
};
|
|
40
|
+
(0, globals_1.expect)(() => jwt_tools_1.JwtTools.generateRefreshToken(payload, invalidConfig.refreshToken)).toThrow(errors_1.UndefinedTokenSecretError);
|
|
41
|
+
});
|
|
42
|
+
(0, globals_1.it)("should verify a valid access token", () => {
|
|
43
|
+
const token = jwt_tools_1.JwtTools.generateAccessToken(payload, config.accessToken);
|
|
44
|
+
const decoded = jwt_tools_1.JwtTools.verifyAccessToken(token, config.accessToken);
|
|
45
|
+
(0, globals_1.expect)(decoded.id).toBe(payload.id);
|
|
46
|
+
});
|
|
47
|
+
(0, globals_1.it)("should throw error for undefined access token", () => {
|
|
48
|
+
(0, globals_1.expect)(() => jwt_tools_1.JwtTools.verifyAccessToken("", config.accessToken)).toThrow(errors_1.UndefinedTokenError);
|
|
49
|
+
});
|
|
50
|
+
(0, globals_1.it)("should throw error for invalid access token", () => {
|
|
51
|
+
(0, globals_1.expect)(() => jwt_tools_1.JwtTools.verifyAccessToken("invalid-token", config.accessToken)).toThrow(errors_1.InvalidTokenError);
|
|
52
|
+
});
|
|
53
|
+
(0, globals_1.it)("should verify a valid refresh token", () => {
|
|
54
|
+
const token = jwt_tools_1.JwtTools.generateRefreshToken(payload, config.refreshToken);
|
|
55
|
+
const decoded = jwt_tools_1.JwtTools.verifyRefreshToken(token, config.refreshToken);
|
|
56
|
+
(0, globals_1.expect)(decoded.id).toBe(payload.id);
|
|
57
|
+
});
|
|
58
|
+
(0, globals_1.it)("should throw error for undefined refresh token", () => {
|
|
59
|
+
(0, globals_1.expect)(() => jwt_tools_1.JwtTools.verifyRefreshToken("", config.refreshToken)).toThrow(errors_1.UndefinedTokenError);
|
|
60
|
+
});
|
|
61
|
+
(0, globals_1.it)("should throw error for invalid refresh token", () => {
|
|
62
|
+
(0, globals_1.expect)(() => jwt_tools_1.JwtTools.verifyRefreshToken("invalid-token", config.refreshToken)).toThrow(errors_1.InvalidTokenError);
|
|
63
|
+
});
|
|
64
|
+
(0, globals_1.it)("should set the access token in the response header", () => {
|
|
65
|
+
const token = jwt_tools_1.JwtTools.generateAccessToken(payload, config.accessToken);
|
|
66
|
+
const context = { res: { setHeader: globals_1.jest.fn() } };
|
|
67
|
+
jwt_tools_1.JwtTools.setAccessTokenHeader(token, context);
|
|
68
|
+
(0, globals_1.expect)(context.res.setHeader).toHaveBeenCalledWith("Authorization", `Bearer ${token}`);
|
|
69
|
+
});
|
|
70
|
+
(0, globals_1.it)("should set the refresh token in cookies", () => {
|
|
71
|
+
const token = jwt_tools_1.JwtTools.generateRefreshToken(payload, config.refreshToken);
|
|
72
|
+
const context = { res: { cookie: globals_1.jest.fn() } };
|
|
73
|
+
jwt_tools_1.JwtTools.setRefreshTokenCookie(token, context);
|
|
74
|
+
(0, globals_1.expect)(context.res.cookie).toHaveBeenCalledWith("refreshToken", token, globals_1.expect.any(Object));
|
|
75
|
+
});
|
|
76
|
+
(0, globals_1.it)("should clear tokens from headers and cookies", () => {
|
|
77
|
+
const context = {
|
|
78
|
+
res: { clearCookie: globals_1.jest.fn(), setHeader: globals_1.jest.fn() },
|
|
79
|
+
};
|
|
80
|
+
jwt_tools_1.JwtTools.clearTokens(context);
|
|
81
|
+
(0, globals_1.expect)(context.res.clearCookie).toHaveBeenCalledWith("refreshToken");
|
|
82
|
+
(0, globals_1.expect)(context.res.setHeader).toHaveBeenCalledWith("Authorization", "");
|
|
83
|
+
});
|
|
84
|
+
(0, globals_1.it)("should retrieve an access token from request headers", () => {
|
|
85
|
+
const token = jwt_tools_1.JwtTools.generateAccessToken(payload, config.accessToken);
|
|
86
|
+
const context = {
|
|
87
|
+
req: { headers: { authorization: `Bearer ${token}` } },
|
|
88
|
+
};
|
|
89
|
+
const extractedToken = jwt_tools_1.JwtTools.getAccessToken(context);
|
|
90
|
+
(0, globals_1.expect)(extractedToken).toBe(token);
|
|
91
|
+
});
|
|
92
|
+
(0, globals_1.it)("should retrieve a refresh token from request cookies", () => {
|
|
93
|
+
const token = jwt_tools_1.JwtTools.generateRefreshToken(payload, config.refreshToken);
|
|
94
|
+
const context = { req: { cookies: { refreshToken: token } } };
|
|
95
|
+
const extractedToken = jwt_tools_1.JwtTools.getRefreshToken(context);
|
|
96
|
+
(0, globals_1.expect)(extractedToken).toBe(token);
|
|
97
|
+
});
|
|
98
|
+
});
|