@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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { AuthThrottleConfig } from "../types";
|
|
3
|
+
export declare class AuthThrottleService {
|
|
4
|
+
private config;
|
|
5
|
+
private logger;
|
|
6
|
+
constructor(config: AuthThrottleConfig, logger: Soap.Logger);
|
|
7
|
+
checkFailedAttempts(identifier: string): Promise<void>;
|
|
8
|
+
incrementFailedAttempts(account: any): Promise<void>;
|
|
9
|
+
resetFailedAttempts(account: any): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthThrottleService = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
class AuthThrottleService {
|
|
6
|
+
config;
|
|
7
|
+
logger;
|
|
8
|
+
constructor(config, logger) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
}
|
|
12
|
+
async checkFailedAttempts(identifier) {
|
|
13
|
+
let failedAttempts;
|
|
14
|
+
if (this.config?.maxFailedAttempts) {
|
|
15
|
+
try {
|
|
16
|
+
failedAttempts =
|
|
17
|
+
(await this.config.getFailedAttempts?.(identifier)) || 0;
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
this.logger.error("Check failed attempts:", e);
|
|
21
|
+
}
|
|
22
|
+
if (Number.isInteger(failedAttempts) &&
|
|
23
|
+
failedAttempts >= this.config.maxFailedAttempts) {
|
|
24
|
+
this.logger.warn(`User ${identifier} is temporarily locked out.`);
|
|
25
|
+
throw new errors_1.AccountLockedError();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async incrementFailedAttempts(account) {
|
|
30
|
+
if (this.config?.incrementFailedAttempts) {
|
|
31
|
+
await this.config.incrementFailedAttempts(account);
|
|
32
|
+
const failedAttempts = (await this.config.getFailedAttempts?.(account)) || 0;
|
|
33
|
+
if (this.config?.maxFailedAttempts &&
|
|
34
|
+
failedAttempts >= this.config.maxFailedAttempts) {
|
|
35
|
+
throw new errors_1.AccountLockedError();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async resetFailedAttempts(account) {
|
|
40
|
+
await this.config?.resetFailedAttempts?.(account);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.AuthThrottleService = AuthThrottleService;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./account-lock.service";
|
|
2
|
+
export * from "./auth-throttle.service";
|
|
3
|
+
export * from "./jwks.service";
|
|
4
|
+
export * from "./mfa.service";
|
|
5
|
+
export * from "./password.service";
|
|
6
|
+
export * from "./pkce.service";
|
|
7
|
+
export * from "./rate-limit.service";
|
|
8
|
+
export * from "./role.service";
|
|
@@ -14,6 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./
|
|
18
|
-
__exportStar(require("./
|
|
19
|
-
__exportStar(require("./
|
|
17
|
+
__exportStar(require("./account-lock.service"), exports);
|
|
18
|
+
__exportStar(require("./auth-throttle.service"), exports);
|
|
19
|
+
__exportStar(require("./jwks.service"), exports);
|
|
20
|
+
__exportStar(require("./mfa.service"), exports);
|
|
21
|
+
__exportStar(require("./password.service"), exports);
|
|
22
|
+
__exportStar(require("./pkce.service"), exports);
|
|
23
|
+
__exportStar(require("./rate-limit.service"), exports);
|
|
24
|
+
__exportStar(require("./role.service"), exports);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.JwtService = void 0;
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
const jwks_rsa_1 = __importDefault(require("jwks-rsa"));
|
|
9
|
+
const oauth2_errors_1 = require("../strategies/oauth2/oauth2.errors");
|
|
10
|
+
class JwtService {
|
|
11
|
+
config;
|
|
12
|
+
client;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.client = (0, jwks_rsa_1.default)({ jwksUri: this.config.jwksUri });
|
|
16
|
+
}
|
|
17
|
+
async verify(idToken) {
|
|
18
|
+
try {
|
|
19
|
+
const decodedHeader = jsonwebtoken_1.default.decode(idToken, { complete: true });
|
|
20
|
+
if (!decodedHeader?.header?.kid) {
|
|
21
|
+
throw new oauth2_errors_1.InvalidIdTokenError("Invalid ID Token structure.");
|
|
22
|
+
}
|
|
23
|
+
const key = await this.client.getSigningKey(decodedHeader.header.kid);
|
|
24
|
+
const publicKey = key.getPublicKey();
|
|
25
|
+
const payload = jsonwebtoken_1.default.verify(idToken, publicKey, {
|
|
26
|
+
algorithms: this.config.algorithms || ["RS256"],
|
|
27
|
+
issuer: this.config.issuer,
|
|
28
|
+
audience: this.config.audience,
|
|
29
|
+
});
|
|
30
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
31
|
+
throw new oauth2_errors_1.InvalidIdTokenError("ID Token has expired");
|
|
32
|
+
}
|
|
33
|
+
return payload;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error("JWT verification failed:", error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.JwtService = JwtService;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { MfaConfig } from "../types";
|
|
3
|
+
export declare class MfaService<TContext = unknown, TUser = unknown> {
|
|
4
|
+
private config;
|
|
5
|
+
private logger;
|
|
6
|
+
constructor(config: MfaConfig, logger: Soap.Logger);
|
|
7
|
+
checkMfa(user: TUser, context: TContext): Promise<void>;
|
|
8
|
+
protected lockMfaOnFailure(user: TUser): void;
|
|
9
|
+
protected getMfaAttempts(user: TUser): Promise<number>;
|
|
10
|
+
protected incrementMfaAttempts(user: TUser): void;
|
|
11
|
+
protected resetMfaAttempts(user: TUser): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MfaService = void 0;
|
|
4
|
+
class MfaService {
|
|
5
|
+
config;
|
|
6
|
+
logger;
|
|
7
|
+
constructor(config, logger) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.logger = logger;
|
|
10
|
+
}
|
|
11
|
+
async checkMfa(user, context) {
|
|
12
|
+
try {
|
|
13
|
+
if (this.config.isMfaRequired?.(user)) {
|
|
14
|
+
const mfaCode = this.config.extractMfaCode?.(context);
|
|
15
|
+
if (!mfaCode) {
|
|
16
|
+
await this.config.sendMfaCode?.(user, context);
|
|
17
|
+
throw new Error("2FA required. A verification code has been sent.");
|
|
18
|
+
}
|
|
19
|
+
const attempts = (await this.getMfaAttempts(user)) || 0;
|
|
20
|
+
if (this.config.maxMfaAttempts &&
|
|
21
|
+
attempts >= this.config.maxMfaAttempts) {
|
|
22
|
+
this.logger?.warn(`User ${user} exceeded maximum MFA attempts.`);
|
|
23
|
+
await this.lockMfaOnFailure(user);
|
|
24
|
+
throw new Error("Your account has been temporarily locked due to too many failed 2FA attempts.");
|
|
25
|
+
}
|
|
26
|
+
const isValidMfa = await this.config.validateMfaCode?.(user, mfaCode);
|
|
27
|
+
if (!isValidMfa) {
|
|
28
|
+
this.logger?.warn(`Invalid MFA code attempt for user: ${user}`);
|
|
29
|
+
await this.incrementMfaAttempts(user);
|
|
30
|
+
throw new Error("Invalid 2FA code provided.");
|
|
31
|
+
}
|
|
32
|
+
await this.resetMfaAttempts(user);
|
|
33
|
+
this.logger?.info(`2FA successfully validated for user: ${user}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
this.logger?.error(`2FA validation error for user: ${user}`, error);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
lockMfaOnFailure(user) {
|
|
42
|
+
try {
|
|
43
|
+
this.config.lockMfaOnFailure?.(user);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.logger?.error(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async getMfaAttempts(user) {
|
|
50
|
+
try {
|
|
51
|
+
return this.config.getMfaAttempts?.(user);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
this.logger?.error(error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
incrementMfaAttempts(user) {
|
|
58
|
+
try {
|
|
59
|
+
this.config.incrementMfaAttempts?.(user);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
this.logger?.error(error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
resetMfaAttempts(user) {
|
|
66
|
+
try {
|
|
67
|
+
this.config.resetMfaAttempts?.(user);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.logger?.error(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.MfaService = MfaService;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { PasswordPolicyConfig } from "../types";
|
|
3
|
+
export declare class PasswordService {
|
|
4
|
+
private config;
|
|
5
|
+
private logger;
|
|
6
|
+
constructor(config: PasswordPolicyConfig, logger: Soap.Logger);
|
|
7
|
+
validatePassword(password: string): Promise<boolean>;
|
|
8
|
+
getLastPasswordChange(identifier: string): Promise<Date>;
|
|
9
|
+
generateResetToken(identifier: string): Promise<string>;
|
|
10
|
+
sendResetEmail(identifier: string, token: string): Promise<void>;
|
|
11
|
+
validateResetToken(token: string): Promise<boolean>;
|
|
12
|
+
updatePassword(identifier: string, newPassword: string): Promise<void>;
|
|
13
|
+
isPasswordChangeRequired(identifier: string): Promise<boolean>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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.PasswordService = void 0;
|
|
27
|
+
const Soap = __importStar(require("@soapjs/soap"));
|
|
28
|
+
class PasswordService {
|
|
29
|
+
config;
|
|
30
|
+
logger;
|
|
31
|
+
constructor(config, logger) {
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.logger = logger;
|
|
34
|
+
}
|
|
35
|
+
async validatePassword(password) {
|
|
36
|
+
return this.config.validatePassword?.(password);
|
|
37
|
+
}
|
|
38
|
+
async getLastPasswordChange(identifier) {
|
|
39
|
+
return this.config.getLastPasswordChange?.(identifier);
|
|
40
|
+
}
|
|
41
|
+
async generateResetToken(identifier) {
|
|
42
|
+
if (!this.config?.generateResetToken) {
|
|
43
|
+
throw new Soap.NotImplementedError("generateResetToken");
|
|
44
|
+
}
|
|
45
|
+
return this.config.generateResetToken?.(identifier);
|
|
46
|
+
}
|
|
47
|
+
async sendResetEmail(identifier, token) {
|
|
48
|
+
if (!this.config?.sendResetEmail) {
|
|
49
|
+
throw new Soap.NotImplementedError("sendResetEmail");
|
|
50
|
+
}
|
|
51
|
+
return this.config.sendResetEmail?.(identifier, token);
|
|
52
|
+
}
|
|
53
|
+
async validateResetToken(token) {
|
|
54
|
+
if (!this.config?.validateResetToken) {
|
|
55
|
+
throw new Soap.NotImplementedError("validateResetToken");
|
|
56
|
+
}
|
|
57
|
+
return this.config.validateResetToken?.(token);
|
|
58
|
+
}
|
|
59
|
+
async updatePassword(identifier, newPassword) {
|
|
60
|
+
if (!this.config?.updatePassword) {
|
|
61
|
+
throw new Soap.NotImplementedError("updatePassword");
|
|
62
|
+
}
|
|
63
|
+
return this.config.updatePassword?.(identifier, newPassword);
|
|
64
|
+
}
|
|
65
|
+
async isPasswordChangeRequired(identifier) {
|
|
66
|
+
if (this.config.passwordExpirationDays) {
|
|
67
|
+
if (!this.config.getLastPasswordChange) {
|
|
68
|
+
throw new Soap.NotImplementedError("getLastPasswordChange");
|
|
69
|
+
}
|
|
70
|
+
const lastChanged = await this.config.getLastPasswordChange(identifier);
|
|
71
|
+
return (lastChanged &&
|
|
72
|
+
Date.now() - Number(lastChanged) >
|
|
73
|
+
this.config.passwordExpirationDays * 86400000);
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.PasswordService = PasswordService;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PKCEConfig } from "../types";
|
|
2
|
+
export declare class PKCEService<TContext> {
|
|
3
|
+
private config;
|
|
4
|
+
constructor(config: PKCEConfig<TContext>);
|
|
5
|
+
private defaultGenerateCodeChallenge;
|
|
6
|
+
generateCodeVerifier(context: TContext): Promise<string>;
|
|
7
|
+
extractCodeVerifier(context: TContext): string | undefined;
|
|
8
|
+
generateCodeChallenge(codeVerifier: string, context: TContext): Promise<string>;
|
|
9
|
+
extractCodeChallenge(context: TContext): string | undefined;
|
|
10
|
+
isCodeVerifierExpired(context: TContext): Promise<boolean>;
|
|
11
|
+
isCodeChallengeExpired(context: TContext): Promise<boolean>;
|
|
12
|
+
clearCodeVerifier(context: TContext): Promise<void>;
|
|
13
|
+
clearCodeChallenge(context: TContext): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PKCEService = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const tools_1 = require("../tools");
|
|
9
|
+
class PKCEService {
|
|
10
|
+
config;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
defaultGenerateCodeChallenge(verifier) {
|
|
15
|
+
return crypto_1.default
|
|
16
|
+
.createHash("sha256")
|
|
17
|
+
.update(verifier)
|
|
18
|
+
.digest("base64")
|
|
19
|
+
.replace(/\+/g, "-")
|
|
20
|
+
.replace(/\//g, "_")
|
|
21
|
+
.replace(/=+$/, "");
|
|
22
|
+
}
|
|
23
|
+
async generateCodeVerifier(context) {
|
|
24
|
+
const cv = this.config.verifier.generate?.() || (0, tools_1.generateRandomString)();
|
|
25
|
+
this.config.verifier.embed(context, cv);
|
|
26
|
+
const expirationTime = Date.now() + (this.config.verifier.expiresIn ?? 300) * 1000;
|
|
27
|
+
await this.config.verifier.persistence?.store(cv, {
|
|
28
|
+
expiration: expirationTime,
|
|
29
|
+
});
|
|
30
|
+
return cv;
|
|
31
|
+
}
|
|
32
|
+
extractCodeVerifier(context) {
|
|
33
|
+
return this.config.verifier.extract(context);
|
|
34
|
+
}
|
|
35
|
+
async generateCodeChallenge(codeVerifier, context) {
|
|
36
|
+
const challenge = this.config.challenge.generate?.(codeVerifier) ||
|
|
37
|
+
this.defaultGenerateCodeChallenge(codeVerifier);
|
|
38
|
+
this.config.challenge.embed(context, challenge);
|
|
39
|
+
const expirationTime = Date.now() + (this.config.challenge.expiresIn ?? 300) * 1000;
|
|
40
|
+
await this.config.challenge.persistence?.store?.(challenge, {
|
|
41
|
+
expiration: expirationTime,
|
|
42
|
+
});
|
|
43
|
+
return challenge;
|
|
44
|
+
}
|
|
45
|
+
extractCodeChallenge(context) {
|
|
46
|
+
return this.config.challenge.extract(context);
|
|
47
|
+
}
|
|
48
|
+
async isCodeVerifierExpired(context) {
|
|
49
|
+
const codeVerifier = this.extractCodeVerifier(context);
|
|
50
|
+
if (!codeVerifier)
|
|
51
|
+
return true;
|
|
52
|
+
const stored = await this.config.verifier?.persistence?.read?.(codeVerifier);
|
|
53
|
+
if (!stored || !stored.expiration)
|
|
54
|
+
return true;
|
|
55
|
+
return Date.now() > stored.expiration;
|
|
56
|
+
}
|
|
57
|
+
async isCodeChallengeExpired(context) {
|
|
58
|
+
const challenge = this.extractCodeChallenge(context);
|
|
59
|
+
if (!challenge)
|
|
60
|
+
return true;
|
|
61
|
+
const stored = await this.config.challenge?.persistence?.read?.(challenge);
|
|
62
|
+
if (!stored || !stored.expiration)
|
|
63
|
+
return true;
|
|
64
|
+
return Date.now() > stored.expiration;
|
|
65
|
+
}
|
|
66
|
+
async clearCodeVerifier(context) {
|
|
67
|
+
const cv = this.config.verifier.extract(context);
|
|
68
|
+
if (cv) {
|
|
69
|
+
await this.config.verifier.persistence?.remove?.(cv);
|
|
70
|
+
this.config.verifier.embed(context, "");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async clearCodeChallenge(context) {
|
|
74
|
+
const challenge = this.config.challenge.extract(context);
|
|
75
|
+
if (challenge) {
|
|
76
|
+
await this.config.challenge.persistence?.remove?.(challenge);
|
|
77
|
+
this.config.challenge.embed(context, "");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.PKCEService = PKCEService;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { RateLimitConfig } from "../types";
|
|
3
|
+
export declare class RateLimitService {
|
|
4
|
+
private config;
|
|
5
|
+
private logger;
|
|
6
|
+
constructor(config: RateLimitConfig, logger: Soap.Logger);
|
|
7
|
+
incrementRequestCount(...args: unknown[]): Promise<void>;
|
|
8
|
+
checkRateLimit(data: unknown): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RateLimitService = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
class RateLimitService {
|
|
6
|
+
config;
|
|
7
|
+
logger;
|
|
8
|
+
constructor(config, logger) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
}
|
|
12
|
+
async incrementRequestCount(...args) {
|
|
13
|
+
try {
|
|
14
|
+
await this.config.incrementRequestCount(...args);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
this.logger?.error(error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async checkRateLimit(data) {
|
|
21
|
+
if (await this.config.checkRateLimit?.(data)) {
|
|
22
|
+
throw new errors_1.RateLimitExceededError();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.RateLimitService = RateLimitService;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { RoleAuthorizationConfig } from "../types";
|
|
3
|
+
export declare class RoleService<TUser> {
|
|
4
|
+
private config;
|
|
5
|
+
private logger;
|
|
6
|
+
private roles;
|
|
7
|
+
constructor(config: RoleAuthorizationConfig, logger: Soap.Logger);
|
|
8
|
+
isAuthorized(user: TUser): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RoleService = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
class RoleService {
|
|
6
|
+
config;
|
|
7
|
+
logger;
|
|
8
|
+
roles;
|
|
9
|
+
constructor(config, logger) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
this.roles = config.roles || [];
|
|
13
|
+
}
|
|
14
|
+
async isAuthorized(user) {
|
|
15
|
+
if (typeof this.config.authorizeByRoles === "function" &&
|
|
16
|
+
Array.isArray(this.roles) &&
|
|
17
|
+
this.roles.length > 0) {
|
|
18
|
+
const hasAccess = await this.config.authorizeByRoles(user, this.roles);
|
|
19
|
+
if (!hasAccess) {
|
|
20
|
+
throw new errors_1.UnauthorizedRoleError();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.RoleService = RoleService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const file_session_store_1 = require("../file.session-store");
|
|
7
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
jest.mock("fs/promises", () => ({
|
|
10
|
+
mkdir: jest.fn(),
|
|
11
|
+
readFile: jest.fn(),
|
|
12
|
+
writeFile: jest.fn(),
|
|
13
|
+
unlink: jest.fn(),
|
|
14
|
+
readdir: jest.fn(),
|
|
15
|
+
}));
|
|
16
|
+
describe("FileSessionStore", () => {
|
|
17
|
+
let store;
|
|
18
|
+
const mockSessionsDir = "/mock/sessions";
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
store = new file_session_store_1.FileSessionStore(mockSessionsDir);
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
describe("init", () => {
|
|
24
|
+
it("should create the sessions directory recursively", async () => {
|
|
25
|
+
await store.init();
|
|
26
|
+
expect(promises_1.default.mkdir).toHaveBeenCalledWith(mockSessionsDir, {
|
|
27
|
+
recursive: true,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
it("should catch and log errors if mkdir fails", async () => {
|
|
31
|
+
const consoleSpy = jest
|
|
32
|
+
.spyOn(console, "error")
|
|
33
|
+
.mockImplementation(() => { });
|
|
34
|
+
promises_1.default.mkdir.mockRejectedValueOnce(new Error("mkdir error"));
|
|
35
|
+
await store.init();
|
|
36
|
+
expect(consoleSpy).toHaveBeenCalledWith("Error initializing session directory:", expect.any(Error));
|
|
37
|
+
consoleSpy.mockRestore();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe("getSession", () => {
|
|
41
|
+
it("should return parsed JSON if file read is successful", async () => {
|
|
42
|
+
const mockData = { user: { id: "123" } };
|
|
43
|
+
promises_1.default.readFile.mockResolvedValueOnce(JSON.stringify(mockData));
|
|
44
|
+
const session = await store.getSession("session123");
|
|
45
|
+
expect(session).toEqual(mockData);
|
|
46
|
+
expect(promises_1.default.readFile).toHaveBeenCalledWith(path_1.default.join(mockSessionsDir, "session123"), "utf8");
|
|
47
|
+
});
|
|
48
|
+
it("should return null and log error if readFile fails", async () => {
|
|
49
|
+
const consoleSpy = jest
|
|
50
|
+
.spyOn(console, "error")
|
|
51
|
+
.mockImplementation(() => { });
|
|
52
|
+
promises_1.default.readFile.mockRejectedValueOnce(new Error("read error"));
|
|
53
|
+
const session = await store.getSession("badSession");
|
|
54
|
+
expect(session).toBeNull();
|
|
55
|
+
expect(consoleSpy).toHaveBeenCalledWith("Error getting session:", expect.any(Error));
|
|
56
|
+
consoleSpy.mockRestore();
|
|
57
|
+
});
|
|
58
|
+
it("should return null if JSON parse fails", async () => {
|
|
59
|
+
const consoleSpy = jest
|
|
60
|
+
.spyOn(console, "error")
|
|
61
|
+
.mockImplementation(() => { });
|
|
62
|
+
promises_1.default.readFile.mockResolvedValueOnce("invalid-json");
|
|
63
|
+
const session = await store.getSession("corruptSession");
|
|
64
|
+
expect(session).toBeNull();
|
|
65
|
+
expect(consoleSpy).toHaveBeenCalledWith("Error getting session:", expect.any(Error));
|
|
66
|
+
consoleSpy.mockRestore();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("setSession", () => {
|
|
70
|
+
it("should write stringified session data to a file", async () => {
|
|
71
|
+
const mockData = { user: { id: "xyz" } };
|
|
72
|
+
await store.setSession("sessionABC", mockData);
|
|
73
|
+
expect(promises_1.default.writeFile).toHaveBeenCalledWith(path_1.default.join(mockSessionsDir, "sessionABC"), JSON.stringify(mockData), "utf8");
|
|
74
|
+
});
|
|
75
|
+
it("should throw error if writeFile fails", async () => {
|
|
76
|
+
promises_1.default.writeFile.mockRejectedValueOnce(new Error("write error"));
|
|
77
|
+
await expect(store.setSession("sessionError", { user: { id: "1" } })).rejects.toThrow("write error");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("destroySession", () => {
|
|
81
|
+
it("should unlink the file associated with the session ID", async () => {
|
|
82
|
+
await store.destroySession("destroyMe");
|
|
83
|
+
expect(promises_1.default.unlink).toHaveBeenCalledWith(path_1.default.join(mockSessionsDir, "destroyMe"));
|
|
84
|
+
});
|
|
85
|
+
it("should ignore ENOENT errors (file not found)", async () => {
|
|
86
|
+
promises_1.default.unlink.mockRejectedValueOnce({ code: "ENOENT" });
|
|
87
|
+
await expect(store.destroySession("notExists")).resolves.toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
it("should rethrow other errors", async () => {
|
|
90
|
+
promises_1.default.unlink.mockRejectedValueOnce(new Error("unlink error"));
|
|
91
|
+
await expect(store.destroySession("sessionX")).rejects.toThrow("unlink error");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("touchSession", () => {
|
|
95
|
+
it("should call setSession with updated data", async () => {
|
|
96
|
+
const setSessionSpy = jest.spyOn(store, "setSession").mockResolvedValue();
|
|
97
|
+
const newData = { user: { id: "touchMe" } };
|
|
98
|
+
await store.touchSession("touchId", newData);
|
|
99
|
+
expect(setSessionSpy).toHaveBeenCalledWith("touchId", newData);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("getSessionIds", () => {
|
|
103
|
+
it("should return the base names of files in the sessions directory", async () => {
|
|
104
|
+
promises_1.default.readdir.mockResolvedValueOnce([
|
|
105
|
+
{ name: "abc", isFile: () => true },
|
|
106
|
+
{ name: "def.json", isFile: () => true },
|
|
107
|
+
{ name: "subdir", isFile: () => false },
|
|
108
|
+
]);
|
|
109
|
+
const result = await store.getSessionIds();
|
|
110
|
+
expect(result).toEqual(["abc", "def"]);
|
|
111
|
+
});
|
|
112
|
+
it("should throw if readdir fails", async () => {
|
|
113
|
+
promises_1.default.readdir.mockRejectedValueOnce(new Error("readdir error"));
|
|
114
|
+
await expect(store.getSessionIds()).rejects.toThrow("readdir error");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|