@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GoogleStrategy = void 0;
|
|
4
|
+
const http_oauth2_strategy_1 = require("./http-oauth2.strategy");
|
|
5
|
+
const GOOGLE_ENDPOINTS = {
|
|
6
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
7
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
8
|
+
userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
|
|
9
|
+
revocationUrl: "https://oauth2.googleapis.com/revoke",
|
|
10
|
+
};
|
|
11
|
+
class GoogleStrategy extends http_oauth2_strategy_1.HttpOAuth2Strategy {
|
|
12
|
+
name = "google";
|
|
13
|
+
constructor(config, session, jwt, logger) {
|
|
14
|
+
super({
|
|
15
|
+
grantType: "authorization_code",
|
|
16
|
+
scope: config.scope ?? ["openid", "email", "profile"],
|
|
17
|
+
routes: {
|
|
18
|
+
login: { path: "/auth/google", method: "GET" },
|
|
19
|
+
callback: { path: "/auth/google/callback", method: "GET" },
|
|
20
|
+
logout: { path: "/auth/google/logout", method: "POST" },
|
|
21
|
+
...config.routes,
|
|
22
|
+
},
|
|
23
|
+
...config,
|
|
24
|
+
endpoints: { ...GOOGLE_ENDPOINTS, ...config.endpoints },
|
|
25
|
+
}, session, jwt, logger);
|
|
26
|
+
}
|
|
27
|
+
async fetchUser(accessToken) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(GOOGLE_ENDPOINTS.userInfoUrl, {
|
|
30
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok)
|
|
33
|
+
return null;
|
|
34
|
+
const profile = await response.json();
|
|
35
|
+
if (this.config.user?.validateUser) {
|
|
36
|
+
return this.config.user.validateUser(profile);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
id: profile.sub,
|
|
40
|
+
email: profile.email,
|
|
41
|
+
username: profile.name,
|
|
42
|
+
picture: profile.picture,
|
|
43
|
+
emailVerified: profile.email_verified,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
this.logger?.error("GoogleStrategy.fetchUser failed:", error);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.GoogleStrategy = GoogleStrategy;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { OAuth2Strategy } from "../oauth2.strategy";
|
|
3
|
+
import { OAuth2StrategyConfig } from "../oauth2.types";
|
|
4
|
+
import { SessionHandler } from "../../../session/session-handler";
|
|
5
|
+
import { JwtStrategy } from "../../jwt/jwt.strategy";
|
|
6
|
+
export declare abstract class HttpOAuth2Strategy<TUser extends Soap.AuthUser = Soap.AuthUser> extends OAuth2Strategy<Soap.HttpContext, TUser> {
|
|
7
|
+
constructor(config: OAuth2StrategyConfig<Soap.HttpContext, TUser>, session?: SessionHandler, jwt?: JwtStrategy<Soap.HttpContext, TUser>, logger?: Soap.Logger);
|
|
8
|
+
protected extractAccessToken(ctx: Soap.HttpContext): Promise<string | undefined>;
|
|
9
|
+
protected extractRefreshToken(ctx: Soap.HttpContext): Promise<string | undefined>;
|
|
10
|
+
protected storeAccessToken(_token: string, _ctx: Soap.HttpContext): Promise<void>;
|
|
11
|
+
protected storeRefreshToken(_token: string, _ctx: Soap.HttpContext): Promise<void>;
|
|
12
|
+
protected embedAccessToken(token: string, ctx: Soap.HttpContext): void;
|
|
13
|
+
protected embedRefreshToken(token: string, ctx: Soap.HttpContext): void;
|
|
14
|
+
protected extractAuthorizationCode(ctx: Soap.HttpContext): string | null;
|
|
15
|
+
protected redirectUser(ctx: Soap.HttpContext, authUrl: string): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpOAuth2Strategy = void 0;
|
|
4
|
+
const oauth2_strategy_1 = require("../oauth2.strategy");
|
|
5
|
+
class HttpOAuth2Strategy extends oauth2_strategy_1.OAuth2Strategy {
|
|
6
|
+
constructor(config, session, jwt, logger) {
|
|
7
|
+
super(config, session, jwt, logger);
|
|
8
|
+
}
|
|
9
|
+
extractAccessToken(ctx) {
|
|
10
|
+
const auth = ctx.req.headers?.authorization;
|
|
11
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
12
|
+
return Promise.resolve(auth.slice(7));
|
|
13
|
+
}
|
|
14
|
+
return Promise.resolve(ctx.req.cookies?.access_token);
|
|
15
|
+
}
|
|
16
|
+
extractRefreshToken(ctx) {
|
|
17
|
+
return Promise.resolve(ctx.req.cookies?.refresh_token);
|
|
18
|
+
}
|
|
19
|
+
storeAccessToken(_token, _ctx) {
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
storeRefreshToken(_token, _ctx) {
|
|
23
|
+
return Promise.resolve();
|
|
24
|
+
}
|
|
25
|
+
embedAccessToken(token, ctx) {
|
|
26
|
+
ctx.res.setHeader("Authorization", `Bearer ${token}`);
|
|
27
|
+
}
|
|
28
|
+
embedRefreshToken(token, ctx) {
|
|
29
|
+
ctx.res.cookie("refresh_token", token, {
|
|
30
|
+
httpOnly: true,
|
|
31
|
+
secure: true,
|
|
32
|
+
sameSite: "lax",
|
|
33
|
+
maxAge: 30 * 24 * 60 * 60 * 1000,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
extractAuthorizationCode(ctx) {
|
|
37
|
+
return ctx.req.query?.code ?? null;
|
|
38
|
+
}
|
|
39
|
+
redirectUser(ctx, authUrl) {
|
|
40
|
+
if (typeof ctx.res.redirect === "function") {
|
|
41
|
+
ctx.res.redirect(authUrl);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
ctx.res.setHeader("Location", authUrl);
|
|
45
|
+
ctx.res.status(302).json({ message: "Redirecting", location: authUrl });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.HttpOAuth2Strategy = HttpOAuth2Strategy;
|
|
@@ -0,0 +1,21 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./http-oauth2.strategy"), exports);
|
|
18
|
+
__exportStar(require("./google.strategy"), exports);
|
|
19
|
+
__exportStar(require("./github.strategy"), exports);
|
|
20
|
+
__exportStar(require("./facebook.strategy"), exports);
|
|
21
|
+
__exportStar(require("./provider.types"), exports);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { OAuth2StrategyConfig, OAuth2Endpoints } from "../oauth2.types";
|
|
3
|
+
export interface SocialProviderConfig<TUser extends Soap.AuthUser = Soap.AuthUser> extends Omit<OAuth2StrategyConfig<Soap.HttpContext, TUser>, "endpoints" | "grantType" | "routes"> {
|
|
4
|
+
grantType?: "authorization_code";
|
|
5
|
+
endpoints?: Partial<OAuth2Endpoints>;
|
|
6
|
+
routes?: OAuth2StrategyConfig<Soap.HttpContext, TUser>["routes"];
|
|
7
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as Soap from "@soapjs/soap";
|
|
2
|
-
import {
|
|
2
|
+
import { TokenAuthStrategyConfig } from "../types";
|
|
3
3
|
import { BaseAuthStrategy } from "./base-auth.strategy";
|
|
4
4
|
import { SessionHandler } from "../session/session-handler";
|
|
5
|
-
export declare abstract class TokenAuthStrategy<TContext =
|
|
5
|
+
export declare abstract class TokenAuthStrategy<TContext = Soap.HttpContext, TUser extends Soap.AuthUser = Soap.AuthUser> extends BaseAuthStrategy<TContext, TUser> {
|
|
6
6
|
protected config: TokenAuthStrategyConfig<TContext, TUser>;
|
|
7
7
|
protected session?: SessionHandler;
|
|
8
8
|
protected logger?: Soap.Logger;
|
|
@@ -20,9 +20,9 @@ export declare abstract class TokenAuthStrategy<TContext = unknown, TUser = unkn
|
|
|
20
20
|
protected abstract embedAccessToken(token: string, context: TContext): void;
|
|
21
21
|
protected abstract embedRefreshToken(token: string, context: TContext): void;
|
|
22
22
|
protected fetchUser(payload: unknown): Promise<TUser | null>;
|
|
23
|
-
authenticate(context: TContext): Promise<AuthResult<TUser
|
|
23
|
+
authenticate(context: TContext): Promise<Soap.AuthResult<TUser> | null>;
|
|
24
24
|
private verifyAndFetchUser;
|
|
25
|
-
refreshTokens(context: TContext, existingUser?: TUser): Promise<AuthResult<TUser>>;
|
|
25
|
+
refreshTokens(context: TContext, existingUser?: TUser): Promise<Soap.AuthResult<TUser>>;
|
|
26
26
|
issueTokens(user: TUser, context: TContext, rotate?: boolean): Promise<{
|
|
27
27
|
accessToken: string;
|
|
28
28
|
refreshToken: any;
|
|
@@ -145,12 +145,11 @@ class TokenAuthStrategy extends base_auth_strategy_1.BaseAuthStrategy {
|
|
|
145
145
|
const accessToken = await this.generateAccessToken(user, context);
|
|
146
146
|
let refreshToken;
|
|
147
147
|
if (this.config.refreshToken) {
|
|
148
|
-
|
|
149
|
-
if (rotate && this.config?.refreshToken?.rotation) {
|
|
148
|
+
if (rotate && this.config.refreshToken.rotation) {
|
|
150
149
|
const oldRefreshToken = this.extractRefreshToken(context);
|
|
151
150
|
refreshToken = await this.rotateToken(oldRefreshToken, user, context);
|
|
152
151
|
}
|
|
153
|
-
else
|
|
152
|
+
else {
|
|
154
153
|
refreshToken = await this.generateRefreshToken(user, context);
|
|
155
154
|
}
|
|
156
155
|
}
|
package/build/tools/tools.js
CHANGED
|
@@ -4,10 +4,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.generateRandomString = exports.resolveConfig = exports.generateUUID = void 0;
|
|
7
|
-
const uuid_1 = require("uuid");
|
|
8
7
|
const crypto_1 = __importDefault(require("crypto"));
|
|
9
8
|
const generateUUID = async () => {
|
|
10
|
-
return Promise.resolve(
|
|
9
|
+
return Promise.resolve(crypto_1.default.randomUUID());
|
|
11
10
|
};
|
|
12
11
|
exports.generateUUID = generateUUID;
|
|
13
12
|
function resolveConfig(strategyConfig, globalConfig) {
|
package/build/types.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as Soap from "@soapjs/soap";
|
|
2
|
+
export type AuthResult<TUser extends Soap.AuthUser = Soap.AuthUser> = Soap.AuthResult<TUser>;
|
|
3
|
+
export type AuthStrategy<TUser extends Soap.AuthUser = Soap.AuthUser> = Soap.AuthStrategy<TUser>;
|
|
2
4
|
import { LocalStrategyConfig } from "./strategies/local/local.types";
|
|
3
5
|
import { OAuth2StrategyConfig } from "./strategies/oauth2/oauth2.types";
|
|
4
6
|
import { ApiKeyStrategyConfig } from "./strategies/api-key/api-key.types";
|
|
@@ -71,29 +73,43 @@ export interface MfaConfig<TUser = unknown, TContext = unknown> {
|
|
|
71
73
|
resetMfaAttempts?: (user: TUser) => Promise<void>;
|
|
72
74
|
incrementMfaAttempts?: (user: TUser) => Promise<void>;
|
|
73
75
|
}
|
|
76
|
+
export type PasswordType = "default" | "one-time" | "temporary";
|
|
77
|
+
export type NewPasswordOptions = {
|
|
78
|
+
expiresIn?: number;
|
|
79
|
+
type: PasswordType;
|
|
80
|
+
additional?: Record<string, unknown>;
|
|
81
|
+
};
|
|
82
|
+
export type PasswordInfo = {
|
|
83
|
+
type: PasswordType;
|
|
84
|
+
expiresIn?: number;
|
|
85
|
+
lastChangeDate?: Date;
|
|
86
|
+
};
|
|
74
87
|
export interface PasswordPolicyConfig {
|
|
75
|
-
validatePassword?: (password: string) => Promise<
|
|
76
|
-
|
|
88
|
+
validatePassword?: (password: string, previousPassword?: string) => Promise<void>;
|
|
89
|
+
getPasswordPasswordInfo?: (identifier: string) => Promise<PasswordInfo>;
|
|
77
90
|
passwordExpirationDays?: number;
|
|
78
91
|
generateResetToken?: (identifier: string) => Promise<string>;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
updatePassword?: (identifier: string, newPassword: string) => Promise<void>;
|
|
92
|
+
sendPasswordResetEmail?: (identifier: string, token: string) => Promise<void>;
|
|
93
|
+
validatePasswordResetToken?: (token: string) => Promise<boolean>;
|
|
94
|
+
updatePassword?: (identifier: string, newPassword: string, options?: NewPasswordOptions) => Promise<void>;
|
|
95
|
+
generatePassword?: (identifier: string, options: NewPasswordOptions) => Promise<string>;
|
|
82
96
|
}
|
|
83
97
|
export interface UserConfig<TUser = unknown> {
|
|
84
98
|
fetchUser: (payload: unknown) => Promise<TUser | null>;
|
|
85
99
|
validateUser?: (payload: unknown) => Promise<any>;
|
|
86
100
|
}
|
|
87
|
-
export
|
|
101
|
+
export type CredentailsConfig<TContext = any> = CredentialsConfig<TContext>;
|
|
102
|
+
export interface CredentialsConfig<TContext = any> {
|
|
88
103
|
extractCredentials: <TCredentials = {
|
|
89
104
|
identifier: string;
|
|
90
105
|
password: string;
|
|
106
|
+
newPassword?: string;
|
|
91
107
|
}>(context: TContext) => TCredentials;
|
|
92
108
|
verifyCredentials: (identifier: string, password: string) => Promise<boolean>;
|
|
93
109
|
}
|
|
94
110
|
export interface CredentialAuthStrategyConfig<TContext = unknown, TUser = unknown> extends BaseAuthStrategyConfig<TContext, TUser> {
|
|
95
111
|
passwordPolicy?: PasswordPolicyConfig;
|
|
96
|
-
credentials?:
|
|
112
|
+
credentials?: CredentialsConfig<TContext>;
|
|
97
113
|
jwt?: TokenAuthConfig<TContext>;
|
|
98
114
|
user?: UserConfig<TUser>;
|
|
99
115
|
allowGuest?: boolean;
|
|
@@ -155,43 +171,26 @@ export type AuthFailureContext<TContext = unknown> = {
|
|
|
155
171
|
email?: string;
|
|
156
172
|
additional?: Record<string, unknown>;
|
|
157
173
|
};
|
|
158
|
-
export
|
|
159
|
-
user: TUser;
|
|
160
|
-
session?: SessionInfo<TSessionData>;
|
|
161
|
-
tokens?: {
|
|
162
|
-
accessToken?: string;
|
|
163
|
-
refreshToken?: string | null;
|
|
164
|
-
apiKey?: string | null;
|
|
165
|
-
[key: string]: string;
|
|
166
|
-
};
|
|
167
|
-
};
|
|
168
|
-
export interface AuthStrategy<TContext = unknown, TUser = unknown> {
|
|
169
|
-
init?(...args: unknown[]): Promise<void>;
|
|
170
|
-
authenticate(context?: TContext, ...args: unknown[]): Promise<AuthResult<TUser> | null>;
|
|
171
|
-
authorize?(user: any, action: string, resource?: string): Promise<boolean>;
|
|
172
|
-
refresh?(refreshToken: string): Promise<string>;
|
|
173
|
-
logout?(context?: TContext): Promise<void>;
|
|
174
|
-
logout?(context?: TContext): Promise<void>;
|
|
175
|
-
}
|
|
176
|
-
export interface SoapHttpAuthConfig<TContext = unknown, TUser = unknown> {
|
|
174
|
+
export interface SoapHttpAuthConfig<TContext = unknown, TUser extends Soap.AuthUser = Soap.AuthUser> {
|
|
177
175
|
local?: LocalStrategyConfig<TContext, TUser>;
|
|
176
|
+
jwt?: JwtConfig<TContext, TUser>;
|
|
178
177
|
oauth2?: {
|
|
179
178
|
[provider: string]: OAuth2StrategyConfig<TContext, TUser>;
|
|
180
179
|
};
|
|
181
180
|
apiKey?: ApiKeyStrategyConfig<TContext, TUser>;
|
|
182
181
|
basic?: BasicStrategyConfig<TContext, TUser>;
|
|
183
|
-
custom
|
|
184
|
-
[label: string]: AuthStrategy;
|
|
182
|
+
custom?: {
|
|
183
|
+
[label: string]: Soap.AuthStrategy;
|
|
185
184
|
};
|
|
186
185
|
}
|
|
187
|
-
export interface SoapSocketAuthConfig<TContext = unknown, TUser =
|
|
186
|
+
export interface SoapSocketAuthConfig<TContext = unknown, TUser extends Soap.AuthUser = Soap.AuthUser> {
|
|
188
187
|
jwt?: JwtConfig<TContext, TUser>;
|
|
189
188
|
apiKey?: ApiKeyStrategyConfig<TContext, TUser>;
|
|
190
|
-
custom
|
|
191
|
-
[provider: string]: AuthStrategy;
|
|
189
|
+
custom?: {
|
|
190
|
+
[provider: string]: Soap.AuthStrategy;
|
|
192
191
|
};
|
|
193
192
|
}
|
|
194
|
-
export interface SoapAuthConfig<TContext = unknown, TUser =
|
|
193
|
+
export interface SoapAuthConfig<TContext = unknown, TUser extends Soap.AuthUser = Soap.AuthUser> {
|
|
195
194
|
session?: SessionConfig;
|
|
196
195
|
jwt?: TokenAuthConfig<TContext>;
|
|
197
196
|
http?: SoapHttpAuthConfig<TContext, TUser>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare class ValidationError extends Error {
|
|
2
|
+
field?: string;
|
|
3
|
+
constructor(message: string, field?: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class ValidationUtils {
|
|
6
|
+
static required(value: any, fieldName: string): void;
|
|
7
|
+
static nonEmptyString(value: any, fieldName: string): string;
|
|
8
|
+
static email(value: any, fieldName: string): string;
|
|
9
|
+
static password(value: any, fieldName: string, minLength?: number): string;
|
|
10
|
+
static positiveNumber(value: any, fieldName: string): number;
|
|
11
|
+
static range(value: any, fieldName: string, min: number, max: number): number;
|
|
12
|
+
static oneOf<T>(value: any, fieldName: string, allowedValues: T[]): T;
|
|
13
|
+
static url(value: any, fieldName: string): string;
|
|
14
|
+
static jwtToken(value: any, fieldName: string): string;
|
|
15
|
+
static uuid(value: any, fieldName: string): string;
|
|
16
|
+
static object(value: any, fieldName: string): object;
|
|
17
|
+
static array(value: any, fieldName: string): any[];
|
|
18
|
+
static function(value: any, fieldName: string): Function;
|
|
19
|
+
static validateConfig(config: any, requiredFields: string[], fieldName?: string): void;
|
|
20
|
+
static pattern(value: any, fieldName: string, regex: RegExp, message?: string): string;
|
|
21
|
+
static date(value: any, fieldName: string): Date;
|
|
22
|
+
static boolean(value: any, fieldName: string): boolean;
|
|
23
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ValidationUtils = exports.ValidationError = void 0;
|
|
4
|
+
class ValidationError extends Error {
|
|
5
|
+
field;
|
|
6
|
+
constructor(message, field) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.field = field;
|
|
9
|
+
this.name = "ValidationError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.ValidationError = ValidationError;
|
|
13
|
+
class ValidationUtils {
|
|
14
|
+
static required(value, fieldName) {
|
|
15
|
+
if (value === null || value === undefined) {
|
|
16
|
+
throw new ValidationError(`${fieldName} is required`, fieldName);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
static nonEmptyString(value, fieldName) {
|
|
20
|
+
this.required(value, fieldName);
|
|
21
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
22
|
+
throw new ValidationError(`${fieldName} must be a non-empty string`, fieldName);
|
|
23
|
+
}
|
|
24
|
+
return value.trim();
|
|
25
|
+
}
|
|
26
|
+
static email(value, fieldName) {
|
|
27
|
+
const email = this.nonEmptyString(value, fieldName);
|
|
28
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
29
|
+
if (!emailRegex.test(email)) {
|
|
30
|
+
throw new ValidationError(`${fieldName} must be a valid email address`, fieldName);
|
|
31
|
+
}
|
|
32
|
+
return email;
|
|
33
|
+
}
|
|
34
|
+
static password(value, fieldName, minLength = 8) {
|
|
35
|
+
const password = this.nonEmptyString(value, fieldName);
|
|
36
|
+
if (password.length < minLength) {
|
|
37
|
+
throw new ValidationError(`${fieldName} must be at least ${minLength} characters long`, fieldName);
|
|
38
|
+
}
|
|
39
|
+
return password;
|
|
40
|
+
}
|
|
41
|
+
static positiveNumber(value, fieldName) {
|
|
42
|
+
this.required(value, fieldName);
|
|
43
|
+
const num = Number(value);
|
|
44
|
+
if (isNaN(num) || num <= 0) {
|
|
45
|
+
throw new ValidationError(`${fieldName} must be a positive number`, fieldName);
|
|
46
|
+
}
|
|
47
|
+
return num;
|
|
48
|
+
}
|
|
49
|
+
static range(value, fieldName, min, max) {
|
|
50
|
+
const num = this.positiveNumber(value, fieldName);
|
|
51
|
+
if (num < min || num > max) {
|
|
52
|
+
throw new ValidationError(`${fieldName} must be between ${min} and ${max}`, fieldName);
|
|
53
|
+
}
|
|
54
|
+
return num;
|
|
55
|
+
}
|
|
56
|
+
static oneOf(value, fieldName, allowedValues) {
|
|
57
|
+
this.required(value, fieldName);
|
|
58
|
+
if (!allowedValues.includes(value)) {
|
|
59
|
+
throw new ValidationError(`${fieldName} must be one of: ${allowedValues.join(", ")}`, fieldName);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
static url(value, fieldName) {
|
|
64
|
+
const url = this.nonEmptyString(value, fieldName);
|
|
65
|
+
try {
|
|
66
|
+
new URL(url);
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new ValidationError(`${fieldName} must be a valid URL`, fieldName);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
static jwtToken(value, fieldName) {
|
|
74
|
+
const token = this.nonEmptyString(value, fieldName);
|
|
75
|
+
const parts = token.split(".");
|
|
76
|
+
if (parts.length !== 3) {
|
|
77
|
+
throw new ValidationError(`${fieldName} must be a valid JWT token`, fieldName);
|
|
78
|
+
}
|
|
79
|
+
return token;
|
|
80
|
+
}
|
|
81
|
+
static uuid(value, fieldName) {
|
|
82
|
+
const uuid = this.nonEmptyString(value, fieldName);
|
|
83
|
+
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
84
|
+
if (!uuidRegex.test(uuid)) {
|
|
85
|
+
throw new ValidationError(`${fieldName} must be a valid UUID`, fieldName);
|
|
86
|
+
}
|
|
87
|
+
return uuid;
|
|
88
|
+
}
|
|
89
|
+
static object(value, fieldName) {
|
|
90
|
+
this.required(value, fieldName);
|
|
91
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
92
|
+
throw new ValidationError(`${fieldName} must be an object`, fieldName);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
static array(value, fieldName) {
|
|
97
|
+
this.required(value, fieldName);
|
|
98
|
+
if (!Array.isArray(value)) {
|
|
99
|
+
throw new ValidationError(`${fieldName} must be an array`, fieldName);
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
static function(value, fieldName) {
|
|
104
|
+
this.required(value, fieldName);
|
|
105
|
+
if (typeof value !== "function") {
|
|
106
|
+
throw new ValidationError(`${fieldName} must be a function`, fieldName);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
static validateConfig(config, requiredFields, fieldName = "config") {
|
|
111
|
+
this.object(config, fieldName);
|
|
112
|
+
for (const field of requiredFields) {
|
|
113
|
+
this.required(config[field], `${fieldName}.${field}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
static pattern(value, fieldName, regex, message) {
|
|
117
|
+
const str = this.nonEmptyString(value, fieldName);
|
|
118
|
+
if (!regex.test(str)) {
|
|
119
|
+
throw new ValidationError(message || `${fieldName} does not match required pattern`, fieldName);
|
|
120
|
+
}
|
|
121
|
+
return str;
|
|
122
|
+
}
|
|
123
|
+
static date(value, fieldName) {
|
|
124
|
+
this.required(value, fieldName);
|
|
125
|
+
const date = new Date(value);
|
|
126
|
+
if (isNaN(date.getTime())) {
|
|
127
|
+
throw new ValidationError(`${fieldName} must be a valid date`, fieldName);
|
|
128
|
+
}
|
|
129
|
+
return date;
|
|
130
|
+
}
|
|
131
|
+
static boolean(value, fieldName) {
|
|
132
|
+
this.required(value, fieldName);
|
|
133
|
+
if (typeof value !== "boolean") {
|
|
134
|
+
throw new ValidationError(`${fieldName} must be a boolean`, fieldName);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
exports.ValidationUtils = ValidationUtils;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soapjs/soap-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"homepage": "https://docs.soapjs.com",
|
|
6
6
|
"repository": "https://github.com/soapjs/soap-auth",
|
|
@@ -15,20 +15,21 @@
|
|
|
15
15
|
"prepublish": "npm run clean && tsc --project tsconfig.build.json"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@soapjs/soap": "^0.
|
|
18
|
+
"@soapjs/soap": "^0.12.0",
|
|
19
19
|
"@types/jest": "^27.0.3",
|
|
20
20
|
"jest": "^27.4.5",
|
|
21
21
|
"ts-jest": "^27.1.3",
|
|
22
22
|
"typescript": "^4.8.2"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@soapjs/soap": ">=0.
|
|
25
|
+
"@soapjs/soap": ">=0.12.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
26
29
|
},
|
|
27
30
|
"dependencies": {
|
|
28
|
-
"
|
|
29
|
-
"bcrypt": "^5.1.1",
|
|
31
|
+
"bcrypt": "^6.0.0",
|
|
30
32
|
"jsonwebtoken": "^9.0.2",
|
|
31
|
-
"jwks-rsa": "^3.1.0"
|
|
32
|
-
"uuid": "^9.0.1"
|
|
33
|
+
"jwks-rsa": "^3.1.0"
|
|
33
34
|
}
|
|
34
35
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const globals_1 = require("@jest/globals");
|
|
4
|
-
const soap_auth_1 = require("../soap-auth");
|
|
5
|
-
describe("SoapAuth", () => {
|
|
6
|
-
let soapAuth;
|
|
7
|
-
const mockLogger = { error: globals_1.jest.fn(), info: globals_1.jest.fn() };
|
|
8
|
-
const mockStrategy = {
|
|
9
|
-
authenticate: globals_1.jest.fn(),
|
|
10
|
-
init: globals_1.jest.fn(),
|
|
11
|
-
logout: globals_1.jest.fn(),
|
|
12
|
-
};
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
globals_1.jest.clearAllMocks();
|
|
15
|
-
soapAuth = new soap_auth_1.SoapAuth({ logger: mockLogger });
|
|
16
|
-
});
|
|
17
|
-
test("addStrategy should add a valid strategy", () => {
|
|
18
|
-
expect(() => soapAuth.addStrategy(mockStrategy, "jwt", "http")).not.toThrow();
|
|
19
|
-
expect(soapAuth.hasStrategy("jwt", "http")).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
test("addStrategy should throw error if strategy is invalid", () => {
|
|
22
|
-
expect(() => soapAuth.addStrategy({}, "invalid", "http")).toThrow("Invalid authentication strategy: does not implement required methods.");
|
|
23
|
-
});
|
|
24
|
-
test("removeStrategy should remove an existing strategy", () => {
|
|
25
|
-
soapAuth.addStrategy(mockStrategy, "jwt", "http");
|
|
26
|
-
expect(soapAuth.hasStrategy("jwt", "http")).toBe(true);
|
|
27
|
-
soapAuth.removeStrategy("jwt", "http");
|
|
28
|
-
expect(soapAuth.hasStrategy("jwt", "http")).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
test("getStrategy should return an existing strategy", () => {
|
|
31
|
-
soapAuth.addStrategy(mockStrategy, "jwt", "http");
|
|
32
|
-
expect(soapAuth.getStrategy("jwt", "http")).toBe(mockStrategy);
|
|
33
|
-
});
|
|
34
|
-
test("getStrategy should throw an error if strategy does not exist", () => {
|
|
35
|
-
expect(() => soapAuth.getStrategy("nonexistent", "http")).toThrow('Authentication strategy "nonexistent" not found.');
|
|
36
|
-
});
|
|
37
|
-
test("listStrategies should return all registered strategy names", () => {
|
|
38
|
-
soapAuth.addStrategy(mockStrategy, "jwt", "http");
|
|
39
|
-
soapAuth.addStrategy(mockStrategy, "oauth", "http");
|
|
40
|
-
expect(soapAuth.listStrategies("http")).toEqual(["jwt", "oauth"]);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const errors_1 = require("../../errors");
|
|
4
|
-
const account_lock_service_1 = require("../account-lock.service");
|
|
5
|
-
const mockConfig = {
|
|
6
|
-
isAccountLocked: jest.fn(),
|
|
7
|
-
lockAccount: jest.fn(),
|
|
8
|
-
hasAccountLockExpired: jest.fn(),
|
|
9
|
-
removeAccountLock: jest.fn(),
|
|
10
|
-
logFailedAttempt: jest.fn(),
|
|
11
|
-
notifyOnLockout: jest.fn(),
|
|
12
|
-
};
|
|
13
|
-
const mockLogger = {
|
|
14
|
-
error: jest.fn(),
|
|
15
|
-
};
|
|
16
|
-
describe("AccountLockService", () => {
|
|
17
|
-
let service;
|
|
18
|
-
const mockAccount = { id: "123" };
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
jest.clearAllMocks();
|
|
21
|
-
service = new account_lock_service_1.AccountLockService(mockConfig, mockLogger);
|
|
22
|
-
});
|
|
23
|
-
it("isAccountLocked throws error if account is locked", async () => {
|
|
24
|
-
mockConfig.isAccountLocked.mockResolvedValue(true);
|
|
25
|
-
await expect(service.isAccountLocked(mockAccount)).rejects.toThrow(errors_1.AccountLockedError);
|
|
26
|
-
});
|
|
27
|
-
it("isAccountLocked returns false if account is not locked", async () => {
|
|
28
|
-
mockConfig.isAccountLocked.mockResolvedValue(false);
|
|
29
|
-
await expect(service.isAccountLocked(mockAccount)).resolves.toBe(false);
|
|
30
|
-
});
|
|
31
|
-
it("lockAccount locks the account and notifies if enabled", async () => {
|
|
32
|
-
mockConfig.notifyOnLockout = jest.fn();
|
|
33
|
-
await service.lockAccount(mockAccount);
|
|
34
|
-
expect(mockConfig.lockAccount).toHaveBeenCalledWith(mockAccount);
|
|
35
|
-
expect(mockConfig.notifyOnLockout).toHaveBeenCalledWith(mockAccount);
|
|
36
|
-
});
|
|
37
|
-
it("lockAccount does not notify if notifyOnLockout is not defined", async () => {
|
|
38
|
-
mockConfig.notifyOnLockout = undefined;
|
|
39
|
-
await service.lockAccount(mockAccount);
|
|
40
|
-
expect(mockConfig.lockAccount).toHaveBeenCalledWith(mockAccount);
|
|
41
|
-
});
|
|
42
|
-
it("hasAccountLockExpired returns expected value", async () => {
|
|
43
|
-
mockConfig.hasAccountLockExpired.mockResolvedValue(true);
|
|
44
|
-
await expect(service.hasAccountLockExpired(mockAccount)).resolves.toBe(true);
|
|
45
|
-
});
|
|
46
|
-
it("removeAccountLock calls the correct method", async () => {
|
|
47
|
-
await service.removeAccountLock(mockAccount);
|
|
48
|
-
expect(mockConfig.removeAccountLock).toHaveBeenCalledWith(mockAccount);
|
|
49
|
-
});
|
|
50
|
-
it("logFailedAttempt logs action and handles error if logging fails", async () => {
|
|
51
|
-
mockConfig.logFailedAttempt.mockRejectedValue(new Error("Logging failed"));
|
|
52
|
-
await service.logFailedAttempt("LOGIN_ATTEMPT", mockAccount, {});
|
|
53
|
-
expect(mockLogger.error).toHaveBeenCalled();
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|