@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
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ import TenantServiceFactory from "./factory/TenantServiceFactory.js";
|
|
|
5
5
|
import UserApiKeyServiceFactory from "./factory/UserApiKeyServiceFactory.js";
|
|
6
6
|
import UserLoginFailServiceFactory from "./factory/UserLoginFailServiceFactory.js";
|
|
7
7
|
import UserSessionServiceFactory from "./factory/UserSessionServiceFactory.js";
|
|
8
|
+
import PasswordPolicyServiceFactory from "./factory/PasswordPolicyServiceFactory.js";
|
|
9
|
+
import UserPasswordHistoryServiceFactory from "./factory/UserPasswordHistoryServiceFactory.js";
|
|
8
10
|
|
|
9
11
|
import RoleService from "./services/RoleService.js";
|
|
10
12
|
import UserService from "./services/UserService.js";
|
|
@@ -13,6 +15,9 @@ import PermissionService from "./services/PermissionService.js";
|
|
|
13
15
|
import UserApiKeyService from "./services/UserApiKeyService.js";
|
|
14
16
|
import UserSessionService from "./services/UserSessionService.js";
|
|
15
17
|
import UserLoginFailService from "./services/UserLoginFailService.js";
|
|
18
|
+
import UserPasswordHistoryService from "./services/UserPasswordHistoryService.js";
|
|
19
|
+
import PasswordPolicyService from "./services/PasswordPolicyService.js";
|
|
20
|
+
import PasswordPolicyResolver from "./resolver/PasswordPolicyResolver.js";
|
|
16
21
|
|
|
17
22
|
import Rbac from "./rbac/Rbac.js";
|
|
18
23
|
|
|
@@ -29,6 +34,7 @@ import {rbacMiddleware} from "./middleware/rbacMiddleware.js";
|
|
|
29
34
|
import {apiKeyMiddleware} from "./middleware/apiKeyMiddleware.js";
|
|
30
35
|
|
|
31
36
|
import IdentityConfig from "./config/IdentityConfig.js";
|
|
37
|
+
import PasswordPolicyConfig from "./config/PasswordPolicyConfig.js";
|
|
32
38
|
import BadCredentialsError from "./errors/BadCredentialsError.js";
|
|
33
39
|
|
|
34
40
|
import CreateUserIfNotExist from "./setup/CreateUserIfNotExist.js";
|
|
@@ -37,6 +43,7 @@ import CreateOrUpdateRole from "./setup/CreateOrUpdateRole.js";
|
|
|
37
43
|
import LoadPermissions from "./setup/LoadPermissions.js";
|
|
38
44
|
import LoadIdentityConfigFromEnv from "./setup/LoadIdentityConfigFromEnv.js";
|
|
39
45
|
import RecoveryUserPassword from "./setup/RecoveryUserPassword.js";
|
|
46
|
+
import SetProjectPasswordPolicy from "./setup/SetProjectPasswordPolicy.js";
|
|
40
47
|
|
|
41
48
|
import type {IRoleRepository} from "./interfaces/IRoleRepository";
|
|
42
49
|
import type {ITenantRepository} from "./interfaces/ITenantRepository";
|
|
@@ -44,6 +51,7 @@ import type {IUserRepository} from "./interfaces/IUserRepository";
|
|
|
44
51
|
import type {IUserApiKeyRepository} from "./interfaces/IUserApiKeyRepository";
|
|
45
52
|
import type {IUserLoginFailRepository} from "./interfaces/IUserLoginFailRepository";
|
|
46
53
|
import type {IUserSessionRepository} from "./interfaces/IUserSessionRepository";
|
|
54
|
+
import type {IUserPasswordHistoryRepository} from "./interfaces/IUserPasswordHistoryRepository";
|
|
47
55
|
|
|
48
56
|
import {RoleModel, RoleMongoSchema} from "./models/RoleModel.js";
|
|
49
57
|
import {TenantModel, TenantMongoSchema} from "./models/TenantModel.js";
|
|
@@ -51,6 +59,7 @@ import {UserModel, UserMongoSchema} from "./models/UserModel.js";
|
|
|
51
59
|
import {UserApiKeyModel, UserApiKeyMongoSchema} from "./models/UserApiKeyModel.js";
|
|
52
60
|
import {UserSessionModel,UserSessionMongoSchema} from "./models/UserSessionModel.js";
|
|
53
61
|
import {UserLoginFailModel,UserLoginFailMongoSchema} from "./models/UserLoginFailModel.js";
|
|
62
|
+
import {UserPasswordHistoryModel, UserPasswordHistoryMongoSchema} from "./models/UserPasswordHistoryModel.js";
|
|
54
63
|
|
|
55
64
|
|
|
56
65
|
import RoleMongoRepository from "./repository/mongo/RoleMongoRepository.js";
|
|
@@ -59,6 +68,7 @@ import UserMongoRepository from "./repository/mongo/UserMongoRepository.js";
|
|
|
59
68
|
import UserApiKeyMongoRepository from "./repository/mongo/UserApiKeyMongoRepository.js";
|
|
60
69
|
import UserSessionMongoRepository from "./repository/mongo/UserSessionMongoRepository.js";
|
|
61
70
|
import UserLoginFailMongoRepository from "./repository/mongo/UserLoginFailMongoRepository.js";
|
|
71
|
+
import UserPasswordHistoryMongoRepository from "./repository/mongo/UserPasswordHistoryMongoRepository.js";
|
|
62
72
|
|
|
63
73
|
import RoleSqliteRepository from "./repository/sqlite/RoleSqliteRepository.js";
|
|
64
74
|
import TenantSqliteRepository from "./repository/sqlite/TenantSqliteRepository.js";
|
|
@@ -66,6 +76,7 @@ import UserSqliteRepository from "./repository/sqlite/UserSqliteRepository.js";
|
|
|
66
76
|
import UserApiKeySqliteRepository from "./repository/sqlite/UserApiKeySqliteRepository.js";
|
|
67
77
|
import UserLoginFailSqliteRepository from "./repository/sqlite/UserLoginFailSqliteRepository.js";
|
|
68
78
|
import UserSessionSqliteRepository from "./repository/sqlite/UserSessionSqliteRepository.js";
|
|
79
|
+
import UserPasswordHistorySqliteRepository from "./repository/sqlite/UserPasswordHistorySqliteRepository.js";
|
|
69
80
|
|
|
70
81
|
|
|
71
82
|
import {RolePermissions} from "./permissions/RolePermissions.js";
|
|
@@ -81,6 +92,8 @@ import {RoleSchema, RoleBaseSchema} from "./schemas/RoleSchema.js";
|
|
|
81
92
|
import {UserApiKeySchema, UserApiKeyBaseSchema} from "./schemas/UserApiKeySchema.js";
|
|
82
93
|
import {UserLoginFailBaseSchema, UserLoginFailSchema} from "./schemas/UserLoginFailSchema.js";
|
|
83
94
|
import {UserSessionBaseSchema, UserSessionSchema} from "./schemas/UserSessionSchema.js";
|
|
95
|
+
import {defaultPasswordPolicy} from "./policies/defaultPasswordPolicy.js";
|
|
96
|
+
import {PasswordPolicySchema} from "./schemas/PasswordPolicySchema.js";
|
|
84
97
|
|
|
85
98
|
|
|
86
99
|
const graphqlMergeResult = await GraphqlMerge()
|
|
@@ -95,6 +108,7 @@ export type {
|
|
|
95
108
|
IUserApiKeyRepository,
|
|
96
109
|
IUserLoginFailRepository,
|
|
97
110
|
IUserSessionRepository,
|
|
111
|
+
IUserPasswordHistoryRepository,
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
export {
|
|
@@ -118,6 +132,9 @@ export {
|
|
|
118
132
|
UserApiKeyService,
|
|
119
133
|
UserSessionService,
|
|
120
134
|
UserLoginFailService,
|
|
135
|
+
UserPasswordHistoryService,
|
|
136
|
+
PasswordPolicyService,
|
|
137
|
+
PasswordPolicyResolver,
|
|
121
138
|
PermissionService,
|
|
122
139
|
Rbac,
|
|
123
140
|
|
|
@@ -128,6 +145,8 @@ export {
|
|
|
128
145
|
UserApiKeyServiceFactory,
|
|
129
146
|
UserSessionServiceFactory,
|
|
130
147
|
UserLoginFailServiceFactory,
|
|
148
|
+
UserPasswordHistoryServiceFactory,
|
|
149
|
+
PasswordPolicyServiceFactory,
|
|
131
150
|
|
|
132
151
|
//GQL
|
|
133
152
|
identityTypeDefs,
|
|
@@ -163,6 +182,7 @@ export {
|
|
|
163
182
|
UserApiKeyMongoRepository,
|
|
164
183
|
UserSessionMongoRepository,
|
|
165
184
|
UserLoginFailMongoRepository,
|
|
185
|
+
UserPasswordHistoryMongoRepository,
|
|
166
186
|
|
|
167
187
|
//Mongo Models
|
|
168
188
|
RoleModel,
|
|
@@ -171,6 +191,7 @@ export {
|
|
|
171
191
|
UserApiKeyModel,
|
|
172
192
|
UserSessionModel,
|
|
173
193
|
UserLoginFailModel,
|
|
194
|
+
UserPasswordHistoryModel,
|
|
174
195
|
|
|
175
196
|
RoleMongoSchema,
|
|
176
197
|
TenantMongoSchema,
|
|
@@ -178,6 +199,7 @@ export {
|
|
|
178
199
|
UserApiKeyMongoSchema,
|
|
179
200
|
UserSessionMongoSchema,
|
|
180
201
|
UserLoginFailMongoSchema,
|
|
202
|
+
UserPasswordHistoryMongoSchema,
|
|
181
203
|
|
|
182
204
|
//Sqlite Repositories
|
|
183
205
|
RoleSqliteRepository,
|
|
@@ -186,12 +208,16 @@ export {
|
|
|
186
208
|
UserApiKeySqliteRepository,
|
|
187
209
|
UserLoginFailSqliteRepository,
|
|
188
210
|
UserSessionSqliteRepository,
|
|
211
|
+
UserPasswordHistorySqliteRepository,
|
|
189
212
|
|
|
190
213
|
//Config
|
|
191
214
|
IdentityConfig,
|
|
215
|
+
PasswordPolicyConfig,
|
|
192
216
|
|
|
193
217
|
//Errors
|
|
194
218
|
BadCredentialsError,
|
|
219
|
+
defaultPasswordPolicy,
|
|
220
|
+
PasswordPolicySchema,
|
|
195
221
|
|
|
196
222
|
//Setup
|
|
197
223
|
LoadIdentityConfigFromEnv,
|
|
@@ -199,7 +225,6 @@ export {
|
|
|
199
225
|
CreateOrUpdateRole,
|
|
200
226
|
CreateUserIfNotExist,
|
|
201
227
|
CreateTenantIfNotExist,
|
|
202
|
-
RecoveryUserPassword
|
|
228
|
+
RecoveryUserPassword,
|
|
229
|
+
SetProjectPasswordPolicy
|
|
203
230
|
}
|
|
204
|
-
|
|
205
|
-
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface IUserPasswordHistory {
|
|
2
|
+
_id?: string
|
|
3
|
+
id?: string
|
|
4
|
+
user: string
|
|
5
|
+
passwordHash: string
|
|
6
|
+
createdAt?: string | Date
|
|
7
|
+
updatedAt?: string | Date
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface IUserPasswordHistoryCreate {
|
|
11
|
+
_id?: string
|
|
12
|
+
id?: string
|
|
13
|
+
user: string
|
|
14
|
+
passwordHash: string
|
|
15
|
+
createdAt?: string | Date
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
IUserPasswordHistory,
|
|
20
|
+
IUserPasswordHistoryCreate
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type {IUserPasswordHistory, IUserPasswordHistoryCreate} from "./IUserPasswordHistory.js";
|
|
2
|
+
|
|
3
|
+
interface IUserPasswordHistoryRepository {
|
|
4
|
+
create(data: IUserPasswordHistoryCreate): Promise<IUserPasswordHistory>
|
|
5
|
+
findLatestByUserId(userId: string, limit: number): Promise<IUserPasswordHistory[]>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type {IUserPasswordHistoryRepository}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {mongoose} from "@drax/common-back";
|
|
2
|
+
import {PaginateModel} from "mongoose";
|
|
3
|
+
import uniqueValidator from "mongoose-unique-validator";
|
|
4
|
+
import mongoosePaginate from "mongoose-paginate-v2";
|
|
5
|
+
import type {IUserPasswordHistory} from "../interfaces/IUserPasswordHistory.js";
|
|
6
|
+
|
|
7
|
+
const UserPasswordHistoryMongoSchema = new mongoose.Schema<IUserPasswordHistory>({
|
|
8
|
+
user: {type: String, required: true, index: true},
|
|
9
|
+
passwordHash: {type: String, required: true, index: false}
|
|
10
|
+
}, {timestamps: true});
|
|
11
|
+
|
|
12
|
+
UserPasswordHistoryMongoSchema.plugin(uniqueValidator, {message: "validation.unique"});
|
|
13
|
+
UserPasswordHistoryMongoSchema.plugin(mongoosePaginate);
|
|
14
|
+
|
|
15
|
+
UserPasswordHistoryMongoSchema.virtual("id").get(function () {
|
|
16
|
+
return this._id.toString();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
UserPasswordHistoryMongoSchema.set("toJSON", {getters: true, virtuals: true});
|
|
20
|
+
UserPasswordHistoryMongoSchema.set("toObject", {getters: true, virtuals: true});
|
|
21
|
+
|
|
22
|
+
const MODEL_NAME = "UserPasswordHistory";
|
|
23
|
+
const COLLECTION_NAME = "user_password_history";
|
|
24
|
+
|
|
25
|
+
let UserPasswordHistoryModel;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
UserPasswordHistoryModel = mongoose.model<IUserPasswordHistory, PaginateModel<IUserPasswordHistory>>(MODEL_NAME, UserPasswordHistoryMongoSchema, COLLECTION_NAME);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (e.name === "OverwriteModelError") {
|
|
31
|
+
UserPasswordHistoryModel = mongoose.model<IUserPasswordHistory, PaginateModel<IUserPasswordHistory>>(MODEL_NAME);
|
|
32
|
+
} else {
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
UserPasswordHistoryMongoSchema,
|
|
39
|
+
UserPasswordHistoryModel
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default UserPasswordHistoryModel
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type {IPasswordPolicy} from "@drax/identity-share/dist";
|
|
2
|
+
|
|
3
|
+
const defaultPasswordPolicy: IPasswordPolicy = {
|
|
4
|
+
minLength: 8,
|
|
5
|
+
maxLength: 64,
|
|
6
|
+
requireUppercase: true,
|
|
7
|
+
requireLowercase: true,
|
|
8
|
+
requireNumber: true,
|
|
9
|
+
requireSpecialChar: false,
|
|
10
|
+
disallowSpaces: true,
|
|
11
|
+
preventReuse: 3,
|
|
12
|
+
expirationDays: null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
defaultPasswordPolicy
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {AbstractMongoRepository} from "@drax/crud-back";
|
|
2
|
+
import type {IUserPasswordHistoryRepository} from "../../interfaces/IUserPasswordHistoryRepository.js";
|
|
3
|
+
import type {IUserPasswordHistory, IUserPasswordHistoryCreate} from "../../interfaces/IUserPasswordHistory.js";
|
|
4
|
+
import {UserPasswordHistoryModel} from "../../models/UserPasswordHistoryModel.js";
|
|
5
|
+
|
|
6
|
+
class UserPasswordHistoryMongoRepository extends AbstractMongoRepository<IUserPasswordHistory, IUserPasswordHistoryCreate, IUserPasswordHistoryCreate> implements IUserPasswordHistoryRepository {
|
|
7
|
+
constructor() {
|
|
8
|
+
super();
|
|
9
|
+
this._model = UserPasswordHistoryModel;
|
|
10
|
+
this._searchFields = ["user"];
|
|
11
|
+
this._populateFields = ["user"];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async findLatestByUserId(userId: string, limit: number): Promise<IUserPasswordHistory[]> {
|
|
15
|
+
return UserPasswordHistoryModel
|
|
16
|
+
.find({user: userId})
|
|
17
|
+
.sort({createdAt: -1})
|
|
18
|
+
.limit(limit)
|
|
19
|
+
.lean(true)
|
|
20
|
+
.exec() as Promise<IUserPasswordHistory[]>
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default UserPasswordHistoryMongoRepository
|
|
25
|
+
export {UserPasswordHistoryMongoRepository}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type {SqliteTableField} from "@drax/common-back";
|
|
2
|
+
import {AbstractSqliteRepository} from "@drax/crud-back";
|
|
3
|
+
import type {IUserPasswordHistoryRepository} from "../../interfaces/IUserPasswordHistoryRepository.js";
|
|
4
|
+
import type {IUserPasswordHistory, IUserPasswordHistoryCreate} from "../../interfaces/IUserPasswordHistory.js";
|
|
5
|
+
|
|
6
|
+
class UserPasswordHistorySqliteRepository extends AbstractSqliteRepository<IUserPasswordHistory, IUserPasswordHistoryCreate, IUserPasswordHistoryCreate> implements IUserPasswordHistoryRepository {
|
|
7
|
+
protected db: any;
|
|
8
|
+
protected tableName: string = "user_password_history";
|
|
9
|
+
protected dataBaseFile: string;
|
|
10
|
+
protected searchFields: string[] = [];
|
|
11
|
+
protected booleanFields: string[] = [];
|
|
12
|
+
protected identifier: string = "_id";
|
|
13
|
+
protected populateFields = [
|
|
14
|
+
{field: "user", table: "users", identifier: "_id"}
|
|
15
|
+
]
|
|
16
|
+
protected tableFields: SqliteTableField[] = [
|
|
17
|
+
{name: "user", type: "TEXT", unique: false, primary: false},
|
|
18
|
+
{name: "passwordHash", type: "TEXT", unique: false, primary: false},
|
|
19
|
+
{name: "createdAt", type: "TEXT", unique: false, primary: false},
|
|
20
|
+
{name: "updatedAt", type: "TEXT", unique: false, primary: false},
|
|
21
|
+
]
|
|
22
|
+
protected verbose: boolean;
|
|
23
|
+
|
|
24
|
+
async prepareData(): Promise<void> {
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async prepareItem(item: IUserPasswordHistory): Promise<void> {
|
|
28
|
+
if (item.createdAt && typeof item.createdAt === "string") {
|
|
29
|
+
item.createdAt = new Date(item.createdAt)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (item.updatedAt && typeof item.updatedAt === "string") {
|
|
33
|
+
item.updatedAt = new Date(item.updatedAt)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async findLatestByUserId(userId: string, limit: number): Promise<IUserPasswordHistory[]> {
|
|
38
|
+
const rows = this.db.prepare(`SELECT * FROM ${this.tableName} WHERE user = ? ORDER BY createdAt DESC LIMIT ?`).all(userId, limit);
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
await this.decorate(row)
|
|
41
|
+
}
|
|
42
|
+
return rows
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default UserPasswordHistorySqliteRepository
|
|
47
|
+
export {UserPasswordHistorySqliteRepository}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {IPasswordPolicy, IPasswordPolicyProject} from "@drax/identity-share";
|
|
2
|
+
import {defaultPasswordPolicy} from "../policies/defaultPasswordPolicy.js";
|
|
3
|
+
import {PartialPasswordPolicySchema, PasswordPolicySchema} from "../schemas/PasswordPolicySchema.js";
|
|
4
|
+
import getPasswordEnvPolicy from "../utils/getPasswordEnvPolicy.js";
|
|
5
|
+
|
|
6
|
+
class PasswordPolicyResolver {
|
|
7
|
+
|
|
8
|
+
private projectPolicy : IPasswordPolicyProject = {};
|
|
9
|
+
|
|
10
|
+
setProjectPolicy (projectPolicy : IPasswordPolicyProject){
|
|
11
|
+
this.projectPolicy = projectPolicy;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async resolve(): Promise<IPasswordPolicy> {
|
|
15
|
+
const projectPolicy = await this.getProjectPolicy()
|
|
16
|
+
const envPolicy = getPasswordEnvPolicy()
|
|
17
|
+
|
|
18
|
+
return PasswordPolicySchema.parse({
|
|
19
|
+
...defaultPasswordPolicy,
|
|
20
|
+
...projectPolicy,
|
|
21
|
+
...envPolicy
|
|
22
|
+
}) as IPasswordPolicy
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private async getProjectPolicy(): Promise<Partial<IPasswordPolicy>> {
|
|
26
|
+
return this.projectPolicy
|
|
27
|
+
? PartialPasswordPolicySchema.parse(this.projectPolicy)
|
|
28
|
+
: {}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default PasswordPolicyResolver
|
|
33
|
+
export {PasswordPolicyResolver}
|
package/src/routes/UserRoutes.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../schemas/PasswordSchema.js";
|
|
11
11
|
import {SwitchTenantBodyRequestSchema, SwitchTenantBodyResponseSchema} from "../schemas/SwitchTenantSchema.js";
|
|
12
12
|
import zod from "zod"
|
|
13
|
+
import {PasswordPolicySchema} from "../schemas/PasswordPolicySchema.js";
|
|
13
14
|
|
|
14
15
|
async function UserRoutes(fastify, options) {
|
|
15
16
|
|
|
@@ -44,6 +45,16 @@ async function UserRoutes(fastify, options) {
|
|
|
44
45
|
},
|
|
45
46
|
(req, rep) => controller.auth(req, rep))
|
|
46
47
|
|
|
48
|
+
fastify.get('/api/auth/password-policy', {
|
|
49
|
+
schema: {
|
|
50
|
+
tags: ['Auth'],
|
|
51
|
+
response: {
|
|
52
|
+
200: zod.toJSONSchema(PasswordPolicySchema),
|
|
53
|
+
500: schemas.jsonErrorBodyResponse,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}, (req, rep) => controller.passwordPolicy(req, rep))
|
|
57
|
+
|
|
47
58
|
fastify.get('/api/auth/me', {
|
|
48
59
|
schema: {
|
|
49
60
|
tags: ['Auth'],
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
|
|
3
|
+
const PasswordPolicySchemaBase = z.object({
|
|
4
|
+
minLength: z.number().int().min(1),
|
|
5
|
+
maxLength: z.number().int().min(1),
|
|
6
|
+
requireUppercase: z.boolean(),
|
|
7
|
+
requireLowercase: z.boolean(),
|
|
8
|
+
requireNumber: z.boolean(),
|
|
9
|
+
requireSpecialChar: z.boolean(),
|
|
10
|
+
disallowSpaces: z.boolean(),
|
|
11
|
+
preventReuse: z.number().int().min(0),
|
|
12
|
+
expirationDays: z.number().int().min(1).nullable(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const PasswordPolicySchema = PasswordPolicySchemaBase.refine((
|
|
16
|
+
policy) =>
|
|
17
|
+
policy.maxLength >= policy.minLength,
|
|
18
|
+
{
|
|
19
|
+
message: "validation.password.maxLength",
|
|
20
|
+
path: ["maxLength"]
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const PartialPasswordPolicySchema = PasswordPolicySchemaBase.partial();
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
PasswordPolicySchema,
|
|
28
|
+
PartialPasswordPolicySchema
|
|
29
|
+
}
|
|
@@ -8,9 +8,7 @@ const RegisterBodyRequestSchema = z.object({
|
|
|
8
8
|
email: email("validation.email.invalid"),
|
|
9
9
|
phone: string({ error: "validation.required" }).optional(),
|
|
10
10
|
password: string({ error: "validation.required" })
|
|
11
|
-
.min(1, "validation.required")
|
|
12
|
-
.min(8, "validation.password.min8")
|
|
13
|
-
.max(64, "validation.password.max64"),
|
|
11
|
+
.min(1, "validation.required"),
|
|
14
12
|
});
|
|
15
13
|
|
|
16
14
|
const RegisterBodyResponseSchema = z.object({
|
|
@@ -17,9 +17,7 @@ const UserBaseSchema = object({
|
|
|
17
17
|
|
|
18
18
|
const UserCreateSchema = UserBaseSchema.extend({
|
|
19
19
|
password: string({error: "validation.required"})
|
|
20
|
-
.min(1, "validation.required")
|
|
21
|
-
.min(8, "validation.password.min8")
|
|
22
|
-
.max(64, "validation.password.max64"),
|
|
20
|
+
.min(1, "validation.required"),
|
|
23
21
|
});
|
|
24
22
|
|
|
25
23
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {randomInt} from "crypto";
|
|
2
|
+
import {ZodError, ZodType} from "zod";
|
|
3
|
+
import {ValidationError, ZodErrorToValidationError} from "@drax/common-back";
|
|
4
|
+
import type {IUser} from "@drax/identity-share";
|
|
5
|
+
import type {IUserPasswordHistory} from "../interfaces/IUserPasswordHistory";
|
|
6
|
+
import type {IPasswordPolicy} from "@drax/identity-share";
|
|
7
|
+
import type UserPasswordHistoryService from "./UserPasswordHistoryService";
|
|
8
|
+
import type {IUserRepository} from "../interfaces/IUserRepository";
|
|
9
|
+
import AuthUtils from "../utils/AuthUtils.js";
|
|
10
|
+
import PasswordPolicyResolver from "../resolver/PasswordPolicyResolver.js";
|
|
11
|
+
import PasswordPolicySchemaFactory from "../utils/PasswordPolicySchemaFactory.js";
|
|
12
|
+
|
|
13
|
+
interface IValidatePasswordOptions {
|
|
14
|
+
field?: string
|
|
15
|
+
userId?: string
|
|
16
|
+
currentPasswordHash?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class PasswordPolicyService {
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly resolver: PasswordPolicyResolver,
|
|
22
|
+
private readonly userRepository?: IUserRepository,
|
|
23
|
+
private readonly userPasswordHistoryService?: UserPasswordHistoryService
|
|
24
|
+
) {
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getFinalPolicy(): Promise<IPasswordPolicy> {
|
|
28
|
+
return this.resolver.resolve()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async getPasswordSchema(): Promise<ZodType<string>> {
|
|
32
|
+
const policy = await this.getFinalPolicy()
|
|
33
|
+
return PasswordPolicySchemaFactory.create(policy)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async validatePassword(password: string, options?: IValidatePasswordOptions): Promise<void> {
|
|
37
|
+
const field = options?.field || "password"
|
|
38
|
+
try {
|
|
39
|
+
const schema = await this.getPasswordSchema()
|
|
40
|
+
await schema.parseAsync(password)
|
|
41
|
+
} catch (e) {
|
|
42
|
+
if (e instanceof ZodError) {
|
|
43
|
+
const validationError = ZodErrorToValidationError(e, {[field]: password})
|
|
44
|
+
validationError.errors = validationError.errors.map((error) => ({...error, field}))
|
|
45
|
+
throw validationError
|
|
46
|
+
}
|
|
47
|
+
throw e
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (options?.userId) {
|
|
51
|
+
await this.validateReuse(password, options.userId, {
|
|
52
|
+
field,
|
|
53
|
+
currentPasswordHash: options.currentPasswordHash
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async generateCompatiblePassword(): Promise<string> {
|
|
59
|
+
const policy = await this.getFinalPolicy()
|
|
60
|
+
const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
61
|
+
const lowercaseChars = "abcdefghijkmnopqrstuvwxyz"
|
|
62
|
+
const numericChars = "0123456789"
|
|
63
|
+
const specialChars = "!@#$%&*_-+="
|
|
64
|
+
const fallbackSpecial = policy.disallowSpaces ? "!" : " "
|
|
65
|
+
const combinedChars = [
|
|
66
|
+
uppercaseChars,
|
|
67
|
+
lowercaseChars,
|
|
68
|
+
numericChars,
|
|
69
|
+
policy.requireSpecialChar ? specialChars : "",
|
|
70
|
+
policy.disallowSpaces ? "" : " "
|
|
71
|
+
].join("") || `${uppercaseChars}${lowercaseChars}${numericChars}${fallbackSpecial}`
|
|
72
|
+
|
|
73
|
+
const chars: string[] = []
|
|
74
|
+
|
|
75
|
+
if (policy.requireUppercase) {
|
|
76
|
+
chars.push(this.randomChar(uppercaseChars))
|
|
77
|
+
}
|
|
78
|
+
if (policy.requireLowercase) {
|
|
79
|
+
chars.push(this.randomChar(lowercaseChars))
|
|
80
|
+
}
|
|
81
|
+
if (policy.requireNumber) {
|
|
82
|
+
chars.push(this.randomChar(numericChars))
|
|
83
|
+
}
|
|
84
|
+
if (policy.requireSpecialChar) {
|
|
85
|
+
chars.push(this.randomChar(specialChars))
|
|
86
|
+
}
|
|
87
|
+
if (!chars.length) {
|
|
88
|
+
chars.push(this.randomChar(`${uppercaseChars}${lowercaseChars}${numericChars}`))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
while (chars.length < policy.minLength) {
|
|
92
|
+
chars.push(this.randomChar(combinedChars))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const password = this.shuffle(chars).join("").slice(0, policy.maxLength)
|
|
96
|
+
await this.validatePassword(password)
|
|
97
|
+
return password
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async getPasswordExpiration(user: IUser): Promise<{expired: boolean, expiresAt: Date | null}> {
|
|
101
|
+
const policy = await this.getFinalPolicy()
|
|
102
|
+
if (!policy.expirationDays) {
|
|
103
|
+
return {expired: false, expiresAt: null}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const lastPasswordChange = await this.getLastPasswordChangeDate(user)
|
|
107
|
+
if (!lastPasswordChange) {
|
|
108
|
+
return {expired: false, expiresAt: null}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const expiresAt = new Date(lastPasswordChange)
|
|
112
|
+
expiresAt.setDate(expiresAt.getDate() + policy.expirationDays)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
expired: expiresAt.getTime() <= Date.now(),
|
|
116
|
+
expiresAt
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async recordPassword(userId: string, passwordHash: string): Promise<void> {
|
|
121
|
+
await this.userPasswordHistoryService?.create(userId, passwordHash)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async validateReuse(password: string, userId: string, options?: {field: string, currentPasswordHash?: string}): Promise<void> {
|
|
125
|
+
const policy = await this.getFinalPolicy()
|
|
126
|
+
if (!policy.preventReuse) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const field = options?.field || "password"
|
|
131
|
+
const recentHashes = await this.getRecentPasswordHashes(userId, policy.preventReuse, options?.currentPasswordHash)
|
|
132
|
+
const reused = recentHashes.some((item) => AuthUtils.checkPassword(password, item.passwordHash))
|
|
133
|
+
if (reused) {
|
|
134
|
+
throw new ValidationError([{field, reason: "validation.password.preventReuse"}])
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async getRecentPasswordHashes(userId: string, limit: number, currentPasswordHash?: string): Promise<IUserPasswordHistory[]> {
|
|
139
|
+
const recent = await this.userPasswordHistoryService?.findLatestByUserId(userId, limit) || []
|
|
140
|
+
const hashes = [...recent]
|
|
141
|
+
|
|
142
|
+
if (currentPasswordHash && !hashes.some((item) => item.passwordHash === currentPasswordHash)) {
|
|
143
|
+
hashes.unshift({user: userId, passwordHash: currentPasswordHash})
|
|
144
|
+
} else if (!currentPasswordHash && this.userRepository) {
|
|
145
|
+
const user = await this.userRepository.findByIdWithPassword(userId)
|
|
146
|
+
if (user?.password && !hashes.some((item) => item.passwordHash === user.password)) {
|
|
147
|
+
hashes.unshift({user: userId, passwordHash: user.password})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return hashes.slice(0, limit)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async getLastPasswordChangeDate(user: IUser): Promise<Date | null> {
|
|
155
|
+
if (!user?._id || !this.userPasswordHistoryService) {
|
|
156
|
+
return user?.updatedAt ? new Date(user.updatedAt) : user?.createdAt ? new Date(user.createdAt) : null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const latest = await this.userPasswordHistoryService.findLatestByUserId(user._id.toString(), 1)
|
|
160
|
+
if (latest[0]?.createdAt) {
|
|
161
|
+
return new Date(latest[0].createdAt)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return user?.updatedAt ? new Date(user.updatedAt) : user?.createdAt ? new Date(user.createdAt) : null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private randomChar(source: string): string {
|
|
168
|
+
return source[randomInt(0, source.length)]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private shuffle(chars: string[]): string[] {
|
|
172
|
+
const items = [...chars]
|
|
173
|
+
for (let i = items.length - 1; i > 0; i -= 1) {
|
|
174
|
+
const j = randomInt(0, i + 1)
|
|
175
|
+
const temp = items[i]
|
|
176
|
+
items[i] = items[j]
|
|
177
|
+
items[j] = temp
|
|
178
|
+
}
|
|
179
|
+
return items
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export default PasswordPolicyService
|
|
184
|
+
export {PasswordPolicyService}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {IUserPasswordHistoryRepository} from "../interfaces/IUserPasswordHistoryRepository.js";
|
|
2
|
+
import type {IUserPasswordHistory} from "../interfaces/IUserPasswordHistory.js";
|
|
3
|
+
|
|
4
|
+
class UserPasswordHistoryService {
|
|
5
|
+
constructor(private readonly repository: IUserPasswordHistoryRepository) {
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async create(userId: string, passwordHash: string): Promise<IUserPasswordHistory> {
|
|
9
|
+
return this.repository.create({
|
|
10
|
+
user: userId,
|
|
11
|
+
passwordHash
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async findLatestByUserId(userId: string, limit: number): Promise<IUserPasswordHistory[]> {
|
|
16
|
+
if (limit <= 0) {
|
|
17
|
+
return []
|
|
18
|
+
}
|
|
19
|
+
return this.repository.findLatestByUserId(userId, limit)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default UserPasswordHistoryService
|