Skip to content

Commit

Permalink
feat: password history
Browse files Browse the repository at this point in the history
  • Loading branch information
Vilsol committed Nov 12, 2024
1 parent e7ddfbc commit ecf1041
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 8 deletions.
7 changes: 5 additions & 2 deletions cfg/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
"minUsernameLength": 8,
"maxUsernameLength": 40,
"passwordComplexityMinScore": 3,
"passwordMinLength": 12
"passwordMinLength": 12,
"passwordHistoryEnabled": true,
"passwordHistorySize": 3,
"passwordHistoryEnforcement": false
},
"logger": {
"console": {
Expand Down Expand Up @@ -423,4 +426,4 @@
"users": "./data/seed_data/seed-accounts.json",
"roles": "./data/seed_data/seed-roles.json"
}
}
}
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@restorecommerce/grpc-client": "^2.2.5",
"@restorecommerce/kafka-client": "1.2.18",
"@restorecommerce/logger": "^1.3.2",
"@restorecommerce/rc-grpc-clients": "5.1.40",
"@restorecommerce/rc-grpc-clients": "5.1.41",
"@restorecommerce/resource-base-interface": "^1.6.2",
"@restorecommerce/scs-jobs": "0.1.45",
"@restorecommerce/service-config": "^1.0.16",
Expand Down
22 changes: 22 additions & 0 deletions src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1596,6 +1596,28 @@ export class UserService extends ServiceBase<UserListResponse, UserList> impleme
return returnOperationStatus(400, `Password is too weak The password score is ${resultPasswordChecker.score}/4, minimum score is ${minScore}. Suggestions: ${resultPasswordChecker.feedback.suggestions} & ${resultPasswordChecker.feedback.warning} User ID ${user.id}`);
}

if (this.cfg.get('service:passwordHistoryEnabled')) {
if (!('password_hash_history' in user)) {
user.password_hash_history = [];
}

if (this.cfg.get('service:passwordHistoryEnforcement')) {
for (const old_hash of user.password_hash_history) {
if (!password.verify(old_hash, newPw)) {
logger.error(`This password has recently been used. User ID:`, user.id);
return returnOperationStatus(400, `This password has recently been used. User ID ${user.id}`);
}
}
}

user.password_hash_history.unshift(user.password_hash);

const limit = this.cfg.get('service:passwordHistorySize');
if (limit > 0) {
user.password_hash_history = user.password_hash_history.slice(0, limit);
}
}

user.password_hash = password.hash(newPw);
const updateStatus = await super.update(UserList.fromPartial({
items: [user]
Expand Down
5 changes: 4 additions & 1 deletion test/cfg/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
"passwordMinLength": 12,
"data": {
"url": "https://www.google.com/"
}
},
"passwordHistoryEnabled": true,
"passwordHistorySize": 3,
"passwordHistoryEnforcement": true
},
"logger": {
"console": {
Expand Down
37 changes: 37 additions & 0 deletions test/service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,43 @@ describe('testing identity-srv', () => {
upUser.activation_code!.should.be.empty();
upUser.password_hash!.should.not.equal(pwHashA);
});

it('should fail to change the password if it was used before', async function changePasswordFail(): Promise<void> {
// store token to Redis as passwordChange looks up the user based on token (as this operation is for logged in user)
let expires_in = new Date(); // set expires_in to +1 day
expires_in.setDate(expires_in.getDate() + 1);
let userWithToken = {
name: 'test.user1', // user registered initially, storing with token in DB
first_name: 'test',
last_name: 'user',
password: 'CNQJrH%43KAayeDpf3h',
email: '[email protected]',
token: 'user-token',
tokens: [{
token: 'user-token',
expires_in
}]
};
const redisConfig = cfg.get('redis');
// for findByToken
redisConfig.database = cfg.get('redis:db-indexes:db-findByToken') || 0;
tokenRedisClient = RedisCreateClient(redisConfig);
tokenRedisClient.on('error', (err) => logger.error('Redis client error in token cache store', err));
await tokenRedisClient.connect();
// store user with tokens and role associations to Redis index `db-findByToken`
await tokenRedisClient.set('user-token', JSON.stringify(userWithToken));

this.timeout(30000);
const changeResult = await (userService.changePassword({
password: 'CNQJrH%44KAayeDpf3h',
new_password: 'CNQJrH%43KAayeDpf3h',
subject: { token: 'user-token' }
}));
should.exist(changeResult);
should.exist(changeResult.operation_status);
changeResult.operation_status!.code!.should.equal(400);
changeResult.operation_status!.message!.should.match(/This password has recently been used. User ID .*/);
});
});

describe('calling changeEmail', function changeEmailId(): void {
Expand Down

0 comments on commit ecf1041

Please sign in to comment.