Skip to content

Commit

Permalink
feat: return all broken password policies at once (#29593)
Browse files Browse the repository at this point in the history
  • Loading branch information
tapiarafael committed Jul 21, 2023
1 parent ad08c26 commit 7070f00
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .changeset/soft-yaks-matter.md
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": minor
---

feat: return all broken password policies at once
59 changes: 35 additions & 24 deletions apps/meteor/app/lib/server/lib/PasswordPolicyClass.js
Expand Up @@ -43,15 +43,16 @@ class PasswordPolicy {
return this._forbidRepeatingCharactersCount;
}

error(error, message) {
error(error, message, reasons) {
if (this.throwError) {
throw new Meteor.Error(error, message);
throw new Meteor.Error(error, message, reasons);
}

return false;
}

validate(password) {
const reasons = [];
if (typeof password !== 'string' || !password.trim().length) {
return this.error('error-password-policy-not-met', "The password provided does not meet the server's password policy.");
}
Expand All @@ -61,46 +62,56 @@ class PasswordPolicy {
}

if (this.minLength >= 1 && password.length < this.minLength) {
return this.error('error-password-policy-not-met-minLength', 'The password does not meet the minimum length password policy.');
reasons.push({
error: 'error-password-policy-not-met-minLength',
message: 'The password does not meet the minimum length password policy.',
});
}

if (this.maxLength >= 1 && password.length > this.maxLength) {
return this.error('error-password-policy-not-met-maxLength', 'The password does not meet the maximum length password policy.');
reasons.push({
error: 'error-password-policy-not-met-maxLength',
message: 'The password does not meet the maximum length password policy.',
});
}

if (this.forbidRepeatingCharacters && this.regex.forbiddingRepeatingCharacters.test(password)) {
return this.error(
'error-password-policy-not-met-repeatingCharacters',
'The password contains repeating characters which is against the password policy.',
);
reasons.push({
error: 'error-password-policy-not-met-repeatingCharacters',
message: 'The password contains repeating characters which is against the password policy.',
});
}

if (this.mustContainAtLeastOneLowercase && !this.regex.mustContainAtLeastOneLowercase.test(password)) {
return this.error(
'error-password-policy-not-met-oneLowercase',
'The password does not contain at least one lowercase character which is against the password policy.',
);
reasons.push({
error: 'error-password-policy-not-met-oneLowercase',
message: 'The password does not contain at least one lowercase character which is against the password policy.',
});
}

if (this.mustContainAtLeastOneUppercase && !this.regex.mustContainAtLeastOneUppercase.test(password)) {
return this.error(
'error-password-policy-not-met-oneUppercase',
'The password does not contain at least one uppercase character which is against the password policy.',
);
reasons.push({
error: 'error-password-policy-not-met-oneUppercase',
message: 'The password does not contain at least one uppercase character which is against the password policy.',
});
}

if (this.mustContainAtLeastOneNumber && !this.regex.mustContainAtLeastOneNumber.test(password)) {
return this.error(
'error-password-policy-not-met-oneNumber',
'The password does not contain at least one numerical character which is against the password policy.',
);
reasons.push({
error: 'error-password-policy-not-met-oneNumber',
message: 'The password does not contain at least one numerical character which is against the password policy.',
});
}

if (this.mustContainAtLeastOneSpecialCharacter && !this.regex.mustContainAtLeastOneSpecialCharacter.test(password)) {
return this.error(
'error-password-policy-not-met-oneSpecial',
'The password does not contain at least one special character which is against the password policy.',
);
reasons.push({
error: 'error-password-policy-not-met-oneSpecial',
message: 'The password does not contain at least one special character which is against the password policy.',
});
}

if (reasons.length) {
return this.error('error-password-policy-not-met', `The password provided does not meet the server's password policy.`, reasons);
}

return true;
Expand Down
216 changes: 215 additions & 1 deletion apps/meteor/tests/end-to-end/api/01-users.js
Expand Up @@ -20,6 +20,7 @@ import { customFieldText, clearCustomFields, setCustomFields } from '../../data/
import { updatePermission, updateSetting } from '../../data/permissions.helper';
import { createUser, login, deleteUser, getUserStatus } from '../../data/users.helper.js';
import { createRoom } from '../../data/rooms.helper';
import { sleep } from '../../../lib/utils/sleep';

async function createChannel(userCredentials, name) {
const res = await request.post(api('channels.create')).set(userCredentials).send({
Expand Down Expand Up @@ -1508,6 +1509,7 @@ describe('[Users]', function () {
});

const newPassword = `${password}test`;
const currentPassword = crypto.createHash('sha256').update(password, 'utf8').digest('hex');
const editedUsername = `basicInfo.name${+new Date()}`;
const editedName = `basic-info-test-name${+new Date()}`;
const editedEmail = `test${+new Date()}@mail.com`;
Expand Down Expand Up @@ -1538,7 +1540,7 @@ describe('[Users]', function () {
data: {
name: editedName,
username: editedUsername,
currentPassword: crypto.createHash('sha256').update(password, 'utf8').digest('hex'),
currentPassword,
newPassword,
},
})
Expand Down Expand Up @@ -1764,6 +1766,218 @@ describe('[Users]', function () {

await updateSetting('Accounts_AllowPasswordChange', true);
});

describe('[Password Policy]', () => {
before(async () => {
await updateSetting('Accounts_Password_Policy_Enabled', true);
await updateSetting('Accounts_TwoFactorAuthentication_Enabled', false);

await sleep(500);
});

after(async () => {
await updateSetting('Accounts_Password_Policy_Enabled', false);
await updateSetting('Accounts_TwoFactorAuthentication_Enabled', true);
});

it('should throw an error if the password length is less than the minimum length', async () => {
const expectedError = {
error: 'error-password-policy-not-met-minLength',
message: 'The password does not meet the minimum length password policy.',
};

await request
.post(api('users.updateOwnBasicInfo'))
.set(userCredentials)
.send({
data: {
currentPassword,
newPassword: '2',
},
})
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body.errorType).to.be.equal('error-password-policy-not-met');
expect(res.body.details).to.be.an('array').that.deep.includes(expectedError);
})
.expect(400);
});

it('should throw an error if the password length is greater than the maximum length', async () => {
await updateSetting('Accounts_Password_Policy_MaxLength', 5);
await sleep(500);

const expectedError = {
error: 'error-password-policy-not-met-maxLength',
message: 'The password does not meet the maximum length password policy.',
};

await request
.post(api('users.updateOwnBasicInfo'))
.set(userCredentials)
.send({
data: {
currentPassword,
newPassword: 'Abc@12345678',
},
})
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body.errorType).to.be.equal('error-password-policy-not-met');
expect(res.body.details).to.be.an('array').that.deep.includes(expectedError);
})
.expect(400);

await updateSetting('Accounts_Password_Policy_MaxLength', -1);
});

it('should throw an error if the password contains repeating characters', async () => {
const expectedError = {
error: 'error-password-policy-not-met-repeatingCharacters',
message: 'The password contains repeating characters which is against the password policy.',
};

await request
.post(api('users.updateOwnBasicInfo'))
.set(userCredentials)
.send({
data: {
currentPassword,
newPassword: 'A@123aaaa',
},
})
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body.errorType).to.be.equal('error-password-policy-not-met');
expect(res.body.details).to.be.an('array').that.deep.includes(expectedError);
})
.expect(400);
});

