Skip to content
Snippets Groups Projects
Commit 3ed656b8 authored by Bradley Hilton's avatar Bradley Hilton Committed by Rodrigo Nascimento
Browse files

[NEW] Implement a local password policy (#9857)

* Implement a local password policy

* Improve ValidatePasswordPolicy and create tests

* Validate user’s password on method saveUserProfile

* Fix typo PasswordPoliceClass
parent 1070edfe
No related branches found
No related tags found
No related merge requests found
......@@ -779,6 +779,14 @@
"error-no-tokens-for-this-user": "There are no tokens for this user",
"error-not-allowed": "Not allowed",
"error-not-authorized": "Not authorized",
"error-password-policy-not-met": "Password does not meet the server's policy",
"error-password-policy-not-met-minLength": "Password does not meet the server's policy of minimum length (password too short)",
"error-password-policy-not-met-maxLength": "Password does not meet the server's policy of maximum length (password too long)",
"error-password-policy-not-met-repeatingCharacters": "Password not not meet the server's policy of forbidden repeating characters (you have too many of the same characters next to each other)",
"error-password-policy-not-met-oneLowercase": "Password does not meet the server's policy of at least one lowercase character",
"error-password-policy-not-met-oneUppercase": "Password does not meet the server's policy of at least one uppercase character",
"error-password-policy-not-met-oneNumber": "Password does not meet the server's policy of at least one numerical character",
"error-password-policy-not-met-oneSpecial": "Password does not meet the server's policy of at least one special character",
"error-push-disabled": "Push is disabled",
"error-remove-last-owner": "This is the last owner. Please set a new owner before removing this one.",
"error-role-in-use": "Cannot delete role because it's in use",
......@@ -1599,6 +1607,25 @@
"Password": "Password",
"Password_Change_Disabled": "Your Rocket.Chat administrator has disabled the changing of passwords",
"Password_changed_successfully": "Password changed successfully",
"Password_Policy": "Password Policy",
"Accounts_Password_Policy_Enabled": "Enable Password Policy",
"Accounts_Password_Policy_Enabled_Description": "When enabled, user passwords must adhere to the policies set forth. Note: this only applies to new passwords, not existing passwords.",
"Accounts_Password_Policy_MinLength": "Minimum Length",
"Accounts_Password_Policy_MinLength_Description": "Ensures that passwords must have at least this amount of characters. Use `-1` to disable.",
"Accounts_Password_Policy_MaxLength": "Maximum Length",
"Accounts_Password_Policy_MaxLength_Description": "Ensures that passwords do not have more than this amount of characters. Use `-1` to disable.",
"Accounts_Password_Policy_ForbidRepeatingCharacters": "Forbid Repeating Characters",
"Accounts_Password_Policy_ForbidRepeatingCharacters_Description": "Ensures passwords do not contain the same character repeating next to each other.",
"Accounts_Password_Policy_ForbidRepeatingCharactersCount": "Max Repeating Characters",
"Accounts_Password_Policy_ForbidRepeatingCharactersCount_Description": "The amount of times a character can be repeating before it is not allowed.",
"Accounts_Password_Policy_AtLeastOneLowercase": "At Least One Lowercase",
"Accounts_Password_Policy_AtLeastOneLowercase_Description": "Enforce that a password contain at least one lowercase character.",
"Accounts_Password_Policy_AtLeastOneUppercase": "At Least One Uppercase",
"Accounts_Password_Policy_AtLeastOneUppercase_Description": "Enforce that a password contain at least one lowercase character.",
"Accounts_Password_Policy_AtLeastOneNumber": "At Least One Number",
"Accounts_Password_Policy_AtLeastOneNumber_Description": "Enforce that a password contain at least one numerical character.",
"Accounts_Password_Policy_AtLeastOneSpecialCharacter": "At Least One Symbol",
"Accounts_Password_Policy_AtLeastOneSpecialCharacter_Description": "Enforce that a password contain at least one special character.",
"Past_Chats": "Past Chats",
"Payload": "Payload",
"People": "People",
......
......@@ -115,6 +115,7 @@ Package.onUse(function(api) {
api.addFiles('server/lib/roomTypes.js', 'server');
api.addFiles('server/lib/sendNotificationsOnMessage.js', 'server');
api.addFiles('server/lib/validateEmailDomain.js', 'server');
api.addFiles('server/lib/passwordPolicy.js', 'server');
// SERVER MODELS
api.addFiles('server/models/_Base.js', 'server');
......
......@@ -187,7 +187,7 @@ RocketChat.saveUser = function(userId, userData) {
RocketChat.setEmail(userData._id, userData.email, shouldSendVerificationEmailToUser);
}
if (userData.password && userData.password.trim() && RocketChat.authz.hasPermission(userId, 'edit-other-user-password')) {
if (userData.password && userData.password.trim() && RocketChat.authz.hasPermission(userId, 'edit-other-user-password') && RocketChat.passwordPolicy.validate(userData.password)) {
Accounts.setPassword(userData._id, userData.password.trim());
}
......
class PasswordPolicy {
constructor({
enabled = false,
minLength = -1,
maxLength = -1,
forbidRepeatingCharacters = false,
forbidRepeatingCharactersCount = 3, //the regex is this number minus one
mustContainAtLeastOneLowercase = false, // /[A-Z]{3,}/ could do this instead of at least one
mustContainAtLeastOneUppercase = false,
mustContainAtLeastOneNumber = false,
mustContainAtLeastOneSpecialCharacter = false,
throwError = true
} = {}) {
this.regex = {
mustContainAtLeastOneLowercase: new RegExp('[a-z]'),
mustContainAtLeastOneUppercase: new RegExp('[A-Z]'),
mustContainAtLeastOneNumber: new RegExp('[0-9]'),
mustContainAtLeastOneSpecialCharacter: new RegExp('[^A-Za-z0-9 ]')
};
this.enabled = enabled;
this.minLength = minLength;
this.maxLength = maxLength;
this.forbidRepeatingCharacters = forbidRepeatingCharacters;
this.forbidRepeatingCharactersCount = forbidRepeatingCharactersCount;
this.mustContainAtLeastOneLowercase = mustContainAtLeastOneLowercase;
this.mustContainAtLeastOneUppercase = mustContainAtLeastOneUppercase;
this.mustContainAtLeastOneNumber = mustContainAtLeastOneNumber;
this.mustContainAtLeastOneSpecialCharacter = mustContainAtLeastOneSpecialCharacter;
this.throwError = throwError;
}
set forbidRepeatingCharactersCount(value) {
this._forbidRepeatingCharactersCount = value;
this.regex.forbiddingRepeatingCharacters = new RegExp(`(.)\\1{${ this.forbidRepeatingCharactersCount },}`);
}
get forbidRepeatingCharactersCount() {
return this._forbidRepeatingCharactersCount;
}
error(error, message) {
if (this.throwError) {
throw new Meteor.Error(error, message);
}
return false;
}
validate(password) {
if (!this.enabled) {
return true;
}
if (!password || typeof password !== 'string' || !password.length) {
return this.error('error-password-policy-not-met', 'The password provided does not meet the server\'s password policy.');
}
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.');
}
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.');
}
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.');
}
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.');
}
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.');
}
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.');
}
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.');
}
return true;
}
}
export default PasswordPolicy;
import PasswordPolicy from './PasswordPolicyClass';
RocketChat.passwordPolicy = new PasswordPolicy();
RocketChat.settings.get('Accounts_Password_Policy_Enabled', (key, value) => RocketChat.passwordPolicy.enabled = value);
RocketChat.settings.get('Accounts_Password_Policy_MinLength', (key, value) => RocketChat.passwordPolicy.minLength = value);
RocketChat.settings.get('Accounts_Password_Policy_MaxLength', (key, value) => RocketChat.passwordPolicy.maxLength = value);
RocketChat.settings.get('Accounts_Password_Policy_ForbidRepeatingCharacters', (key, value) => RocketChat.passwordPolicy.forbidRepeatingCharacters = value);
RocketChat.settings.get('Accounts_Password_Policy_ForbidRepeatingCharactersCount', (key, value) => RocketChat.passwordPolicy.forbidRepeatingCharactersCount = value);
RocketChat.settings.get('Accounts_Password_Policy_AtLeastOneLowercase', (key, value) => RocketChat.passwordPolicy.mustContainAtLeastOneLowercase = value);
RocketChat.settings.get('Accounts_Password_Policy_AtLeastOneUppercase', (key, value) => RocketChat.passwordPolicy.mustContainAtLeastOneUppercase = value);
RocketChat.settings.get('Accounts_Password_Policy_AtLeastOneNumber', (key, value) => RocketChat.passwordPolicy.mustContainAtLeastOneNumber = value);
RocketChat.settings.get('Accounts_Password_Policy_AtLeastOneSpecialCharacter', (key, value) => RocketChat.passwordPolicy.mustContainAtLeastOneSpecialCharacter = value);
......@@ -468,6 +468,57 @@ RocketChat.settings.addGroup('Accounts', function() {
type: 'boolean'
});
});
this.section('Password_Policy', function() {
this.add('Accounts_Password_Policy_Enabled', false, {
type: 'boolean'
});
const enableQuery = {
_id: 'Accounts_Password_Policy_Enabled',
value: true
};
this.add('Accounts_Password_Policy_MinLength', 7, {
type: 'int',
enableQuery
});
this.add('Accounts_Password_Policy_MaxLength', -1, {
type: 'int',
enableQuery
});
this.add('Accounts_Password_Policy_ForbidRepeatingCharacters', true, {
type: 'boolean',
enableQuery
});
this.add('Accounts_Password_Policy_ForbidRepeatingCharactersCount', 3, {
type: 'int',
enableQuery
});
this.add('Accounts_Password_Policy_AtLeastOneLowercase', true, {
type: 'boolean',
enableQuery
});
this.add('Accounts_Password_Policy_AtLeastOneUppercase', true, {
type: 'boolean',
enableQuery
});
this.add('Accounts_Password_Policy_AtLeastOneNumber', true, {
type: 'boolean',
enableQuery
});
this.add('Accounts_Password_Policy_AtLeastOneSpecialCharacter', true, {
type: 'boolean',
enableQuery
});
});
});
RocketChat.settings.addGroup('OAuth', function() {
......
/* eslint-env mocha */
import 'babel-polyfill';
import assert from 'assert';
import PasswordPolicyClass from '../server/lib/PasswordPolicyClass';
describe('PasswordPolicyClass', () => {
describe('Default options', () => {
const passwordPolice = new PasswordPolicyClass();
it('should be disabled', () => {
assert.equal(passwordPolice.enabled, false);
});
it('should have minLength = -1', () => {
assert.equal(passwordPolice.minLength, -1);
});
it('should have maxLength = -1', () => {
assert.equal(passwordPolice.maxLength, -1);
});
it('should have forbidRepeatingCharacters = false', () => {
assert.equal(passwordPolice.forbidRepeatingCharacters, false);
});
it('should have forbidRepeatingCharactersCount = 3', () => {
assert.equal(passwordPolice.forbidRepeatingCharactersCount, 3);
});
it('should have mustContainAtLeastOneLowercase = false', () => {
assert.equal(passwordPolice.mustContainAtLeastOneLowercase, false);
});
it('should have mustContainAtLeastOneUppercase = false', () => {
assert.equal(passwordPolice.mustContainAtLeastOneUppercase, false);
});
it('should have mustContainAtLeastOneNumber = false', () => {
assert.equal(passwordPolice.mustContainAtLeastOneNumber, false);
});
it('should have mustContainAtLeastOneSpecialCharacter = false', () => {
assert.equal(passwordPolice.mustContainAtLeastOneSpecialCharacter, false);
});
describe('Password tests with default options', () => {
it('should allow all passwords', () => {
const passwordPolice = new PasswordPolicyClass();
assert.equal(passwordPolice.validate(), true);
assert.equal(passwordPolice.validate(''), true);
assert.equal(passwordPolice.validate('a'), true);
assert.equal(passwordPolice.validate('aaaaaaaaa'), true);
assert.equal(passwordPolice.validate(' '), true);
});
});
});
describe('Password tests with options', () => {
it('should not allow non string or empty', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
throwError: false
});
assert.equal(passwordPolice.validate(), false);
assert.equal(passwordPolice.validate(1), false);
assert.equal(passwordPolice.validate(true), false);
assert.equal(passwordPolice.validate(new Date), false);
assert.equal(passwordPolice.validate(new Function), false);
assert.equal(passwordPolice.validate(''), false);
});
it('should restrict by minLength', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
minLength: 5,
throwError: false
});
assert.equal(passwordPolice.validate('1'), false);
assert.equal(passwordPolice.validate('1234'), false);
assert.equal(passwordPolice.validate('12345'), true);
assert.equal(passwordPolice.validate(' '), true);
});
it('should restrict by maxLength', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
maxLength: 5,
throwError: false
});
assert.equal(passwordPolice.validate('1'), true);
assert.equal(passwordPolice.validate('12345'), true);
assert.equal(passwordPolice.validate('123456'), false);
assert.equal(passwordPolice.validate(' '), false);
});
it('should allow repeated characters', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
forbidRepeatingCharacters: false,
throwError: false
});
assert.equal(passwordPolice.validate('1'), true);
assert.equal(passwordPolice.validate('12345'), true);
assert.equal(passwordPolice.validate('123456'), true);
assert.equal(passwordPolice.validate(' '), true);
assert.equal(passwordPolice.validate('11111111111111'), true);
});
it('should restrict repeated characters', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
forbidRepeatingCharacters: true,
forbidRepeatingCharactersCount: 3,
throwError: false
});
assert.equal(passwordPolice.validate('1'), true);
assert.equal(passwordPolice.validate('11'), true);
assert.equal(passwordPolice.validate('111'), true);
assert.equal(passwordPolice.validate('1111'), false);
assert.equal(passwordPolice.validate(' '), false);
assert.equal(passwordPolice.validate('123456'), true);
});
it('should restrict repeated characters customized', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
forbidRepeatingCharacters: true,
forbidRepeatingCharactersCount: 5,
throwError: false
});
assert.equal(passwordPolice.validate('1'), true);
assert.equal(passwordPolice.validate('11'), true);
assert.equal(passwordPolice.validate('111'), true);
assert.equal(passwordPolice.validate('1111'), true);
assert.equal(passwordPolice.validate('11111'), true);
assert.equal(passwordPolice.validate('111111'), false);
assert.equal(passwordPolice.validate(' '), false);
assert.equal(passwordPolice.validate('123456'), true);
});
it('should contain one lowercase', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
mustContainAtLeastOneLowercase: true,
throwError: false
});
assert.equal(passwordPolice.validate('a'), true);
assert.equal(passwordPolice.validate('aa'), true);
assert.equal(passwordPolice.validate('A'), false);
assert.equal(passwordPolice.validate(' '), false);
assert.equal(passwordPolice.validate('123456'), false);
assert.equal(passwordPolice.validate('AAAAA'), false);
assert.equal(passwordPolice.validate('AAAaAAA'), true);
});
it('should contain one uppercase', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
mustContainAtLeastOneUppercase: true,
throwError: false
});
assert.equal(passwordPolice.validate('a'), false);
assert.equal(passwordPolice.validate('aa'), false);
assert.equal(passwordPolice.validate('A'), true);
assert.equal(passwordPolice.validate(' '), false);
assert.equal(passwordPolice.validate('123456'), false);
assert.equal(passwordPolice.validate('AAAAA'), true);
assert.equal(passwordPolice.validate('AAAaAAA'), true);
});
it('should contain one uppercase', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
mustContainAtLeastOneNumber: true,
throwError: false
});
assert.equal(passwordPolice.validate('a'), false);
assert.equal(passwordPolice.validate('aa'), false);
assert.equal(passwordPolice.validate('A'), false);
assert.equal(passwordPolice.validate(' '), false);
assert.equal(passwordPolice.validate('123456'), true);
assert.equal(passwordPolice.validate('AAAAA'), false);
assert.equal(passwordPolice.validate('AAAaAAA'), false);
assert.equal(passwordPolice.validate('AAAa1AAA'), true);
});
it('should contain one uppercase', () => {
const passwordPolice = new PasswordPolicyClass({
enabled: true,
mustContainAtLeastOneSpecialCharacter: true,
throwError: false
});
assert.equal(passwordPolice.validate('a'), false);
assert.equal(passwordPolice.validate('aa'), false);
assert.equal(passwordPolice.validate('A'), false);
assert.equal(passwordPolice.validate(' '), false);
assert.equal(passwordPolice.validate('123456'), false);
assert.equal(passwordPolice.validate('AAAAA'), false);
assert.equal(passwordPolice.validate('AAAaAAA'), false);
assert.equal(passwordPolice.validate('AAAa1AAA'), false);
assert.equal(passwordPolice.validate('AAAa@AAA'), true);
});
});
});
......@@ -31,6 +31,8 @@ Meteor.methods({
throw new Meteor.Error ('error-user-registration-secret', 'User registration is only allowed via Secret URL', { method: 'registerUser' });
}
RocketChat.passwordPolicy.validate(formData.pass);
RocketChat.validateEmailDomain(formData.email);
const userData = {
......
......@@ -58,6 +58,8 @@ Meteor.methods({
});
}
RocketChat.passwordPolicy.validate(settings.newPassword);
Accounts.setPassword(Meteor.userId(), settings.newPassword, {
logout: false
});
......
......@@ -18,6 +18,8 @@ Meteor.methods({
});
}
RocketChat.passwordPolicy.validate(password);
Accounts.setPassword(userId, password, {
logout: false
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment