@drax/identity-back 3.19.0 → 3.21.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/constants/PasswordSpecialChars.js +4 -0
- package/dist/controllers/UserController.js +6 -1
- package/dist/schemas/PasswordPolicySchema.js +1 -0
- package/dist/services/PasswordPolicyService.js +2 -1
- package/dist/utils/PasswordPolicySchemaFactory.js +2 -1
- package/package.json +5 -5
- package/src/constants/PasswordSpecialChars.ts +5 -0
- package/src/controllers/UserController.ts +6 -1
- package/src/schemas/PasswordPolicySchema.ts +1 -0
- package/src/services/PasswordPolicyService.ts +2 -1
- package/src/utils/PasswordPolicySchemaFactory.ts +5 -1
- package/test/endpoints/password-policy-route.test.ts +3 -1
- package/test/security/password-policy-schema-factory.test.ts +15 -4
- package/test/services/user-service.test.ts +21 -2
- package/tsconfig.tsbuildinfo +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 +12 -1
- package/types/controllers/UserController.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/utils/PasswordPolicySchemaFactory.d.ts.map +1 -1
|
@@ -10,6 +10,7 @@ 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";
|
|
13
14
|
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
14
15
|
const AVATAR_DIR = DraxConfig.getOrLoad(IdentityConfig.AvatarDir) || 'avatar';
|
|
15
16
|
const BASE_URL = DraxConfig.getOrLoad(CommonConfig.BaseUrl) ? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, '') : '';
|
|
@@ -76,7 +77,11 @@ class UserController extends AbstractFastifyController {
|
|
|
76
77
|
async passwordPolicy(request, reply) {
|
|
77
78
|
try {
|
|
78
79
|
const passwordPolicyService = PasswordPolicyServiceFactory();
|
|
79
|
-
|
|
80
|
+
const policy = await passwordPolicyService.getFinalPolicy();
|
|
81
|
+
return {
|
|
82
|
+
...policy,
|
|
83
|
+
allowedSpecialChars
|
|
84
|
+
};
|
|
80
85
|
}
|
|
81
86
|
catch (e) {
|
|
82
87
|
this.handleError(e, reply);
|
|
@@ -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().optional(),
|
|
9
10
|
disallowSpaces: z.boolean(),
|
|
10
11
|
preventReuse: z.number().int().min(0),
|
|
11
12
|
expirationDays: z.number().int().min(1).nullable(),
|
|
@@ -3,6 +3,7 @@ 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";
|
|
6
7
|
class PasswordPolicyService {
|
|
7
8
|
constructor(resolver, userRepository, userPasswordHistoryService) {
|
|
8
9
|
this.resolver = resolver;
|
|
@@ -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 = allowedSpecialChars;
|
|
46
47
|
const fallbackSpecial = policy.disallowSpaces ? "!" : " ";
|
|
47
48
|
const combinedChars = [
|
|
48
49
|
uppercaseChars,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
|
+
import { allowedSpecialChars } from "../constants/PasswordSpecialChars.js";
|
|
2
3
|
class PasswordPolicySchemaFactory {
|
|
3
4
|
static create(policy) {
|
|
4
5
|
const cacheKey = JSON.stringify(policy);
|
|
@@ -20,7 +21,7 @@ class PasswordPolicySchemaFactory {
|
|
|
20
21
|
schema = schema.regex(/[0-9]/, "validation.password.requireNumber");
|
|
21
22
|
}
|
|
22
23
|
if (policy.requireSpecialChar) {
|
|
23
|
-
schema = schema.
|
|
24
|
+
schema = schema.refine((value) => [...value].some((char) => allowedSpecialChars.includes(char)), "validation.password.requireSpecialChar");
|
|
24
25
|
}
|
|
25
26
|
if (policy.disallowSpaces) {
|
|
26
27
|
schema = schema.refine((value) => !/\s/.test(value), {
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.21.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.21.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.21.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": "9ceebbba20e7abf7337387e2a81d1475b9ba1bca"
|
|
67
67
|
}
|
|
@@ -21,6 +21,7 @@ import {IDraxCrudEvent, IDraxFieldFilter} from "@drax/crud-share";
|
|
|
21
21
|
import TenantServiceFactory from "../factory/TenantServiceFactory.js";
|
|
22
22
|
import {CustomRequest} from "@drax/crud-back/dist";
|
|
23
23
|
import PasswordPolicyServiceFactory from "../factory/PasswordPolicyServiceFactory.js";
|
|
24
|
+
import {allowedSpecialChars} from "../constants/PasswordSpecialChars.js";
|
|
24
25
|
|
|
25
26
|
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
26
27
|
const AVATAR_DIR = DraxConfig.getOrLoad(IdentityConfig.AvatarDir) || 'avatar';
|
|
@@ -95,7 +96,11 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
95
96
|
async passwordPolicy(request, reply) {
|
|
96
97
|
try {
|
|
97
98
|
const passwordPolicyService = PasswordPolicyServiceFactory()
|
|
98
|
-
|
|
99
|
+
const policy = await passwordPolicyService.getFinalPolicy()
|
|
100
|
+
return {
|
|
101
|
+
...policy,
|
|
102
|
+
allowedSpecialChars
|
|
103
|
+
}
|
|
99
104
|
} catch (e) {
|
|
100
105
|
this.handleError(e, reply)
|
|
101
106
|
}
|
|
@@ -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().optional(),
|
|
10
11
|
disallowSpaces: z.boolean(),
|
|
11
12
|
preventReuse: z.number().int().min(0),
|
|
12
13
|
expirationDays: z.number().int().min(1).nullable(),
|
|
@@ -9,6 +9,7 @@ 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";
|
|
12
13
|
|
|
13
14
|
interface IValidatePasswordOptions {
|
|
14
15
|
field?: string
|
|
@@ -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 = allowedSpecialChars
|
|
64
65
|
const fallbackSpecial = policy.disallowSpaces ? "!" : " "
|
|
65
66
|
const combinedChars = [
|
|
66
67
|
uppercaseChars,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
2
|
import type {IPasswordPolicy} from "@drax/identity-share";
|
|
3
|
+
import {allowedSpecialChars} from "../constants/PasswordSpecialChars.js";
|
|
3
4
|
|
|
4
5
|
class PasswordPolicySchemaFactory {
|
|
5
6
|
private static cache = new Map<string, z.ZodType<string>>()
|
|
@@ -29,7 +30,10 @@ class PasswordPolicySchemaFactory {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
if (policy.requireSpecialChar) {
|
|
32
|
-
schema = schema.
|
|
33
|
+
schema = schema.refine(
|
|
34
|
+
(value) => [...value].some((char) => allowedSpecialChars.includes(char)),
|
|
35
|
+
"validation.password.requireSpecialChar"
|
|
36
|
+
)
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
if (policy.disallowSpaces) {
|
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
|
|
5
6
|
describe("Password Policy Route Test", () => {
|
|
6
7
|
const testSetup = new TestSetup("sqlite")
|
|
@@ -28,6 +29,7 @@ describe("Password Policy Route Test", () => {
|
|
|
28
29
|
const body = response.json()
|
|
29
30
|
expect(body.minLength).toBe(18)
|
|
30
31
|
expect(body.requireSpecialChar).toBe(true)
|
|
31
|
-
expect(body.
|
|
32
|
+
expect(body.allowedSpecialChars).toBe(allowedSpecialChars)
|
|
33
|
+
expect(body.requireUppercase).toBe(false)
|
|
32
34
|
})
|
|
33
35
|
})
|
|
@@ -1,11 +1,12 @@
|
|
|
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";
|
|
4
5
|
|
|
5
6
|
describe("PasswordPolicySchemaFactory", () => {
|
|
6
7
|
it("validates minLength", async () => {
|
|
7
8
|
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
8
|
-
await expect(schema.parseAsync("
|
|
9
|
+
await expect(schema.parseAsync("Abc12")).rejects.toThrow()
|
|
9
10
|
})
|
|
10
11
|
|
|
11
12
|
it("validates maxLength", async () => {
|
|
@@ -14,17 +15,17 @@ describe("PasswordPolicySchemaFactory", () => {
|
|
|
14
15
|
})
|
|
15
16
|
|
|
16
17
|
it("validates uppercase", async () => {
|
|
17
|
-
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
18
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireUppercase: true})
|
|
18
19
|
await expect(schema.parseAsync("lowercase1")).rejects.toThrow()
|
|
19
20
|
})
|
|
20
21
|
|
|
21
22
|
it("validates lowercase", async () => {
|
|
22
|
-
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
23
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireLowercase: true})
|
|
23
24
|
await expect(schema.parseAsync("UPPERCASE1")).rejects.toThrow()
|
|
24
25
|
})
|
|
25
26
|
|
|
26
27
|
it("validates number", async () => {
|
|
27
|
-
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
28
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireNumber: true})
|
|
28
29
|
await expect(schema.parseAsync("NoNumbers")).rejects.toThrow()
|
|
29
30
|
})
|
|
30
31
|
|
|
@@ -33,6 +34,16 @@ describe("PasswordPolicySchemaFactory", () => {
|
|
|
33
34
|
await expect(schema.parseAsync("NoSpecial1")).rejects.toThrow()
|
|
34
35
|
})
|
|
35
36
|
|
|
37
|
+
it("accepts allowed special chars", async () => {
|
|
38
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireSpecialChar: true})
|
|
39
|
+
await expect(schema.parseAsync(`Password1${allowedSpecialChars.at(-1)}`)).resolves.toBeDefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("rejects special chars outside the allowed list", async () => {
|
|
43
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireSpecialChar: true})
|
|
44
|
+
await expect(schema.parseAsync("Password1ñ")).rejects.toThrow()
|
|
45
|
+
})
|
|
46
|
+
|
|
36
47
|
it("validates disallowSpaces", async () => {
|
|
37
48
|
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
38
49
|
await expect(schema.parseAsync("Space 123A")).rejects.toThrow()
|
|
@@ -8,6 +8,7 @@ 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";
|
|
11
12
|
|
|
12
13
|
class InMemoryUserRepository implements IUserRepository {
|
|
13
14
|
private items = new Map<string, IUser>()
|
|
@@ -144,7 +145,9 @@ describe("UserServiceTest", function () {
|
|
|
144
145
|
beforeEach(async () => {
|
|
145
146
|
userRepository = new InMemoryUserRepository()
|
|
146
147
|
const userPasswordHistoryService = new UserPasswordHistoryService(new InMemoryUserPasswordHistoryRepository())
|
|
147
|
-
const
|
|
148
|
+
const passwordPolicyResolver = new PasswordPolicyResolver()
|
|
149
|
+
passwordPolicyResolver.setProjectPolicy({requireUppercase: true})
|
|
150
|
+
const passwordPolicyService = new PasswordPolicyService(passwordPolicyResolver, userRepository, userPasswordHistoryService)
|
|
148
151
|
userService = new UserService(userRepository, passwordPolicyService, userPasswordHistoryService)
|
|
149
152
|
|
|
150
153
|
userAdminData = {
|
|
@@ -222,7 +225,7 @@ describe("UserServiceTest", function () {
|
|
|
222
225
|
it("changeOwnPassword rejects password that does not meet policy", async function () {
|
|
223
226
|
const userId = userAdminData._id
|
|
224
227
|
await expect(async () => {
|
|
225
|
-
await userService.changeOwnPassword(userId, "Root1234", "
|
|
228
|
+
await userService.changeOwnPassword(userId, "Root1234", "sho1A")
|
|
226
229
|
}).rejects.toSatisfy((err) => {
|
|
227
230
|
expect(err).toBeInstanceOf(ValidationError)
|
|
228
231
|
expect(err.errors[0].field).toBe('newPassword')
|
|
@@ -256,4 +259,20 @@ describe("UserServiceTest", function () {
|
|
|
256
259
|
return true;
|
|
257
260
|
});
|
|
258
261
|
})
|
|
262
|
+
|
|
263
|
+
it("validatePassword returns allowedSpecialChars when required special char is invalid", async function () {
|
|
264
|
+
const resolver = new PasswordPolicyResolver()
|
|
265
|
+
resolver.setProjectPolicy({requireSpecialChar: true})
|
|
266
|
+
const passwordPolicyService = new PasswordPolicyService(resolver, userRepository)
|
|
267
|
+
|
|
268
|
+
await expect(async () => {
|
|
269
|
+
await passwordPolicyService.validatePassword("Password1ñ")
|
|
270
|
+
}).rejects.toSatisfy((err) => {
|
|
271
|
+
expect(err).toBeInstanceOf(ValidationError)
|
|
272
|
+
expect(err.errors[0].field).toBe('password')
|
|
273
|
+
expect(err.errors[0].reason).toBe('validation.password.requireSpecialChar')
|
|
274
|
+
expect(err.errors[0].allowedSpecialChars).toBe(allowedSpecialChars)
|
|
275
|
+
return true;
|
|
276
|
+
});
|
|
277
|
+
})
|
|
259
278
|
})
|