@drax/identity-back 3.21.0 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/PasswordPolicyConfig.js +1 -0
- package/dist/controllers/UserController.js +1 -6
- package/dist/policies/defaultPasswordPolicy.js +1 -0
- package/dist/schemas/PasswordPolicySchema.js +1 -1
- package/dist/services/PasswordPolicyService.js +3 -3
- package/dist/setup/LoadIdentityConfigFromEnv.js +1 -0
- package/dist/utils/PasswordPolicySchemaFactory.js +1 -2
- package/dist/utils/getPasswordEnvPolicy.js +1 -0
- package/package.json +4 -4
- package/src/config/PasswordPolicyConfig.ts +1 -0
- package/src/controllers/UserController.ts +2 -7
- package/src/policies/defaultPasswordPolicy.ts +2 -1
- package/src/schemas/PasswordPolicySchema.ts +1 -1
- package/src/services/PasswordPolicyService.ts +3 -3
- package/src/setup/LoadIdentityConfigFromEnv.ts +1 -0
- package/src/utils/PasswordPolicySchemaFactory.ts +1 -2
- package/src/utils/getPasswordEnvPolicy.ts +1 -0
- package/test/endpoints/password-policy-route.test.ts +3 -2
- package/test/security/password-policy-resolver.test.ts +16 -2
- package/test/security/password-policy-schema-factory.test.ts +12 -2
- package/test/services/user-service.test.ts +2 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/types/config/PasswordPolicyConfig.d.ts +1 -0
- package/types/config/PasswordPolicyConfig.d.ts.map +1 -1
- package/types/controllers/UserController.d.ts +2 -13
- package/types/controllers/UserController.d.ts.map +1 -1
- package/types/policies/defaultPasswordPolicy.d.ts +1 -1
- package/types/policies/defaultPasswordPolicy.d.ts.map +1 -1
- package/types/schemas/PasswordPolicySchema.d.ts +2 -2
- package/types/services/PasswordPolicyService.d.ts.map +1 -1
- package/types/setup/LoadIdentityConfigFromEnv.d.ts.map +1 -1
- package/types/utils/PasswordPolicySchemaFactory.d.ts.map +1 -1
- package/types/utils/getPasswordEnvPolicy.d.ts.map +1 -1
- package/src/constants/PasswordSpecialChars.ts +0 -5
|
@@ -6,6 +6,7 @@ var PasswordPolicyConfig;
|
|
|
6
6
|
PasswordPolicyConfig["RequireLowercase"] = "PASSWORD_POLICY_REQUIRE_LOWERCASE";
|
|
7
7
|
PasswordPolicyConfig["RequireNumber"] = "PASSWORD_POLICY_REQUIRE_NUMBER";
|
|
8
8
|
PasswordPolicyConfig["RequireSpecialChar"] = "PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR";
|
|
9
|
+
PasswordPolicyConfig["AllowedSpecialChars"] = "PASSWORD_POLICY_ALLOWED_SPECIAL_CHARS";
|
|
9
10
|
PasswordPolicyConfig["DisallowSpaces"] = "PASSWORD_POLICY_DISALLOW_SPACES";
|
|
10
11
|
PasswordPolicyConfig["PreventReuse"] = "PASSWORD_POLICY_PREVENT_REUSE";
|
|
11
12
|
PasswordPolicyConfig["ExpirationDays"] = "PASSWORD_POLICY_EXPIRATION_DAYS";
|
|
@@ -10,7 +10,6 @@ import { IdentityConfig } from "../config/IdentityConfig.js";
|
|
|
10
10
|
import UserEmailService from "../services/UserEmailService.js";
|
|
11
11
|
import TenantServiceFactory from "../factory/TenantServiceFactory.js";
|
|
12
12
|
import PasswordPolicyServiceFactory from "../factory/PasswordPolicyServiceFactory.js";
|
|
13
|
-
import { allowedSpecialChars } from "../constants/PasswordSpecialChars.js";
|
|
14
13
|
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
15
14
|
const AVATAR_DIR = DraxConfig.getOrLoad(IdentityConfig.AvatarDir) || 'avatar';
|
|
16
15
|
const BASE_URL = DraxConfig.getOrLoad(CommonConfig.BaseUrl) ? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, '') : '';
|
|
@@ -77,11 +76,7 @@ class UserController extends AbstractFastifyController {
|
|
|
77
76
|
async passwordPolicy(request, reply) {
|
|
78
77
|
try {
|
|
79
78
|
const passwordPolicyService = PasswordPolicyServiceFactory();
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
...policy,
|
|
83
|
-
allowedSpecialChars
|
|
84
|
-
};
|
|
79
|
+
return await passwordPolicyService.getFinalPolicy();
|
|
85
80
|
}
|
|
86
81
|
catch (e) {
|
|
87
82
|
this.handleError(e, reply);
|
|
@@ -6,7 +6,7 @@ const PasswordPolicySchemaBase = z.object({
|
|
|
6
6
|
requireLowercase: z.boolean(),
|
|
7
7
|
requireNumber: z.boolean(),
|
|
8
8
|
requireSpecialChar: z.boolean(),
|
|
9
|
-
allowedSpecialChars: z.string()
|
|
9
|
+
allowedSpecialChars: z.string(),
|
|
10
10
|
disallowSpaces: z.boolean(),
|
|
11
11
|
preventReuse: z.number().int().min(0),
|
|
12
12
|
expirationDays: z.number().int().min(1).nullable(),
|
|
@@ -3,7 +3,6 @@ import { ZodError } from "zod";
|
|
|
3
3
|
import { ValidationError, ZodErrorToValidationError } from "@drax/common-back";
|
|
4
4
|
import AuthUtils from "../utils/AuthUtils.js";
|
|
5
5
|
import PasswordPolicySchemaFactory from "../utils/PasswordPolicySchemaFactory.js";
|
|
6
|
-
import { allowedSpecialChars } from "../constants/PasswordSpecialChars.js";
|
|
7
6
|
class PasswordPolicyService {
|
|
8
7
|
constructor(resolver, userRepository, userPasswordHistoryService) {
|
|
9
8
|
this.resolver = resolver;
|
|
@@ -19,8 +18,9 @@ class PasswordPolicyService {
|
|
|
19
18
|
}
|
|
20
19
|
async validatePassword(password, options) {
|
|
21
20
|
const field = options?.field || "password";
|
|
21
|
+
const policy = await this.getFinalPolicy();
|
|
22
22
|
try {
|
|
23
|
-
const schema =
|
|
23
|
+
const schema = PasswordPolicySchemaFactory.create(policy);
|
|
24
24
|
await schema.parseAsync(password);
|
|
25
25
|
}
|
|
26
26
|
catch (e) {
|
|
@@ -43,7 +43,7 @@ class PasswordPolicyService {
|
|
|
43
43
|
const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
44
44
|
const lowercaseChars = "abcdefghijkmnopqrstuvwxyz";
|
|
45
45
|
const numericChars = "0123456789";
|
|
46
|
-
const specialChars = allowedSpecialChars;
|
|
46
|
+
const specialChars = policy.allowedSpecialChars;
|
|
47
47
|
const fallbackSpecial = policy.disallowSpaces ? "!" : " ";
|
|
48
48
|
const combinedChars = [
|
|
49
49
|
uppercaseChars,
|
|
@@ -14,6 +14,7 @@ function LoadIdentityConfigFromEnv() {
|
|
|
14
14
|
DraxConfig.set(PasswordPolicyConfig.RequireLowercase, process.env[PasswordPolicyConfig.RequireLowercase]);
|
|
15
15
|
DraxConfig.set(PasswordPolicyConfig.RequireNumber, process.env[PasswordPolicyConfig.RequireNumber]);
|
|
16
16
|
DraxConfig.set(PasswordPolicyConfig.RequireSpecialChar, process.env[PasswordPolicyConfig.RequireSpecialChar]);
|
|
17
|
+
DraxConfig.set(PasswordPolicyConfig.AllowedSpecialChars, process.env[PasswordPolicyConfig.AllowedSpecialChars]);
|
|
17
18
|
DraxConfig.set(PasswordPolicyConfig.DisallowSpaces, process.env[PasswordPolicyConfig.DisallowSpaces]);
|
|
18
19
|
DraxConfig.set(PasswordPolicyConfig.PreventReuse, process.env[PasswordPolicyConfig.PreventReuse]);
|
|
19
20
|
DraxConfig.set(PasswordPolicyConfig.ExpirationDays, process.env[PasswordPolicyConfig.ExpirationDays]);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
|
-
import { allowedSpecialChars } from "../constants/PasswordSpecialChars.js";
|
|
3
2
|
class PasswordPolicySchemaFactory {
|
|
4
3
|
static create(policy) {
|
|
5
4
|
const cacheKey = JSON.stringify(policy);
|
|
@@ -21,7 +20,7 @@ class PasswordPolicySchemaFactory {
|
|
|
21
20
|
schema = schema.regex(/[0-9]/, "validation.password.requireNumber");
|
|
22
21
|
}
|
|
23
22
|
if (policy.requireSpecialChar) {
|
|
24
|
-
schema = schema.refine((value) => [...value].some((char) => allowedSpecialChars.includes(char)), "validation.password.requireSpecialChar");
|
|
23
|
+
schema = schema.refine((value) => [...value].some((char) => policy.allowedSpecialChars.includes(char)), "validation.password.requireSpecialChar");
|
|
25
24
|
}
|
|
26
25
|
if (policy.disallowSpaces) {
|
|
27
26
|
schema = schema.refine((value) => !/\s/.test(value), {
|
|
@@ -9,6 +9,7 @@ function getPasswordEnvPolicy() {
|
|
|
9
9
|
requireLowercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireLowercase, "boolean"),
|
|
10
10
|
requireNumber: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireNumber, "boolean"),
|
|
11
11
|
requireSpecialChar: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireSpecialChar, "boolean"),
|
|
12
|
+
allowedSpecialChars: DraxConfig.getOrLoad(PasswordPolicyConfig.AllowedSpecialChars),
|
|
12
13
|
disallowSpaces: DraxConfig.getOrLoad(PasswordPolicyConfig.DisallowSpaces, "boolean"),
|
|
13
14
|
preventReuse: DraxConfig.getOrLoad(PasswordPolicyConfig.PreventReuse, "number"),
|
|
14
15
|
expirationDays: DraxConfig.getOrLoad(PasswordPolicyConfig.ExpirationDays, "number")
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.23.0",
|
|
7
7
|
"description": "Identity module for user management, authentication and authorization.",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "types/index.d.ts",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"license": "ISC",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@drax/common-back": "^3.18.0",
|
|
32
|
-
"@drax/crud-back": "^3.
|
|
32
|
+
"@drax/crud-back": "^3.23.0",
|
|
33
33
|
"@drax/crud-share": "^3.21.0",
|
|
34
34
|
"@drax/email-back": "^3.1.0",
|
|
35
|
-
"@drax/identity-share": "^3.
|
|
35
|
+
"@drax/identity-share": "^3.23.0",
|
|
36
36
|
"bcryptjs": "^2.4.3",
|
|
37
37
|
"graphql": "^16.8.2",
|
|
38
38
|
"jsonwebtoken": "^9.0.2"
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
"debug": "0"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "5768e7721a2eaa06cf4bcf97811897613a8c9470"
|
|
67
67
|
}
|
|
@@ -5,6 +5,7 @@ enum PasswordPolicyConfig {
|
|
|
5
5
|
RequireLowercase = "PASSWORD_POLICY_REQUIRE_LOWERCASE",
|
|
6
6
|
RequireNumber = "PASSWORD_POLICY_REQUIRE_NUMBER",
|
|
7
7
|
RequireSpecialChar = "PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR",
|
|
8
|
+
AllowedSpecialChars = "PASSWORD_POLICY_ALLOWED_SPECIAL_CHARS",
|
|
8
9
|
DisallowSpaces = "PASSWORD_POLICY_DISALLOW_SPACES",
|
|
9
10
|
PreventReuse = "PASSWORD_POLICY_PREVENT_REUSE",
|
|
10
11
|
ExpirationDays = "PASSWORD_POLICY_EXPIRATION_DAYS",
|
|
@@ -19,9 +19,8 @@ import {IdentityConfig} from "../config/IdentityConfig.js";
|
|
|
19
19
|
import UserEmailService from "../services/UserEmailService.js";
|
|
20
20
|
import {IDraxCrudEvent, IDraxFieldFilter} from "@drax/crud-share";
|
|
21
21
|
import TenantServiceFactory from "../factory/TenantServiceFactory.js";
|
|
22
|
-
import {CustomRequest} from "@drax/crud-back
|
|
22
|
+
import {CustomRequest} from "@drax/crud-back";
|
|
23
23
|
import PasswordPolicyServiceFactory from "../factory/PasswordPolicyServiceFactory.js";
|
|
24
|
-
import {allowedSpecialChars} from "../constants/PasswordSpecialChars.js";
|
|
25
24
|
|
|
26
25
|
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
27
26
|
const AVATAR_DIR = DraxConfig.getOrLoad(IdentityConfig.AvatarDir) || 'avatar';
|
|
@@ -96,11 +95,7 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
96
95
|
async passwordPolicy(request, reply) {
|
|
97
96
|
try {
|
|
98
97
|
const passwordPolicyService = PasswordPolicyServiceFactory()
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
...policy,
|
|
102
|
-
allowedSpecialChars
|
|
103
|
-
}
|
|
98
|
+
return await passwordPolicyService.getFinalPolicy()
|
|
104
99
|
} catch (e) {
|
|
105
100
|
this.handleError(e, reply)
|
|
106
101
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {IPasswordPolicy} from "@drax/identity-share
|
|
1
|
+
import type {IPasswordPolicy} from "@drax/identity-share";
|
|
2
2
|
|
|
3
3
|
const defaultPasswordPolicy: IPasswordPolicy = {
|
|
4
4
|
minLength: 6,
|
|
@@ -7,6 +7,7 @@ const defaultPasswordPolicy: IPasswordPolicy = {
|
|
|
7
7
|
requireLowercase: false,
|
|
8
8
|
requireNumber: false,
|
|
9
9
|
requireSpecialChar: false,
|
|
10
|
+
allowedSpecialChars: "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~",
|
|
10
11
|
disallowSpaces: true,
|
|
11
12
|
preventReuse: 3,
|
|
12
13
|
expirationDays: null
|
|
@@ -7,7 +7,7 @@ const PasswordPolicySchemaBase = z.object({
|
|
|
7
7
|
requireLowercase: z.boolean(),
|
|
8
8
|
requireNumber: z.boolean(),
|
|
9
9
|
requireSpecialChar: z.boolean(),
|
|
10
|
-
allowedSpecialChars: z.string()
|
|
10
|
+
allowedSpecialChars: z.string(),
|
|
11
11
|
disallowSpaces: z.boolean(),
|
|
12
12
|
preventReuse: z.number().int().min(0),
|
|
13
13
|
expirationDays: z.number().int().min(1).nullable(),
|
|
@@ -9,7 +9,6 @@ import type {IUserRepository} from "../interfaces/IUserRepository";
|
|
|
9
9
|
import AuthUtils from "../utils/AuthUtils.js";
|
|
10
10
|
import PasswordPolicyResolver from "../resolver/PasswordPolicyResolver.js";
|
|
11
11
|
import PasswordPolicySchemaFactory from "../utils/PasswordPolicySchemaFactory.js";
|
|
12
|
-
import {allowedSpecialChars} from "../constants/PasswordSpecialChars.js";
|
|
13
12
|
|
|
14
13
|
interface IValidatePasswordOptions {
|
|
15
14
|
field?: string
|
|
@@ -36,8 +35,9 @@ class PasswordPolicyService {
|
|
|
36
35
|
|
|
37
36
|
async validatePassword(password: string, options?: IValidatePasswordOptions): Promise<void> {
|
|
38
37
|
const field = options?.field || "password"
|
|
38
|
+
const policy = await this.getFinalPolicy()
|
|
39
39
|
try {
|
|
40
|
-
const schema =
|
|
40
|
+
const schema = PasswordPolicySchemaFactory.create(policy)
|
|
41
41
|
await schema.parseAsync(password)
|
|
42
42
|
} catch (e) {
|
|
43
43
|
if (e instanceof ZodError) {
|
|
@@ -61,7 +61,7 @@ class PasswordPolicyService {
|
|
|
61
61
|
const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
62
62
|
const lowercaseChars = "abcdefghijkmnopqrstuvwxyz"
|
|
63
63
|
const numericChars = "0123456789"
|
|
64
|
-
const specialChars = allowedSpecialChars
|
|
64
|
+
const specialChars = policy.allowedSpecialChars
|
|
65
65
|
const fallbackSpecial = policy.disallowSpaces ? "!" : " "
|
|
66
66
|
const combinedChars = [
|
|
67
67
|
uppercaseChars,
|
|
@@ -18,6 +18,7 @@ function LoadIdentityConfigFromEnv() {
|
|
|
18
18
|
DraxConfig.set(PasswordPolicyConfig.RequireLowercase, process.env[PasswordPolicyConfig.RequireLowercase])
|
|
19
19
|
DraxConfig.set(PasswordPolicyConfig.RequireNumber, process.env[PasswordPolicyConfig.RequireNumber])
|
|
20
20
|
DraxConfig.set(PasswordPolicyConfig.RequireSpecialChar, process.env[PasswordPolicyConfig.RequireSpecialChar])
|
|
21
|
+
DraxConfig.set(PasswordPolicyConfig.AllowedSpecialChars, process.env[PasswordPolicyConfig.AllowedSpecialChars])
|
|
21
22
|
DraxConfig.set(PasswordPolicyConfig.DisallowSpaces, process.env[PasswordPolicyConfig.DisallowSpaces])
|
|
22
23
|
DraxConfig.set(PasswordPolicyConfig.PreventReuse, process.env[PasswordPolicyConfig.PreventReuse])
|
|
23
24
|
DraxConfig.set(PasswordPolicyConfig.ExpirationDays, process.env[PasswordPolicyConfig.ExpirationDays])
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import type {IPasswordPolicy} from "@drax/identity-share";
|
|
3
|
-
import {allowedSpecialChars} from "../constants/PasswordSpecialChars.js";
|
|
4
3
|
|
|
5
4
|
class PasswordPolicySchemaFactory {
|
|
6
5
|
private static cache = new Map<string, z.ZodType<string>>()
|
|
@@ -31,7 +30,7 @@ class PasswordPolicySchemaFactory {
|
|
|
31
30
|
|
|
32
31
|
if (policy.requireSpecialChar) {
|
|
33
32
|
schema = schema.refine(
|
|
34
|
-
(value) => [...value].some((char) => allowedSpecialChars.includes(char)),
|
|
33
|
+
(value) => [...value].some((char) => policy.allowedSpecialChars.includes(char)),
|
|
35
34
|
"validation.password.requireSpecialChar"
|
|
36
35
|
)
|
|
37
36
|
}
|
|
@@ -11,6 +11,7 @@ function getPasswordEnvPolicy(): Partial<IPasswordPolicy> {
|
|
|
11
11
|
requireLowercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireLowercase, "boolean"),
|
|
12
12
|
requireNumber: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireNumber, "boolean"),
|
|
13
13
|
requireSpecialChar: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireSpecialChar, "boolean"),
|
|
14
|
+
allowedSpecialChars: DraxConfig.getOrLoad(PasswordPolicyConfig.AllowedSpecialChars),
|
|
14
15
|
disallowSpaces: DraxConfig.getOrLoad(PasswordPolicyConfig.DisallowSpaces, "boolean"),
|
|
15
16
|
preventReuse: DraxConfig.getOrLoad(PasswordPolicyConfig.PreventReuse, "number"),
|
|
16
17
|
expirationDays: DraxConfig.getOrLoad(PasswordPolicyConfig.ExpirationDays, "number")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {afterAll, beforeAll, describe, expect, it} from "vitest";
|
|
2
2
|
import {LoadIdentityConfigFromEnv} from "../../src/setup/LoadIdentityConfigFromEnv.js";
|
|
3
3
|
import {TestSetup} from "../setup/TestSetup";
|
|
4
|
-
import {allowedSpecialChars} from "../../src/constants/PasswordSpecialChars.js";
|
|
5
4
|
|
|
6
5
|
describe("Password Policy Route Test", () => {
|
|
7
6
|
const testSetup = new TestSetup("sqlite")
|
|
@@ -10,12 +9,14 @@ describe("Password Policy Route Test", () => {
|
|
|
10
9
|
await testSetup.setup()
|
|
11
10
|
process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR = "true"
|
|
12
11
|
process.env.PASSWORD_POLICY_MIN_LENGTH = "18"
|
|
12
|
+
process.env.PASSWORD_POLICY_ALLOWED_SPECIAL_CHARS = "@%"
|
|
13
13
|
LoadIdentityConfigFromEnv()
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
afterAll(async () => {
|
|
17
17
|
delete process.env.PASSWORD_POLICY_MIN_LENGTH
|
|
18
18
|
delete process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR
|
|
19
|
+
delete process.env.PASSWORD_POLICY_ALLOWED_SPECIAL_CHARS
|
|
19
20
|
await testSetup.dropAndClose()
|
|
20
21
|
})
|
|
21
22
|
|
|
@@ -29,7 +30,7 @@ describe("Password Policy Route Test", () => {
|
|
|
29
30
|
const body = response.json()
|
|
30
31
|
expect(body.minLength).toBe(18)
|
|
31
32
|
expect(body.requireSpecialChar).toBe(true)
|
|
32
|
-
expect(body.allowedSpecialChars).toBe(
|
|
33
|
+
expect(body.allowedSpecialChars).toBe("@%")
|
|
33
34
|
expect(body.requireUppercase).toBe(false)
|
|
34
35
|
})
|
|
35
36
|
})
|
|
@@ -8,18 +8,21 @@ describe("PasswordPolicyResolver", () => {
|
|
|
8
8
|
delete process.env.PASSWORD_POLICY_MIN_LENGTH
|
|
9
9
|
delete process.env.PASSWORD_POLICY_MAX_LENGTH
|
|
10
10
|
delete process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR
|
|
11
|
+
delete process.env.PASSWORD_POLICY_ALLOWED_SPECIAL_CHARS
|
|
11
12
|
DraxConfig.set(PasswordPolicyConfig.MinLength, undefined)
|
|
12
13
|
DraxConfig.set(PasswordPolicyConfig.MaxLength, undefined)
|
|
13
14
|
DraxConfig.set(PasswordPolicyConfig.RequireSpecialChar, undefined)
|
|
15
|
+
DraxConfig.set(PasswordPolicyConfig.AllowedSpecialChars, undefined)
|
|
14
16
|
})
|
|
15
17
|
|
|
16
18
|
it("uses default policy when there are no overrides", async () => {
|
|
17
19
|
const resolver = new PasswordPolicyResolver()
|
|
18
20
|
const policy = await resolver.resolve()
|
|
19
21
|
|
|
20
|
-
expect(policy.minLength).toBe(
|
|
22
|
+
expect(policy.minLength).toBe(6)
|
|
21
23
|
expect(policy.maxLength).toBe(64)
|
|
22
|
-
expect(policy.requireUppercase).toBe(
|
|
24
|
+
expect(policy.requireUppercase).toBe(false)
|
|
25
|
+
expect(policy.allowedSpecialChars).toBe("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~")
|
|
23
26
|
})
|
|
24
27
|
|
|
25
28
|
it("project policy overrides default policy", async () => {
|
|
@@ -32,15 +35,26 @@ describe("PasswordPolicyResolver", () => {
|
|
|
32
35
|
expect(policy.requireUppercase).toBe(false)
|
|
33
36
|
})
|
|
34
37
|
|
|
38
|
+
it("project policy can override allowed special chars", async () => {
|
|
39
|
+
const resolver = new PasswordPolicyResolver()
|
|
40
|
+
resolver.setProjectPolicy({allowedSpecialChars: "%@"})
|
|
41
|
+
const policy = await resolver.resolve()
|
|
42
|
+
|
|
43
|
+
expect(policy.allowedSpecialChars).toBe("%@")
|
|
44
|
+
})
|
|
45
|
+
|
|
35
46
|
it("env policy overrides project policy", async () => {
|
|
36
47
|
process.env.PASSWORD_POLICY_MIN_LENGTH = "16"
|
|
37
48
|
process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR = "false"
|
|
49
|
+
process.env.PASSWORD_POLICY_ALLOWED_SPECIAL_CHARS = "%^"
|
|
38
50
|
|
|
39
51
|
const resolver = new PasswordPolicyResolver()
|
|
52
|
+
resolver.setProjectPolicy({allowedSpecialChars: "@@"})
|
|
40
53
|
const policy = await resolver.resolve()
|
|
41
54
|
|
|
42
55
|
expect(policy.minLength).toBe(16)
|
|
43
56
|
expect(policy.requireSpecialChar).toBe(false)
|
|
57
|
+
expect(policy.allowedSpecialChars).toBe("%^")
|
|
44
58
|
})
|
|
45
59
|
|
|
46
60
|
it("ignores undefined env overrides", async () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {describe, expect, it} from "vitest";
|
|
2
2
|
import PasswordPolicySchemaFactory from "../../src/utils/PasswordPolicySchemaFactory.js";
|
|
3
3
|
import {defaultPasswordPolicy} from "../../src/policies/defaultPasswordPolicy.js";
|
|
4
|
-
import {allowedSpecialChars} from "../../src/constants/PasswordSpecialChars.js";
|
|
5
4
|
|
|
6
5
|
describe("PasswordPolicySchemaFactory", () => {
|
|
7
6
|
it("validates minLength", async () => {
|
|
@@ -36,7 +35,7 @@ describe("PasswordPolicySchemaFactory", () => {
|
|
|
36
35
|
|
|
37
36
|
it("accepts allowed special chars", async () => {
|
|
38
37
|
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireSpecialChar: true})
|
|
39
|
-
await expect(schema.parseAsync(`Password1${allowedSpecialChars.at(-1)}`)).resolves.toBeDefined()
|
|
38
|
+
await expect(schema.parseAsync(`Password1${defaultPasswordPolicy.allowedSpecialChars.at(-1)}`)).resolves.toBeDefined()
|
|
40
39
|
})
|
|
41
40
|
|
|
42
41
|
it("rejects special chars outside the allowed list", async () => {
|
|
@@ -44,6 +43,17 @@ describe("PasswordPolicySchemaFactory", () => {
|
|
|
44
43
|
await expect(schema.parseAsync("Password1ñ")).rejects.toThrow()
|
|
45
44
|
})
|
|
46
45
|
|
|
46
|
+
it("uses the special chars defined in the policy", async () => {
|
|
47
|
+
const schema = PasswordPolicySchemaFactory.create({
|
|
48
|
+
...defaultPasswordPolicy,
|
|
49
|
+
requireSpecialChar: true,
|
|
50
|
+
allowedSpecialChars: "%"
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
await expect(schema.parseAsync("Password1%")).resolves.toBeDefined()
|
|
54
|
+
await expect(schema.parseAsync("Password1!")).rejects.toThrow()
|
|
55
|
+
})
|
|
56
|
+
|
|
47
57
|
it("validates disallowSpaces", async () => {
|
|
48
58
|
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
49
59
|
await expect(schema.parseAsync("Space 123A")).rejects.toThrow()
|
|
@@ -8,7 +8,6 @@ import PasswordPolicyService from "../../src/services/PasswordPolicyService";
|
|
|
8
8
|
import type {IUserPasswordHistory} from "../../src/interfaces/IUserPasswordHistory";
|
|
9
9
|
import type {IUserPasswordHistoryRepository} from "../../src/interfaces/IUserPasswordHistoryRepository";
|
|
10
10
|
import UserPasswordHistoryService from "../../src/services/UserPasswordHistoryService";
|
|
11
|
-
import {allowedSpecialChars} from "../../src/constants/PasswordSpecialChars";
|
|
12
11
|
|
|
13
12
|
class InMemoryUserRepository implements IUserRepository {
|
|
14
13
|
private items = new Map<string, IUser>()
|
|
@@ -262,7 +261,7 @@ describe("UserServiceTest", function () {
|
|
|
262
261
|
|
|
263
262
|
it("validatePassword returns allowedSpecialChars when required special char is invalid", async function () {
|
|
264
263
|
const resolver = new PasswordPolicyResolver()
|
|
265
|
-
resolver.setProjectPolicy({requireSpecialChar: true})
|
|
264
|
+
resolver.setProjectPolicy({requireSpecialChar: true, allowedSpecialChars: "%"})
|
|
266
265
|
const passwordPolicyService = new PasswordPolicyService(resolver, userRepository)
|
|
267
266
|
|
|
268
267
|
await expect(async () => {
|
|
@@ -271,7 +270,7 @@ describe("UserServiceTest", function () {
|
|
|
271
270
|
expect(err).toBeInstanceOf(ValidationError)
|
|
272
271
|
expect(err.errors[0].field).toBe('password')
|
|
273
272
|
expect(err.errors[0].reason).toBe('validation.password.requireSpecialChar')
|
|
274
|
-
expect(err.errors[0].allowedSpecialChars).toBe(
|
|
273
|
+
expect(err.errors[0].allowedSpecialChars).toBe("%")
|
|
275
274
|
return true;
|
|
276
275
|
});
|
|
277
276
|
})
|