[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 PasswordPoliceClasspull/10829/head
parent
1070edfe29
commit
3ed656b8c8
@ -0,0 +1,91 @@ |
||||
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; |
@ -0,0 +1,13 @@ |
||||
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); |
@ -0,0 +1,206 @@ |
||||
/* 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); |
||||
}); |
||||
}); |
||||
}); |
Loading…
Reference in new issue