it('should throw an error if the password does not contain at least one lowercase character', async () => {
const expectedError = {
error: 'error-password-policy-not-met-oneLowercase',
message: 'The password does not contain at least one lowercase character which is against the password policy.',
};

await request
.post(api('users.updateOwnBasicInfo'))
.set(userCredentials)
.send({
data: {
currentPassword,
newPassword: 'PASSWORD@123',
},
})
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body.errorType).to.be.equal('error-password-policy-not-met');
expect(res.body.details).to.be.an('array').that.deep.includes(expectedError);
})
.expect(400);
});

it('should throw an error if the password does not contain at least one uppercase character', async () => {
const expectedError = {
error: 'error-password-policy-not-met-oneUppercase',
message: 'The password does not contain at least one uppercase character which is against the password policy.',
};

await request
.post(api('users.updateOwnBasicInfo'))
.set(userCredentials)
.send({
data: {
currentPassword,
newPassword: 'password@123',
},
})
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body.errorType).to.be.equal('error-password-policy-not-met');
expect(res.body.details).to.be.an('array').that.deep.includes(expectedError);
})
.expect(400);
});

it('should throw an error if the password does not contain at least one numerical character', async () => {
const expectedError = {
error: 'error-password-policy-not-met-oneNumber',
message: 'The password does not contain at least one numerical character which is against the password policy.',
};

await request
.post(api('users.updateOwnBasicInfo'))
.set(userCredentials)
.send({
data: {
currentPassword,
newPassword: 'Password@',
},
})
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body.errorType).to.be.equal('error-password-policy-not-met');
expect(res.body.details).to.be.an('array').that.deep.includes(expectedError);
})
.expect(400);
});

it('should throw an error if the password does not contain at least one special character', async () => {
const expectedError = {
error: 'error-password-policy-not-met-oneSpecial',
message: 'The password does not contain at least one special character which is against the password policy.',
};

await request
.post(api('users.updateOwnBasicInfo'))
.set(userCredentials)
.send({
data: {
currentPassword,
newPassword: 'Password123',
},
})
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error');
expect(res.body.errorType).to.be.equal('error-password-policy-not-met');
expect(res.body.details).to.be.an('array').that.deep.includes(expectedError);
})
.expect(400);
});

it('should be able to update if the password meets all the validation rules', async () => {
await request
.post(api('users.updateOwnBasicInfo'))
.set(userCredentials)
.send({
data: {
currentPassword,
newPassword: '123Abc@!',
},
})
.expect('Content-Type', 'application/json')
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('user');
})
.expect(200);
});
});
});

// TODO check for all response fields
Expand Down

0 comments on commit 7070f00

Please sign in to comment.