@drax/identity-back 3.20.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/constants/PasswordSpecialChars.js +4 -0
- package/dist/policies/defaultPasswordPolicy.js +1 -0
- package/dist/schemas/PasswordPolicySchema.js +1 -0
- package/dist/services/PasswordPolicyService.js +3 -2
- package/dist/setup/LoadIdentityConfigFromEnv.js +1 -0
- package/dist/utils/PasswordPolicySchemaFactory.js +1 -1
- package/dist/utils/getPasswordEnvPolicy.js +1 -0
- package/package.json +5 -5
- package/src/config/PasswordPolicyConfig.ts +1 -0
- package/src/controllers/UserController.ts +1 -1
- package/src/policies/defaultPasswordPolicy.ts +2 -1
- package/src/schemas/PasswordPolicySchema.ts +1 -0
- package/src/services/PasswordPolicyService.ts +3 -2
- package/src/setup/LoadIdentityConfigFromEnv.ts +1 -0
- package/src/utils/PasswordPolicySchemaFactory.ts +4 -1
- package/src/utils/getPasswordEnvPolicy.ts +1 -0
- package/test/endpoints/password-policy-route.test.ts +4 -1
- package/test/security/password-policy-resolver.test.ts +16 -2
- package/test/security/password-policy-schema-factory.test.ts +25 -4
- package/test/services/user-service.test.ts +20 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/types/config/PasswordPolicyConfig.d.ts +1 -0
- package/types/config/PasswordPolicyConfig.d.ts.map +1 -1
- package/types/constants/PasswordSpecialChars.d.ts +4 -0
- package/types/constants/PasswordSpecialChars.d.ts.map +1 -0
- package/types/controllers/UserController.d.ts +1 -1
- 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 -0
- package/types/schemas/PasswordPolicySchema.d.ts.map +1 -1
- 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
|
@@ -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";
|
|
@@ -6,6 +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
10
|
disallowSpaces: z.boolean(),
|
|
10
11
|
preventReuse: z.number().int().min(0),
|
|
11
12
|
expirationDays: z.number().int().min(1).nullable(),
|
|
@@ -18,8 +18,9 @@ class PasswordPolicyService {
|
|
|
18
18
|
}
|
|
19
19
|
async validatePassword(password, options) {
|
|
20
20
|
const field = options?.field || "password";
|
|
21
|
+
const policy = await this.getFinalPolicy();
|
|
21
22
|
try {
|
|
22
|
-
const schema =
|
|
23
|
+
const schema = PasswordPolicySchemaFactory.create(policy);
|
|
23
24
|
await schema.parseAsync(password);
|
|
24
25
|
}
|
|
25
26
|
catch (e) {
|
|
@@ -42,7 +43,7 @@ class PasswordPolicyService {
|
|
|
42
43
|
const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
43
44
|
const lowercaseChars = "abcdefghijkmnopqrstuvwxyz";
|
|
44
45
|
const numericChars = "0123456789";
|
|
45
|
-
const specialChars =
|
|
46
|
+
const specialChars = policy.allowedSpecialChars;
|
|
46
47
|
const fallbackSpecial = policy.disallowSpaces ? "!" : " ";
|
|
47
48
|
const combinedChars = [
|
|
48
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]);
|
|
@@ -20,7 +20,7 @@ class PasswordPolicySchemaFactory {
|
|
|
20
20
|
schema = schema.regex(/[0-9]/, "validation.password.requireNumber");
|
|
21
21
|
}
|
|
22
22
|
if (policy.requireSpecialChar) {
|
|
23
|
-
schema = schema.
|
|
23
|
+
schema = schema.refine((value) => [...value].some((char) => policy.allowedSpecialChars.includes(char)), "validation.password.requireSpecialChar");
|
|
24
24
|
}
|
|
25
25
|
if (policy.disallowSpaces) {
|
|
26
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.
|
|
33
|
-
"@drax/crud-share": "^3.
|
|
32
|
+
"@drax/crud-back": "^3.23.0",
|
|
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,7 +19,7 @@ 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
24
|
|
|
25
25
|
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
@@ -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,6 +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
11
|
disallowSpaces: z.boolean(),
|
|
11
12
|
preventReuse: z.number().int().min(0),
|
|
12
13
|
expirationDays: z.number().int().min(1).nullable(),
|
|
@@ -35,8 +35,9 @@ class PasswordPolicyService {
|
|
|
35
35
|
|
|
36
36
|
async validatePassword(password: string, options?: IValidatePasswordOptions): Promise<void> {
|
|
37
37
|
const field = options?.field || "password"
|
|
38
|
+
const policy = await this.getFinalPolicy()
|
|
38
39
|
try {
|
|
39
|
-
const schema =
|
|
40
|
+
const schema = PasswordPolicySchemaFactory.create(policy)
|
|
40
41
|
await schema.parseAsync(password)
|
|
41
42
|
} catch (e) {
|
|
42
43
|
if (e instanceof ZodError) {
|
|
@@ -60,7 +61,7 @@ class PasswordPolicyService {
|
|
|
60
61
|
const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
61
62
|
const lowercaseChars = "abcdefghijkmnopqrstuvwxyz"
|
|
62
63
|
const numericChars = "0123456789"
|
|
63
|
-
const specialChars =
|
|
64
|
+
const specialChars = policy.allowedSpecialChars
|
|
64
65
|
const fallbackSpecial = policy.disallowSpaces ? "!" : " "
|
|
65
66
|
const combinedChars = [
|
|
66
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])
|
|
@@ -29,7 +29,10 @@ class PasswordPolicySchemaFactory {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if (policy.requireSpecialChar) {
|
|
32
|
-
schema = schema.
|
|
32
|
+
schema = schema.refine(
|
|
33
|
+
(value) => [...value].some((char) => policy.allowedSpecialChars.includes(char)),
|
|
34
|
+
"validation.password.requireSpecialChar"
|
|
35
|
+
)
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
if (policy.disallowSpaces) {
|
|
@@ -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")
|
|
@@ -9,12 +9,14 @@ describe("Password Policy Route Test", () => {
|
|
|
9
9
|
await testSetup.setup()
|
|
10
10
|
process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR = "true"
|
|
11
11
|
process.env.PASSWORD_POLICY_MIN_LENGTH = "18"
|
|
12
|
+
process.env.PASSWORD_POLICY_ALLOWED_SPECIAL_CHARS = "@%"
|
|
12
13
|
LoadIdentityConfigFromEnv()
|
|
13
14
|
})
|
|
14
15
|
|
|
15
16
|
afterAll(async () => {
|
|
16
17
|
delete process.env.PASSWORD_POLICY_MIN_LENGTH
|
|
17
18
|
delete process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR
|
|
19
|
+
delete process.env.PASSWORD_POLICY_ALLOWED_SPECIAL_CHARS
|
|
18
20
|
await testSetup.dropAndClose()
|
|
19
21
|
})
|
|
20
22
|
|
|
@@ -28,6 +30,7 @@ describe("Password Policy Route Test", () => {
|
|
|
28
30
|
const body = response.json()
|
|
29
31
|
expect(body.minLength).toBe(18)
|
|
30
32
|
expect(body.requireSpecialChar).toBe(true)
|
|
31
|
-
expect(body.
|
|
33
|
+
expect(body.allowedSpecialChars).toBe("@%")
|
|
34
|
+
expect(body.requireUppercase).toBe(false)
|
|
32
35
|
})
|
|
33
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 () => {
|
|
@@ -5,7 +5,7 @@ import {defaultPasswordPolicy} from "../../src/policies/defaultPasswordPolicy.js
|
|
|
5
5
|
describe("PasswordPolicySchemaFactory", () => {
|
|
6
6
|
it("validates minLength", async () => {
|
|
7
7
|
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
8
|
-
await expect(schema.parseAsync("
|
|
8
|
+
await expect(schema.parseAsync("Abc12")).rejects.toThrow()
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
it("validates maxLength", async () => {
|
|
@@ -14,17 +14,17 @@ describe("PasswordPolicySchemaFactory", () => {
|
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
it("validates uppercase", async () => {
|
|
17
|
-
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
17
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireUppercase: true})
|
|
18
18
|
await expect(schema.parseAsync("lowercase1")).rejects.toThrow()
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
it("validates lowercase", async () => {
|
|
22
|
-
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
22
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireLowercase: true})
|
|
23
23
|
await expect(schema.parseAsync("UPPERCASE1")).rejects.toThrow()
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
it("validates number", async () => {
|
|
27
|
-
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
27
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireNumber: true})
|
|
28
28
|
await expect(schema.parseAsync("NoNumbers")).rejects.toThrow()
|
|
29
29
|
})
|
|
30
30
|
|
|
@@ -33,6 +33,27 @@ describe("PasswordPolicySchemaFactory", () => {
|
|
|
33
33
|
await expect(schema.parseAsync("NoSpecial1")).rejects.toThrow()
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
+
it("accepts allowed special chars", async () => {
|
|
37
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireSpecialChar: true})
|
|
38
|
+
await expect(schema.parseAsync(`Password1${defaultPasswordPolicy.allowedSpecialChars.at(-1)}`)).resolves.toBeDefined()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("rejects special chars outside the allowed list", async () => {
|
|
42
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireSpecialChar: true})
|
|
43
|
+
await expect(schema.parseAsync("Password1ñ")).rejects.toThrow()
|
|
44
|
+
})
|
|
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
|
+
|
|
36
57
|
it("validates disallowSpaces", async () => {
|
|
37
58
|
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
38
59
|
await expect(schema.parseAsync("Space 123A")).rejects.toThrow()
|
|
@@ -144,7 +144,9 @@ describe("UserServiceTest", function () {
|
|
|
144
144
|
beforeEach(async () => {
|
|
145
145
|
userRepository = new InMemoryUserRepository()
|
|
146
146
|
const userPasswordHistoryService = new UserPasswordHistoryService(new InMemoryUserPasswordHistoryRepository())
|
|
147
|
-
const
|
|
147
|
+
const passwordPolicyResolver = new PasswordPolicyResolver()
|
|
148
|
+
passwordPolicyResolver.setProjectPolicy({requireUppercase: true})
|
|
149
|
+
const passwordPolicyService = new PasswordPolicyService(passwordPolicyResolver, userRepository, userPasswordHistoryService)
|
|
148
150
|
userService = new UserService(userRepository, passwordPolicyService, userPasswordHistoryService)
|
|
149
151
|
|
|
150
152
|
userAdminData = {
|
|
@@ -222,7 +224,7 @@ describe("UserServiceTest", function () {
|
|
|
222
224
|
it("changeOwnPassword rejects password that does not meet policy", async function () {
|
|
223
225
|
const userId = userAdminData._id
|
|
224
226
|
await expect(async () => {
|
|
225
|
-
await userService.changeOwnPassword(userId, "Root1234", "
|
|
227
|
+
await userService.changeOwnPassword(userId, "Root1234", "sho1A")
|
|
226
228
|
}).rejects.toSatisfy((err) => {
|
|
227
229
|
expect(err).toBeInstanceOf(ValidationError)
|
|
228
230
|
expect(err.errors[0].field).toBe('newPassword')
|
|
@@ -256,4 +258,20 @@ describe("UserServiceTest", function () {
|
|
|
256
258
|
return true;
|
|
257
259
|
});
|
|
258
260
|
})
|
|
261
|
+
|
|
262
|
+
it("validatePassword returns allowedSpecialChars when required special char is invalid", async function () {
|
|
263
|
+
const resolver = new PasswordPolicyResolver()
|
|
264
|
+
resolver.setProjectPolicy({requireSpecialChar: true, allowedSpecialChars: "%"})
|
|
265
|
+
const passwordPolicyService = new PasswordPolicyService(resolver, userRepository)
|
|
266
|
+
|
|
267
|
+
await expect(async () => {
|
|
268
|
+
await passwordPolicyService.validatePassword("Password1ñ")
|
|
269
|
+
}).rejects.toSatisfy((err) => {
|
|
270
|
+
expect(err).toBeInstanceOf(ValidationError)
|
|
271
|
+
expect(err.errors[0].field).toBe('password')
|
|
272
|
+
expect(err.errors[0].reason).toBe('validation.password.requireSpecialChar')
|
|
273
|
+
expect(err.errors[0].allowedSpecialChars).toBe("%")
|
|
274
|
+
return true;
|
|
275
|
+
});
|
|
276
|
+
})
|
|
259
277
|
})
|