@tomei/sso 0.60.1 → 0.60.2
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/src/components/login-user/user.js +8 -1
- package/dist/src/components/login-user/user.js.map +1 -1
- package/dist/src/components/user-password-history/index.d.ts +2 -0
- package/dist/src/components/user-password-history/index.js +19 -0
- package/dist/src/components/user-password-history/index.js.map +1 -0
- package/dist/src/components/user-password-history/user-password-history.d.ts +20 -0
- package/dist/src/components/user-password-history/user-password-history.js +112 -0
- package/dist/src/components/user-password-history/user-password-history.js.map +1 -0
- package/dist/src/components/user-password-history/user-password-history.repository.d.ts +8 -0
- package/dist/src/components/user-password-history/user-password-history.repository.js +49 -0
- package/dist/src/components/user-password-history/user-password-history.repository.js.map +1 -0
- package/dist/src/interfaces/user-password-history.interface.d.ts +6 -0
- package/dist/src/interfaces/user-password-history.interface.js +3 -0
- package/dist/src/interfaces/user-password-history.interface.js.map +1 -0
- package/dist/src/models/user-password-history.d.ts +10 -0
- package/dist/src/models/user-password-history.js +60 -0
- package/dist/src/models/user-password-history.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migrations/20250326043818-crate-user-password-history.js +41 -0
- package/package.json +1 -1
- package/src/components/login-user/user.ts +46 -1
- package/src/components/user-password-history/index.ts +2 -0
- package/src/components/user-password-history/user-password-history.repository.ts +39 -0
- package/src/components/user-password-history/user-password-history.ts +185 -0
- package/src/interfaces/user-password-history.interface.ts +6 -0
- package/src/models/user-password-history.ts +52 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
/** @type {import('sequelize-cli').Migration} */
|
4
|
+
module.exports = {
|
5
|
+
async up(queryInterface, Sequelize) {
|
6
|
+
await queryInterface.createTable('sso_UserPasswordHistory', {
|
7
|
+
HistoryId: {
|
8
|
+
type: Sequelize.INTEGER,
|
9
|
+
autoIncrement: true,
|
10
|
+
primaryKey: true,
|
11
|
+
allowNull: false,
|
12
|
+
},
|
13
|
+
UserId: {
|
14
|
+
type: Sequelize.INTEGER,
|
15
|
+
allowNull: false,
|
16
|
+
references: {
|
17
|
+
model: 'sso_User',
|
18
|
+
key: 'UserId',
|
19
|
+
},
|
20
|
+
onUpdate: 'CASCADE',
|
21
|
+
onDelete: 'CASCADE',
|
22
|
+
},
|
23
|
+
PasswordHash: {
|
24
|
+
type: Sequelize.STRING(225),
|
25
|
+
allowNull: false,
|
26
|
+
},
|
27
|
+
CreatedAt: {
|
28
|
+
type: Sequelize.DATE,
|
29
|
+
allowNull: false,
|
30
|
+
},
|
31
|
+
UpdatedAt: {
|
32
|
+
type: Sequelize.DATE,
|
33
|
+
allowNull: false,
|
34
|
+
},
|
35
|
+
});
|
36
|
+
},
|
37
|
+
|
38
|
+
async down(queryInterface, Sequelize) {
|
39
|
+
await queryInterface.dropTable('sso_UserPasswordHistory');
|
40
|
+
},
|
41
|
+
};
|
package/package.json
CHANGED
@@ -36,6 +36,7 @@ import { AuthContext } from 'types';
|
|
36
36
|
import UserModel from '../../models/user.entity';
|
37
37
|
import { UserReportingHierarchyRepository } from '../user-reporting-hierarchy/user-reporting-hierarchy.repository';
|
38
38
|
import { IUserReportingHierarchyAttr } from 'interfaces/user-reporting-hierarchy.interface';
|
39
|
+
import { UserPasswordHistory } from '../user-password-history/user-password-history';
|
39
40
|
|
40
41
|
export class User extends UserBase {
|
41
42
|
ObjectId: string;
|
@@ -1462,6 +1463,19 @@ export class User extends UserBase {
|
|
1462
1463
|
await sessionService.deleteAuthorizationCode(hashedSubmittedToken);
|
1463
1464
|
console.log(`Token verified for user: ${userId}`);
|
1464
1465
|
|
1466
|
+
// Part 2: Validate Password History
|
1467
|
+
// Call by passing:
|
1468
|
+
// dbTransaction
|
1469
|
+
// UserId: loginUser.ObjectId
|
1470
|
+
// Password: password
|
1471
|
+
const passwordHashService = new PasswordHashService();
|
1472
|
+
await UserPasswordHistory.validate(
|
1473
|
+
dbTransaction,
|
1474
|
+
parseInt(userId),
|
1475
|
+
password,
|
1476
|
+
passwordHashService,
|
1477
|
+
);
|
1478
|
+
|
1465
1479
|
//Instantiate user
|
1466
1480
|
const user = await User.init(
|
1467
1481
|
sessionService,
|
@@ -1473,7 +1487,7 @@ export class User extends UserBase {
|
|
1473
1487
|
await User.setPassword(dbTransaction, user, password);
|
1474
1488
|
|
1475
1489
|
//Update user record
|
1476
|
-
await User._Repository.update(
|
1490
|
+
let userData = await User._Repository.update(
|
1477
1491
|
{
|
1478
1492
|
Password: user._Password,
|
1479
1493
|
DefaultPasswordChangedYN: YN.Yes,
|
@@ -1486,6 +1500,12 @@ export class User extends UserBase {
|
|
1486
1500
|
transaction: dbTransaction,
|
1487
1501
|
},
|
1488
1502
|
);
|
1503
|
+
|
1504
|
+
await UserPasswordHistory.create(
|
1505
|
+
dbTransaction,
|
1506
|
+
parseInt(userId),
|
1507
|
+
userData.Password,
|
1508
|
+
);
|
1489
1509
|
} catch (error) {
|
1490
1510
|
throw error;
|
1491
1511
|
}
|
@@ -1603,6 +1623,12 @@ export class User extends UserBase {
|
|
1603
1623
|
userInfo,
|
1604
1624
|
);
|
1605
1625
|
|
1626
|
+
await UserPasswordHistory.create(
|
1627
|
+
dbTransaction,
|
1628
|
+
userToBeCreated.UserId,
|
1629
|
+
userToBeCreated.Password,
|
1630
|
+
);
|
1631
|
+
|
1606
1632
|
//Part 5: Record Create User Activity
|
1607
1633
|
const activity = new Activity();
|
1608
1634
|
activity.ActivityId = activity.createId();
|
@@ -2845,6 +2871,19 @@ export class User extends UserBase {
|
|
2845
2871
|
) {
|
2846
2872
|
try {
|
2847
2873
|
const passwordHashService = new PasswordHashService();
|
2874
|
+
|
2875
|
+
// Part 2: Validate Password History
|
2876
|
+
// Call by passing:
|
2877
|
+
// dbTransaction
|
2878
|
+
// UserId: loginUser.ObjectId
|
2879
|
+
// Password: password
|
2880
|
+
await UserPasswordHistory.validate(
|
2881
|
+
dbTransaction,
|
2882
|
+
loginUser.UserId,
|
2883
|
+
newPassword,
|
2884
|
+
passwordHashService,
|
2885
|
+
);
|
2886
|
+
|
2848
2887
|
const isPasswordValid = await passwordHashService.verify(
|
2849
2888
|
oldPassword,
|
2850
2889
|
this.Password,
|
@@ -2929,6 +2968,12 @@ export class User extends UserBase {
|
|
2929
2968
|
},
|
2930
2969
|
);
|
2931
2970
|
|
2971
|
+
await UserPasswordHistory.create(
|
2972
|
+
dbTransaction,
|
2973
|
+
this.UserId,
|
2974
|
+
this.Password,
|
2975
|
+
);
|
2976
|
+
|
2932
2977
|
// Record update activity using Activity class create method.
|
2933
2978
|
const activity = new Activity();
|
2934
2979
|
activity.ActivityId = activity.createId();
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { RepositoryBase, IRepositoryBase } from '@tomei/general';
|
2
|
+
import UserPasswordHistoryModel from '../../models/user-password-history';
|
3
|
+
import { Op } from 'sequelize';
|
4
|
+
|
5
|
+
export class UserPasswordHistoryRepository
|
6
|
+
extends RepositoryBase<UserPasswordHistoryModel>
|
7
|
+
implements IRepositoryBase<UserPasswordHistoryModel>
|
8
|
+
{
|
9
|
+
constructor() {
|
10
|
+
super(UserPasswordHistoryModel);
|
11
|
+
}
|
12
|
+
|
13
|
+
async findByPk(id: string, options?: any): Promise<UserPasswordHistoryModel> {
|
14
|
+
return await UserPasswordHistoryModel.findByPk(parseInt(id), options);
|
15
|
+
}
|
16
|
+
|
17
|
+
async destroy(HistoryId: number, dbTransaction: any): Promise<void> {
|
18
|
+
await UserPasswordHistoryModel.destroy({
|
19
|
+
where: {
|
20
|
+
HistoryId: HistoryId,
|
21
|
+
},
|
22
|
+
transaction: dbTransaction,
|
23
|
+
});
|
24
|
+
}
|
25
|
+
|
26
|
+
async destroyMultiple(
|
27
|
+
HistoryIdList: number[],
|
28
|
+
dbTransaction: any,
|
29
|
+
): Promise<void> {
|
30
|
+
await UserPasswordHistoryModel.destroy({
|
31
|
+
where: {
|
32
|
+
HistoryId: {
|
33
|
+
[Op.in]: HistoryIdList,
|
34
|
+
},
|
35
|
+
},
|
36
|
+
transaction: dbTransaction,
|
37
|
+
});
|
38
|
+
}
|
39
|
+
}
|
@@ -0,0 +1,185 @@
|
|
1
|
+
import { ClassError, ObjectBase } from '@tomei/general';
|
2
|
+
import { ComponentConfig } from '@tomei/config';
|
3
|
+
import { IUserPasswordHistoryAttr } from '../../interfaces/user-password-history.interface';
|
4
|
+
import { UserPasswordHistoryRepository } from './user-password-history.repository';
|
5
|
+
import { PasswordHashService } from '../../components/password-hash';
|
6
|
+
|
7
|
+
export class UserPasswordHistory
|
8
|
+
extends ObjectBase
|
9
|
+
implements IUserPasswordHistoryAttr
|
10
|
+
{
|
11
|
+
ObjectId: string;
|
12
|
+
ObjectName: string;
|
13
|
+
ObjectType = 'UserPasswordHistory';
|
14
|
+
TableName = 'sso_UserPasswordHistory';
|
15
|
+
UserId: number;
|
16
|
+
PasswordHash: string;
|
17
|
+
private _CreatedAt: Date;
|
18
|
+
|
19
|
+
private static _Repo = new UserPasswordHistoryRepository();
|
20
|
+
|
21
|
+
get HistoryId(): number {
|
22
|
+
return parseInt(this.ObjectId);
|
23
|
+
}
|
24
|
+
|
25
|
+
set HistoryId(value: number) {
|
26
|
+
this.ObjectId = value.toString();
|
27
|
+
}
|
28
|
+
|
29
|
+
get CreatedAt(): Date {
|
30
|
+
return this._CreatedAt;
|
31
|
+
}
|
32
|
+
|
33
|
+
private constructor(params?: IUserPasswordHistoryAttr) {
|
34
|
+
super();
|
35
|
+
if (params) {
|
36
|
+
this.ObjectId = params.HistoryId.toString();
|
37
|
+
this.UserId = params.UserId;
|
38
|
+
this.PasswordHash = params.PasswordHash;
|
39
|
+
this._CreatedAt = params.CreatedAt;
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
public static async init(
|
44
|
+
historyId?: number,
|
45
|
+
dbTransaction?: any,
|
46
|
+
): Promise<UserPasswordHistory> {
|
47
|
+
try {
|
48
|
+
if (historyId) {
|
49
|
+
const data = await UserPasswordHistory._Repo.findByPk(
|
50
|
+
historyId.toString(),
|
51
|
+
dbTransaction,
|
52
|
+
);
|
53
|
+
if (!data) {
|
54
|
+
throw new ClassError(
|
55
|
+
'UserPasswordHistory',
|
56
|
+
'UserPasswordHistoryErrMsg01',
|
57
|
+
'UserPasswordHistory not found',
|
58
|
+
'init',
|
59
|
+
400,
|
60
|
+
);
|
61
|
+
}
|
62
|
+
|
63
|
+
return new UserPasswordHistory(data.get({ plain: true }));
|
64
|
+
}
|
65
|
+
return new UserPasswordHistory();
|
66
|
+
} catch (error) {
|
67
|
+
throw error;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
|
71
|
+
public static async validate(
|
72
|
+
dbTransaction: any,
|
73
|
+
UserId: number,
|
74
|
+
Password: string,
|
75
|
+
passwordHashService: PasswordHashService,
|
76
|
+
): Promise<void> {
|
77
|
+
// This method used to check if password entered is valid by checking previous password history
|
78
|
+
try {
|
79
|
+
// Part 1-2: Retrieve password history policy by using component config, call ComponentConfig. by passing:
|
80
|
+
// - ComponentName: "@tomei/sso"
|
81
|
+
// - ConfigKey: "passwordHistory"
|
82
|
+
// If no password history set, use default value 3
|
83
|
+
|
84
|
+
const passwordHistoryPolicy =
|
85
|
+
ComponentConfig.getComponentConfigValue(
|
86
|
+
'@tomei/sso',
|
87
|
+
'passwordHistory',
|
88
|
+
) || 3;
|
89
|
+
|
90
|
+
// Part 3-4: Retrieve records from the table by using class._repo findAll() by passing:
|
91
|
+
// where: { UserId: params.UserId }
|
92
|
+
// order: [['CreatedAt', 'DESC']]
|
93
|
+
// limit: passwordHistory count above.
|
94
|
+
// If no record found, return null.
|
95
|
+
|
96
|
+
let passwordHistory = await UserPasswordHistory._Repo.findAll({
|
97
|
+
where: { UserId: UserId },
|
98
|
+
order: [['CreatedAt', 'DESC']],
|
99
|
+
limit: passwordHistoryPolicy,
|
100
|
+
transaction: dbTransaction,
|
101
|
+
});
|
102
|
+
|
103
|
+
if (passwordHistory?.length < 1) {
|
104
|
+
return null;
|
105
|
+
} else {
|
106
|
+
// Part 5: If record found, map each record to compare params.Password and record.PasswordHash using the params.passwordHashService. If match, stop the mapping, and return ClassError:
|
107
|
+
// ClassName: "UserPasswordHistory"
|
108
|
+
// MethodName: "validate"
|
109
|
+
// MessageCode: "UserPasswordHistory01"
|
110
|
+
// Message: You cannot reuse your last ${passwordHistory} passwords. Please choose a new and unique password.
|
111
|
+
for (let index = 0; index < passwordHistory.length; index++) {
|
112
|
+
const isPasswordSame = await passwordHashService.verify(
|
113
|
+
Password,
|
114
|
+
passwordHistory[index].PasswordHash,
|
115
|
+
);
|
116
|
+
|
117
|
+
if (isPasswordSame) {
|
118
|
+
throw new ClassError(
|
119
|
+
'UserPasswordHistory',
|
120
|
+
'UserPasswordHistory01',
|
121
|
+
`You cannot reuse your last ${passwordHistoryPolicy} passwords. Please choose a new and unique password.`,
|
122
|
+
'validate',
|
123
|
+
403,
|
124
|
+
);
|
125
|
+
}
|
126
|
+
}
|
127
|
+
}
|
128
|
+
} catch (error) {
|
129
|
+
throw error;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
public static async create(
|
134
|
+
dbTransaction: any,
|
135
|
+
UserId: number,
|
136
|
+
PasswordHash: string,
|
137
|
+
): Promise<void> {
|
138
|
+
// This method used to check if password entered is valid by checking previous password history
|
139
|
+
try {
|
140
|
+
// Part 1-2: Retrieve password history policy by using component config, call ComponentConfig. by passing:
|
141
|
+
// - ComponentName: "@tomei/sso"
|
142
|
+
// - ConfigKey: "passwordHistory"
|
143
|
+
// If no password history set, use default value 3
|
144
|
+
|
145
|
+
const passwordHistoryPolicy =
|
146
|
+
ComponentConfig.getComponentConfigValue(
|
147
|
+
'@tomei/sso',
|
148
|
+
'passwordHistory',
|
149
|
+
) || 3;
|
150
|
+
|
151
|
+
// Part 3: Insert new password history by calling class _repo create() method.
|
152
|
+
let passwordHistory = await UserPasswordHistory._Repo.create(
|
153
|
+
{
|
154
|
+
UserId: UserId,
|
155
|
+
PasswordHash: PasswordHash,
|
156
|
+
},
|
157
|
+
{
|
158
|
+
transaction: dbTransaction,
|
159
|
+
},
|
160
|
+
);
|
161
|
+
|
162
|
+
// Part 3: When inserted successfully, retrieve all the password history for the user to check
|
163
|
+
// how many previous password records. If records more than the passwordHistory count from
|
164
|
+
// config. Remove the oldest record.
|
165
|
+
if (passwordHistory) {
|
166
|
+
let passwordHistoryList = await UserPasswordHistory._Repo.findAll({
|
167
|
+
where: { UserId: UserId },
|
168
|
+
order: [['CreatedAt', 'DESC']],
|
169
|
+
transaction: dbTransaction,
|
170
|
+
});
|
171
|
+
|
172
|
+
if (passwordHistoryList.length > passwordHistoryPolicy) {
|
173
|
+
let deleteList = passwordHistoryList.slice(passwordHistoryPolicy);
|
174
|
+
let historyIdList = deleteList.map((record) => record.HistoryId);
|
175
|
+
await UserPasswordHistory._Repo.destroyMultiple(
|
176
|
+
historyIdList,
|
177
|
+
dbTransaction,
|
178
|
+
);
|
179
|
+
}
|
180
|
+
}
|
181
|
+
} catch (error) {
|
182
|
+
throw error;
|
183
|
+
}
|
184
|
+
}
|
185
|
+
}
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import {
|
2
|
+
BelongsTo,
|
3
|
+
Column,
|
4
|
+
CreatedAt,
|
5
|
+
DataType,
|
6
|
+
ForeignKey,
|
7
|
+
Model,
|
8
|
+
Table,
|
9
|
+
} from 'sequelize-typescript';
|
10
|
+
import User from './user.entity';
|
11
|
+
import { IUserPasswordHistoryAttr } from '../interfaces/user-password-history.interface';
|
12
|
+
|
13
|
+
@Table({
|
14
|
+
tableName: 'sso_UserPasswordHistory',
|
15
|
+
timestamps: true,
|
16
|
+
createdAt: 'CreatedAt',
|
17
|
+
updatedAt: 'UpdatedAt',
|
18
|
+
})
|
19
|
+
export default class UserPasswordHistoryModel
|
20
|
+
extends Model
|
21
|
+
implements IUserPasswordHistoryAttr
|
22
|
+
{
|
23
|
+
@Column({
|
24
|
+
primaryKey: true,
|
25
|
+
allowNull: false,
|
26
|
+
type: DataType.INTEGER,
|
27
|
+
autoIncrement: true,
|
28
|
+
})
|
29
|
+
HistoryId: number;
|
30
|
+
|
31
|
+
@ForeignKey(() => User)
|
32
|
+
@Column({
|
33
|
+
allowNull: false,
|
34
|
+
type: DataType.INTEGER,
|
35
|
+
})
|
36
|
+
UserId: number;
|
37
|
+
|
38
|
+
@Column({
|
39
|
+
allowNull: false,
|
40
|
+
type: DataType.STRING(225),
|
41
|
+
})
|
42
|
+
PasswordHash: string;
|
43
|
+
|
44
|
+
@CreatedAt
|
45
|
+
CreatedAt: Date;
|
46
|
+
|
47
|
+
@BelongsTo(() => User, {
|
48
|
+
as: 'User',
|
49
|
+
foreignKey: 'UserId',
|
50
|
+
})
|
51
|
+
User: User;
|
52
|
+
}
|