@drax/identity-back 3.14.0 → 3.15.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 +14 -0
- package/dist/controllers/UserController.js +10 -0
- package/dist/factory/PasswordPolicyResolverFactory.js +9 -0
- package/dist/factory/PasswordPolicyServiceFactory.js +27 -0
- package/dist/factory/UserPasswordHistoryServiceFactory.js +25 -0
- package/dist/factory/UserServiceFactory.js +3 -1
- package/dist/index.js +20 -8
- package/dist/interfaces/IUserPasswordHistory.js +1 -0
- package/dist/interfaces/IUserPasswordHistoryRepository.js +1 -0
- package/dist/models/UserPasswordHistoryModel.js +30 -0
- package/dist/policies/defaultPasswordPolicy.js +12 -0
- package/dist/repository/mongo/UserPasswordHistoryMongoRepository.js +20 -0
- package/dist/repository/sqlite/UserPasswordHistorySqliteRepository.js +38 -0
- package/dist/resolver/PasswordPolicyResolver.js +27 -0
- package/dist/routes/UserRoutes.js +10 -0
- package/dist/schemas/PasswordPolicySchema.js +18 -0
- package/dist/schemas/RegisterSchema.js +1 -3
- package/dist/schemas/UserSchema.js +1 -3
- package/dist/security/constants/defaultPasswordPolicy.js +12 -0
- package/dist/security/interfaces/IPasswordPolicy.js +1 -0
- package/dist/security/interfaces/IPasswordPolicyProjectContext.js +1 -0
- package/dist/security/schemas/PasswordPolicySchema.js +18 -0
- package/dist/security/services/PasswordPolicyResolver.js +21 -0
- package/dist/security/services/PasswordPolicyService.js +147 -0
- package/dist/security/utils/PasswordPolicySchemaFactory.js +36 -0
- package/dist/security/utils/getPasswordEnvPolicy.js +19 -0
- package/dist/services/PasswordPolicyService.js +147 -0
- package/dist/services/UserPasswordHistoryService.js +18 -0
- package/dist/services/UserService.js +34 -9
- package/dist/setup/LoadIdentityConfigFromEnv.js +10 -0
- package/dist/setup/SetProjectPasswordPolicy.js +7 -0
- package/dist/utils/PasswordPolicySchemaFactory.js +36 -0
- package/dist/utils/getPasswordEnvPolicy.js +19 -0
- package/docs/password-policy.md +33 -0
- package/package.json +4 -4
- package/src/config/PasswordPolicyConfig.ts +14 -0
- package/src/controllers/UserController.ts +10 -1
- package/src/factory/PasswordPolicyResolverFactory.ts +14 -0
- package/src/factory/PasswordPolicyServiceFactory.ts +38 -0
- package/src/factory/UserPasswordHistoryServiceFactory.ts +31 -0
- package/src/factory/UserServiceFactory.ts +7 -1
- package/src/index.ts +28 -3
- package/src/interfaces/IUserPasswordHistory.ts +21 -0
- package/src/interfaces/IUserPasswordHistoryRepository.ts +8 -0
- package/src/models/UserPasswordHistoryModel.ts +42 -0
- package/src/policies/defaultPasswordPolicy.ts +17 -0
- package/src/repository/mongo/UserPasswordHistoryMongoRepository.ts +25 -0
- package/src/repository/sqlite/UserPasswordHistorySqliteRepository.ts +47 -0
- package/src/resolver/PasswordPolicyResolver.ts +33 -0
- package/src/routes/UserRoutes.ts +11 -0
- package/src/schemas/PasswordPolicySchema.ts +29 -0
- package/src/schemas/RegisterSchema.ts +1 -3
- package/src/schemas/UserSchema.ts +1 -3
- package/src/services/PasswordPolicyService.ts +184 -0
- package/src/services/UserPasswordHistoryService.ts +23 -0
- package/src/services/UserService.ts +38 -9
- package/src/setup/LoadIdentityConfigFromEnv.ts +11 -0
- package/src/setup/SetProjectPasswordPolicy.ts +12 -0
- package/src/utils/PasswordPolicySchemaFactory.ts +47 -0
- package/src/utils/getPasswordEnvPolicy.ts +25 -0
- package/test/data-obj/users/root-mongo-user.ts +1 -1
- package/test/data-obj/users/root-sqlite-user.ts +1 -1
- package/test/endpoints/data/users-data.ts +3 -3
- package/test/endpoints/password-policy-route.test.ts +33 -0
- package/test/endpoints/user-route.test.ts +17 -4
- package/test/security/password-policy-resolver.test.ts +55 -0
- package/test/security/password-policy-schema-factory.test.ts +40 -0
- package/test/services/user-service.test.ts +218 -31
- package/test/setup/TestSetup.ts +22 -4
- package/test/setup/data/basic-user.ts +1 -1
- package/test/setup/data/root-user.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/types/config/PasswordPolicyConfig.d.ts +14 -0
- package/types/config/PasswordPolicyConfig.d.ts.map +1 -0
- package/types/controllers/UserController.d.ts +1 -0
- package/types/controllers/UserController.d.ts.map +1 -1
- package/types/factory/PasswordPolicyResolverFactory.d.ts +4 -0
- package/types/factory/PasswordPolicyResolverFactory.d.ts.map +1 -0
- package/types/factory/PasswordPolicyServiceFactory.d.ts +4 -0
- package/types/factory/PasswordPolicyServiceFactory.d.ts.map +1 -0
- package/types/factory/UserPasswordHistoryServiceFactory.d.ts +4 -0
- package/types/factory/UserPasswordHistoryServiceFactory.d.ts.map +1 -0
- package/types/factory/UserServiceFactory.d.ts.map +1 -1
- package/types/index.d.ts +15 -2
- package/types/index.d.ts.map +1 -1
- package/types/interfaces/IUserPasswordHistory.d.ts +17 -0
- package/types/interfaces/IUserPasswordHistory.d.ts.map +1 -0
- package/types/interfaces/IUserPasswordHistoryRepository.d.ts +7 -0
- package/types/interfaces/IUserPasswordHistoryRepository.d.ts.map +1 -0
- package/types/models/UserPasswordHistoryModel.d.ts +15 -0
- package/types/models/UserPasswordHistoryModel.d.ts.map +1 -0
- package/types/policies/defaultPasswordPolicy.d.ts +4 -0
- package/types/policies/defaultPasswordPolicy.d.ts.map +1 -0
- package/types/repository/mongo/UserPasswordHistoryMongoRepository.d.ts +10 -0
- package/types/repository/mongo/UserPasswordHistoryMongoRepository.d.ts.map +1 -0
- package/types/repository/sqlite/UserPasswordHistorySqliteRepository.d.ts +25 -0
- package/types/repository/sqlite/UserPasswordHistorySqliteRepository.d.ts.map +1 -0
- package/types/resolver/PasswordPolicyResolver.d.ts +10 -0
- package/types/resolver/PasswordPolicyResolver.d.ts.map +1 -0
- package/types/routes/UserRoutes.d.ts.map +1 -1
- package/types/schemas/PasswordPolicySchema.d.ts +25 -0
- package/types/schemas/PasswordPolicySchema.d.ts.map +1 -0
- package/types/schemas/RegisterSchema.d.ts.map +1 -1
- package/types/schemas/UserSchema.d.ts.map +1 -1
- package/types/security/constants/defaultPasswordPolicy.d.ts +4 -0
- package/types/security/constants/defaultPasswordPolicy.d.ts.map +1 -0
- package/types/security/interfaces/IPasswordPolicy.d.ts +13 -0
- package/types/security/interfaces/IPasswordPolicy.d.ts.map +1 -0
- package/types/security/interfaces/IPasswordPolicyProjectContext.d.ts +6 -0
- package/types/security/interfaces/IPasswordPolicyProjectContext.d.ts.map +1 -0
- package/types/security/schemas/PasswordPolicySchema.d.ts +25 -0
- package/types/security/schemas/PasswordPolicySchema.d.ts.map +1 -0
- package/types/security/services/PasswordPolicyResolver.d.ts +9 -0
- package/types/security/services/PasswordPolicyResolver.d.ts.map +1 -0
- package/types/security/services/PasswordPolicyService.d.ts +35 -0
- package/types/security/services/PasswordPolicyService.d.ts.map +1 -0
- package/types/security/utils/PasswordPolicySchemaFactory.d.ts +9 -0
- package/types/security/utils/PasswordPolicySchemaFactory.d.ts.map +1 -0
- package/types/security/utils/getPasswordEnvPolicy.d.ts +5 -0
- package/types/security/utils/getPasswordEnvPolicy.d.ts.map +1 -0
- package/types/services/PasswordPolicyService.d.ts +34 -0
- package/types/services/PasswordPolicyService.d.ts.map +1 -0
- package/types/services/UserPasswordHistoryService.d.ts +10 -0
- package/types/services/UserPasswordHistoryService.d.ts.map +1 -0
- package/types/services/UserService.d.ts +5 -1
- package/types/services/UserService.d.ts.map +1 -1
- package/types/setup/LoadIdentityConfigFromEnv.d.ts.map +1 -1
- package/types/setup/SetProjectPasswordPolicy.d.ts +5 -0
- package/types/setup/SetProjectPasswordPolicy.d.ts.map +1 -0
- package/types/utils/PasswordPolicySchemaFactory.d.ts +9 -0
- package/types/utils/PasswordPolicySchemaFactory.d.ts.map +1 -0
- package/types/utils/getPasswordEnvPolicy.d.ts +5 -0
- package/types/utils/getPasswordEnvPolicy.d.ts.map +1 -0
|
@@ -11,12 +11,20 @@ import {AbstractService} from "@drax/crud-back";
|
|
|
11
11
|
import {randomUUID} from "crypto"
|
|
12
12
|
import UserLoginFailServiceFactory from "../factory/UserLoginFailServiceFactory.js";
|
|
13
13
|
import UserSessionServiceFactory from "../factory/UserSessionServiceFactory.js";
|
|
14
|
+
import type PasswordPolicyService from "./PasswordPolicyService";
|
|
15
|
+
import PasswordPolicyServiceFactory from "../factory/PasswordPolicyServiceFactory.js";
|
|
16
|
+
import type UserPasswordHistoryService from "./UserPasswordHistoryService.js";
|
|
17
|
+
import UserPasswordHistoryServiceFactory from "../factory/UserPasswordHistoryServiceFactory.js";
|
|
14
18
|
|
|
15
19
|
class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
|
|
16
20
|
|
|
17
21
|
_repository: IUserRepository
|
|
18
22
|
|
|
19
|
-
constructor(
|
|
23
|
+
constructor(
|
|
24
|
+
userRepository: IUserRepository,
|
|
25
|
+
private readonly passwordPolicyService: PasswordPolicyService = PasswordPolicyServiceFactory(),
|
|
26
|
+
private readonly userPasswordHistoryService: UserPasswordHistoryService = UserPasswordHistoryServiceFactory()
|
|
27
|
+
) {
|
|
20
28
|
super(userRepository, UserBaseSchema);
|
|
21
29
|
this._repository = userRepository;
|
|
22
30
|
}
|
|
@@ -75,7 +83,7 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
|
|
|
75
83
|
user = await this.findByEmail(email)
|
|
76
84
|
|
|
77
85
|
if (!user && createIfNotFound) {
|
|
78
|
-
userData.password = userData.password ? userData.password :
|
|
86
|
+
userData.password = userData.password ? userData.password : await this.passwordPolicyService.generateCompatiblePassword()
|
|
79
87
|
userData.active = userData.active === undefined ? true : userData.active
|
|
80
88
|
user = await this.create(userData)
|
|
81
89
|
}
|
|
@@ -105,8 +113,14 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
|
|
|
105
113
|
async changeUserPassword(userId: string, newPassword: string):Promise<IUser> {
|
|
106
114
|
const user = await this._repository.findByIdWithPassword(userId)
|
|
107
115
|
if (user) {
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
await this.passwordPolicyService.validatePassword(newPassword, {
|
|
117
|
+
field: 'newPassword',
|
|
118
|
+
userId,
|
|
119
|
+
currentPasswordHash: user.password
|
|
120
|
+
})
|
|
121
|
+
const newPasswordHash = AuthUtils.hashPassword(newPassword)
|
|
122
|
+
await this._repository.changePassword(userId, newPasswordHash)
|
|
123
|
+
await this.userPasswordHistoryService.create(userId, newPasswordHash)
|
|
110
124
|
delete user.password
|
|
111
125
|
return user
|
|
112
126
|
} else {
|
|
@@ -124,8 +138,14 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
|
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
if (AuthUtils.checkPassword(currentPassword, user.password)) {
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
await this.passwordPolicyService.validatePassword(newPassword, {
|
|
142
|
+
field: 'newPassword',
|
|
143
|
+
userId,
|
|
144
|
+
currentPasswordHash: user.password
|
|
145
|
+
})
|
|
146
|
+
const newPasswordHash = AuthUtils.hashPassword(newPassword)
|
|
147
|
+
await this._repository.changePassword(userId, newPasswordHash)
|
|
148
|
+
await this.userPasswordHistoryService.create(userId, newPasswordHash)
|
|
129
149
|
delete user.password
|
|
130
150
|
return user
|
|
131
151
|
} else {
|
|
@@ -167,8 +187,14 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
|
|
|
167
187
|
try {
|
|
168
188
|
const user = await this._repository.findByRecoveryCode(recoveryCode)
|
|
169
189
|
if (user && user.active) {
|
|
170
|
-
|
|
171
|
-
|
|
190
|
+
await this.passwordPolicyService.validatePassword(newPassword, {
|
|
191
|
+
field: 'newPassword',
|
|
192
|
+
userId: user._id.toString(),
|
|
193
|
+
currentPasswordHash: user.password
|
|
194
|
+
})
|
|
195
|
+
const newPasswordHash = AuthUtils.hashPassword(newPassword)
|
|
196
|
+
await this._repository.changePassword(user._id, newPasswordHash)
|
|
197
|
+
await this.userPasswordHistoryService.create(user._id.toString(), newPasswordHash)
|
|
172
198
|
await this._repository.updatePartial(user._id, {recoveryCode: null})
|
|
173
199
|
return user
|
|
174
200
|
} else {
|
|
@@ -237,10 +263,13 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
|
|
|
237
263
|
userData.tenant = userData.tenant === "" ? null : userData.tenant
|
|
238
264
|
|
|
239
265
|
await UserCreateSchema.parseAsync(userData)
|
|
266
|
+
await this.passwordPolicyService.validatePassword(userData.password)
|
|
240
267
|
|
|
241
|
-
|
|
268
|
+
const passwordHash = AuthUtils.hashPassword(userData.password.trim())
|
|
269
|
+
userData.password = passwordHash
|
|
242
270
|
|
|
243
271
|
const user: IUser = await this._repository.create(userData)
|
|
272
|
+
await this.userPasswordHistoryService.create(user._id.toString(), passwordHash)
|
|
244
273
|
return user
|
|
245
274
|
} catch (e) {
|
|
246
275
|
console.error("Error creating user", e)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {DraxConfig} from "@drax/common-back";
|
|
2
2
|
import IdentityConfig from "../config/IdentityConfig.js";
|
|
3
|
+
import PasswordPolicyConfig from "../config/PasswordPolicyConfig.js";
|
|
3
4
|
|
|
4
5
|
function LoadIdentityConfigFromEnv() {
|
|
5
6
|
|
|
@@ -10,6 +11,16 @@ function LoadIdentityConfigFromEnv() {
|
|
|
10
11
|
|
|
11
12
|
DraxConfig.set(IdentityConfig.RbacCacheTTL, process.env[IdentityConfig.RbacCacheTTL])
|
|
12
13
|
DraxConfig.set(IdentityConfig.AvatarDir, process.env[IdentityConfig.AvatarDir])
|
|
14
|
+
|
|
15
|
+
DraxConfig.set(PasswordPolicyConfig.MinLength, process.env[PasswordPolicyConfig.MinLength])
|
|
16
|
+
DraxConfig.set(PasswordPolicyConfig.MaxLength, process.env[PasswordPolicyConfig.MaxLength])
|
|
17
|
+
DraxConfig.set(PasswordPolicyConfig.RequireUppercase, process.env[PasswordPolicyConfig.RequireUppercase])
|
|
18
|
+
DraxConfig.set(PasswordPolicyConfig.RequireLowercase, process.env[PasswordPolicyConfig.RequireLowercase])
|
|
19
|
+
DraxConfig.set(PasswordPolicyConfig.RequireNumber, process.env[PasswordPolicyConfig.RequireNumber])
|
|
20
|
+
DraxConfig.set(PasswordPolicyConfig.RequireSpecialChar, process.env[PasswordPolicyConfig.RequireSpecialChar])
|
|
21
|
+
DraxConfig.set(PasswordPolicyConfig.DisallowSpaces, process.env[PasswordPolicyConfig.DisallowSpaces])
|
|
22
|
+
DraxConfig.set(PasswordPolicyConfig.PreventReuse, process.env[PasswordPolicyConfig.PreventReuse])
|
|
23
|
+
DraxConfig.set(PasswordPolicyConfig.ExpirationDays, process.env[PasswordPolicyConfig.ExpirationDays])
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
export default LoadIdentityConfigFromEnv
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type {IPasswordPolicyProject} from "@drax/identity-share";
|
|
2
|
+
import PasswordPolicyResolverFactory from "../factory/PasswordPolicyResolverFactory.js";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function SetProjectPasswordPolicy(projectPasswordPolicy: IPasswordPolicyProject){
|
|
6
|
+
|
|
7
|
+
const passwordPolicyResolver = PasswordPolicyResolverFactory()
|
|
8
|
+
passwordPolicyResolver.setProjectPolicy(projectPasswordPolicy)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default SetProjectPasswordPolicy
|
|
12
|
+
export {SetProjectPasswordPolicy}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import type {IPasswordPolicy} from "@drax/identity-share";
|
|
3
|
+
|
|
4
|
+
class PasswordPolicySchemaFactory {
|
|
5
|
+
private static cache = new Map<string, z.ZodType<string>>()
|
|
6
|
+
|
|
7
|
+
static create(policy: IPasswordPolicy): z.ZodType<string> {
|
|
8
|
+
const cacheKey = JSON.stringify(policy)
|
|
9
|
+
const cached = this.cache.get(cacheKey)
|
|
10
|
+
if (cached) {
|
|
11
|
+
return cached
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let schema = z.string({error: "validation.required"})
|
|
15
|
+
.min(1, "validation.required")
|
|
16
|
+
.min(policy.minLength, "validation.password.minLength")
|
|
17
|
+
.max(policy.maxLength, "validation.password.maxLength")
|
|
18
|
+
|
|
19
|
+
if (policy.requireUppercase) {
|
|
20
|
+
schema = schema.regex(/[A-Z]/, "validation.password.requireUppercase")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (policy.requireLowercase) {
|
|
24
|
+
schema = schema.regex(/[a-z]/, "validation.password.requireLowercase")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (policy.requireNumber) {
|
|
28
|
+
schema = schema.regex(/[0-9]/, "validation.password.requireNumber")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (policy.requireSpecialChar) {
|
|
32
|
+
schema = schema.regex(/[^A-Za-z0-9]/, "validation.password.requireSpecialChar")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (policy.disallowSpaces) {
|
|
36
|
+
schema = schema.refine((value) => !/\s/.test(value), {
|
|
37
|
+
message: "validation.password.disallowSpaces"
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.cache.set(cacheKey, schema)
|
|
42
|
+
return schema
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default PasswordPolicySchemaFactory
|
|
47
|
+
export {PasswordPolicySchemaFactory}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {IPasswordPolicy} from "@drax/identity-share";
|
|
2
|
+
import {DraxConfig} from "@drax/common-back";
|
|
3
|
+
import PasswordPolicyConfig from "../config/PasswordPolicyConfig.js";
|
|
4
|
+
import {PartialPasswordPolicySchema} from "../schemas/PasswordPolicySchema.js";
|
|
5
|
+
|
|
6
|
+
function getPasswordEnvPolicy(): Partial<IPasswordPolicy> {
|
|
7
|
+
const envPolicy = {
|
|
8
|
+
minLength: DraxConfig.getOrLoad(PasswordPolicyConfig.MinLength, "number"),
|
|
9
|
+
maxLength: DraxConfig.getOrLoad(PasswordPolicyConfig.MaxLength, "number"),
|
|
10
|
+
requireUppercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireUppercase, "boolean"),
|
|
11
|
+
requireLowercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireLowercase, "boolean"),
|
|
12
|
+
requireNumber: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireNumber, "boolean"),
|
|
13
|
+
requireSpecialChar: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireSpecialChar, "boolean"),
|
|
14
|
+
disallowSpaces: DraxConfig.getOrLoad(PasswordPolicyConfig.DisallowSpaces, "boolean"),
|
|
15
|
+
preventReuse: DraxConfig.getOrLoad(PasswordPolicyConfig.PreventReuse, "number"),
|
|
16
|
+
expirationDays: DraxConfig.getOrLoad(PasswordPolicyConfig.ExpirationDays, "number")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return PartialPasswordPolicySchema.parse(
|
|
20
|
+
Object.fromEntries(Object.entries(envPolicy).filter(([, value]) => value !== undefined))
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default getPasswordEnvPolicy
|
|
25
|
+
export {getPasswordEnvPolicy}
|
|
@@ -2,7 +2,7 @@ import {IUserCreate} from "@drax/identity-share";
|
|
|
2
2
|
|
|
3
3
|
const USER1: IUserCreate = {
|
|
4
4
|
active: true,
|
|
5
|
-
password: "
|
|
5
|
+
password: "User1234",
|
|
6
6
|
phone: "",
|
|
7
7
|
role: "",
|
|
8
8
|
name: "John Wick",
|
|
@@ -11,7 +11,7 @@ const USER1: IUserCreate = {
|
|
|
11
11
|
}
|
|
12
12
|
const USER2: IUserCreate = {
|
|
13
13
|
active: true,
|
|
14
|
-
password: "
|
|
14
|
+
password: "User1234",
|
|
15
15
|
phone: "",
|
|
16
16
|
role: "",
|
|
17
17
|
name: "John Rambo",
|
|
@@ -20,7 +20,7 @@ const USER2: IUserCreate = {
|
|
|
20
20
|
}
|
|
21
21
|
const USER3: IUserCreate = {
|
|
22
22
|
active: true,
|
|
23
|
-
password: "
|
|
23
|
+
password: "User1234",
|
|
24
24
|
phone: "",
|
|
25
25
|
role: "",
|
|
26
26
|
name: "John Depp",
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {afterAll, beforeAll, describe, expect, it} from "vitest";
|
|
2
|
+
import {LoadIdentityConfigFromEnv} from "../../src/setup/LoadIdentityConfigFromEnv.js";
|
|
3
|
+
import {TestSetup} from "../setup/TestSetup";
|
|
4
|
+
|
|
5
|
+
describe("Password Policy Route Test", () => {
|
|
6
|
+
const testSetup = new TestSetup("sqlite")
|
|
7
|
+
|
|
8
|
+
beforeAll(async () => {
|
|
9
|
+
await testSetup.setup()
|
|
10
|
+
process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR = "true"
|
|
11
|
+
process.env.PASSWORD_POLICY_MIN_LENGTH = "18"
|
|
12
|
+
LoadIdentityConfigFromEnv()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
delete process.env.PASSWORD_POLICY_MIN_LENGTH
|
|
17
|
+
delete process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR
|
|
18
|
+
await testSetup.dropAndClose()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("returns the final effective policy", async () => {
|
|
22
|
+
const response = await testSetup.fastifyInstance.inject({
|
|
23
|
+
method: "GET",
|
|
24
|
+
url: "/api/auth/password-policy"
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
expect(response.statusCode).toBe(200)
|
|
28
|
+
const body = response.json()
|
|
29
|
+
expect(body.minLength).toBe(18)
|
|
30
|
+
expect(body.requireSpecialChar).toBe(true)
|
|
31
|
+
expect(body.requireUppercase).toBe(true)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -6,7 +6,7 @@ import {USER1} from "./data/users-data"
|
|
|
6
6
|
|
|
7
7
|
describe("User Route Test", async function () {
|
|
8
8
|
|
|
9
|
-
let testSetup = new TestSetup()
|
|
9
|
+
let testSetup = new TestSetup("sqlite")
|
|
10
10
|
|
|
11
11
|
beforeAll(async () => {
|
|
12
12
|
await testSetup.setup()
|
|
@@ -91,7 +91,7 @@ describe("User Route Test", async function () {
|
|
|
91
91
|
|
|
92
92
|
const items = await getResp.json();
|
|
93
93
|
expect(getResp.statusCode).toBe(200);
|
|
94
|
-
expect(items
|
|
94
|
+
expect(items.some((item) => item._id === result._id && item.name === USER1.name)).toBe(true);
|
|
95
95
|
})
|
|
96
96
|
|
|
97
97
|
|
|
@@ -123,7 +123,7 @@ describe("User Route Test", async function () {
|
|
|
123
123
|
const respPassword = await testSetup.fastifyInstance.inject({
|
|
124
124
|
method: 'POST',
|
|
125
125
|
url: '/api/users/password/change',
|
|
126
|
-
payload: {currentPassword: "
|
|
126
|
+
payload: {currentPassword: "Basic1234", newPassword: "Newpass123"},
|
|
127
127
|
headers: {Authorization: `Bearer ${accessToken}`}
|
|
128
128
|
});
|
|
129
129
|
|
|
@@ -141,7 +141,7 @@ describe("User Route Test", async function () {
|
|
|
141
141
|
const respPassword = await testSetup.fastifyInstance.inject({
|
|
142
142
|
method: 'POST',
|
|
143
143
|
url: '/api/users/password/change/'+testSetup.rootUser._id,
|
|
144
|
-
payload: {currentPassword: "
|
|
144
|
+
payload: {currentPassword: "Root1234", newPassword: "Newpass123"},
|
|
145
145
|
headers: {Authorization: `Bearer ${accessToken}`}
|
|
146
146
|
});
|
|
147
147
|
|
|
@@ -152,5 +152,18 @@ describe("User Route Test", async function () {
|
|
|
152
152
|
|
|
153
153
|
})
|
|
154
154
|
|
|
155
|
+
it("should return effective password policy", async () => {
|
|
156
|
+
const resp = await testSetup.fastifyInstance.inject({
|
|
157
|
+
method: 'GET',
|
|
158
|
+
url: '/api/auth/password-policy',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const result = await resp.json();
|
|
162
|
+
expect(resp.statusCode).toBe(200);
|
|
163
|
+
expect(result.minLength).toBe(8);
|
|
164
|
+
expect(result.requireUppercase).toBe(true);
|
|
165
|
+
expect(result.preventReuse).toBe(3);
|
|
166
|
+
})
|
|
167
|
+
|
|
155
168
|
|
|
156
169
|
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {afterEach, describe, expect, it} from "vitest";
|
|
2
|
+
import {DraxConfig} from "@drax/common-back";
|
|
3
|
+
import PasswordPolicyResolver from "../../src/resolver/PasswordPolicyResolver.js";
|
|
4
|
+
import PasswordPolicyConfig from "../../src/config/PasswordPolicyConfig.js";
|
|
5
|
+
|
|
6
|
+
describe("PasswordPolicyResolver", () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
delete process.env.PASSWORD_POLICY_MIN_LENGTH
|
|
9
|
+
delete process.env.PASSWORD_POLICY_MAX_LENGTH
|
|
10
|
+
delete process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR
|
|
11
|
+
DraxConfig.set(PasswordPolicyConfig.MinLength, undefined)
|
|
12
|
+
DraxConfig.set(PasswordPolicyConfig.MaxLength, undefined)
|
|
13
|
+
DraxConfig.set(PasswordPolicyConfig.RequireSpecialChar, undefined)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("uses default policy when there are no overrides", async () => {
|
|
17
|
+
const resolver = new PasswordPolicyResolver()
|
|
18
|
+
const policy = await resolver.resolve()
|
|
19
|
+
|
|
20
|
+
expect(policy.minLength).toBe(8)
|
|
21
|
+
expect(policy.maxLength).toBe(64)
|
|
22
|
+
expect(policy.requireUppercase).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("project policy overrides default policy", async () => {
|
|
26
|
+
const resolver = new PasswordPolicyResolver()
|
|
27
|
+
resolver.setProjectPolicy({minLength: 12, requireUppercase: false, requireSpecialChar: true})
|
|
28
|
+
const policy = await resolver.resolve()
|
|
29
|
+
|
|
30
|
+
expect(policy.minLength).toBe(12)
|
|
31
|
+
expect(policy.requireSpecialChar).toBe(true)
|
|
32
|
+
expect(policy.requireUppercase).toBe(false)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("env policy overrides project policy", async () => {
|
|
36
|
+
process.env.PASSWORD_POLICY_MIN_LENGTH = "16"
|
|
37
|
+
process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR = "false"
|
|
38
|
+
|
|
39
|
+
const resolver = new PasswordPolicyResolver()
|
|
40
|
+
const policy = await resolver.resolve()
|
|
41
|
+
|
|
42
|
+
expect(policy.minLength).toBe(16)
|
|
43
|
+
expect(policy.requireSpecialChar).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("ignores undefined env overrides", async () => {
|
|
47
|
+
process.env.PASSWORD_POLICY_MIN_LENGTH = ""
|
|
48
|
+
|
|
49
|
+
const resolver = new PasswordPolicyResolver()
|
|
50
|
+
resolver.setProjectPolicy({minLength: 14})
|
|
51
|
+
const policy = await resolver.resolve()
|
|
52
|
+
|
|
53
|
+
expect(policy.minLength).toBe(14)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {describe, expect, it} from "vitest";
|
|
2
|
+
import PasswordPolicySchemaFactory from "../../src/utils/PasswordPolicySchemaFactory.js";
|
|
3
|
+
import {defaultPasswordPolicy} from "../../src/policies/defaultPasswordPolicy.js";
|
|
4
|
+
|
|
5
|
+
describe("PasswordPolicySchemaFactory", () => {
|
|
6
|
+
it("validates minLength", async () => {
|
|
7
|
+
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
8
|
+
await expect(schema.parseAsync("Abc1234")).rejects.toThrow()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("validates maxLength", async () => {
|
|
12
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, maxLength: 8})
|
|
13
|
+
await expect(schema.parseAsync("Abcd12345")).rejects.toThrow()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("validates uppercase", async () => {
|
|
17
|
+
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
18
|
+
await expect(schema.parseAsync("lowercase1")).rejects.toThrow()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("validates lowercase", async () => {
|
|
22
|
+
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
23
|
+
await expect(schema.parseAsync("UPPERCASE1")).rejects.toThrow()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("validates number", async () => {
|
|
27
|
+
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
28
|
+
await expect(schema.parseAsync("NoNumbers")).rejects.toThrow()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("validates specialChar", async () => {
|
|
32
|
+
const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireSpecialChar: true})
|
|
33
|
+
await expect(schema.parseAsync("NoSpecial1")).rejects.toThrow()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("validates disallowSpaces", async () => {
|
|
37
|
+
const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
|
|
38
|
+
await expect(schema.parseAsync("Space 123A")).rejects.toThrow()
|
|
39
|
+
})
|
|
40
|
+
})
|