2008-04-26 08:31:52 +02:00
|
|
|
<?php
|
|
|
|
|
2016-06-23 01:37:22 +02:00
|
|
|
namespace SilverStripe\Security;
|
|
|
|
|
2017-05-17 07:40:13 +02:00
|
|
|
use SilverStripe\Core\Config\Configurable;
|
2018-01-24 23:10:09 +01:00
|
|
|
use SilverStripe\Core\Extensible;
|
2017-05-17 07:40:13 +02:00
|
|
|
use SilverStripe\Core\Injector\Injectable;
|
2018-01-24 23:10:09 +01:00
|
|
|
use SilverStripe\Dev\Deprecation;
|
2016-06-15 06:03:16 +02:00
|
|
|
use SilverStripe\ORM\ValidationResult;
|
|
|
|
|
2008-04-26 08:31:52 +02:00
|
|
|
/**
|
|
|
|
* This class represents a validator for member passwords.
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2008-04-26 08:31:52 +02:00
|
|
|
* <code>
|
|
|
|
* $pwdVal = new PasswordValidator();
|
|
|
|
* $pwdValidator->minLength(7);
|
|
|
|
* $pwdValidator->checkHistoricalPasswords(6);
|
2013-10-15 12:44:50 +02:00
|
|
|
* $pwdValidator->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"));
|
2014-08-15 08:53:05 +02:00
|
|
|
*
|
2008-04-26 08:31:52 +02:00
|
|
|
* Member::set_password_validator($pwdValidator);
|
|
|
|
* </code>
|
|
|
|
*/
|
2017-05-17 07:40:13 +02:00
|
|
|
class PasswordValidator
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-05-17 07:40:13 +02:00
|
|
|
use Injectable;
|
|
|
|
use Configurable;
|
2018-01-24 23:10:09 +01:00
|
|
|
use Extensible;
|
2013-10-21 19:10:37 +02:00
|
|
|
|
2017-05-17 07:40:13 +02:00
|
|
|
/**
|
|
|
|
* @config
|
|
|
|
* @var array
|
|
|
|
*/
|
2018-01-24 23:10:09 +01:00
|
|
|
private static $character_strength_tests = [
|
2016-11-23 06:09:10 +01:00
|
|
|
'lowercase' => '/[a-z]/',
|
|
|
|
'uppercase' => '/[A-Z]/',
|
|
|
|
'digits' => '/[0-9]/',
|
|
|
|
'punctuation' => '/[^A-Za-z0-9]/',
|
2018-01-24 23:10:09 +01:00
|
|
|
];
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2018-01-24 23:10:09 +01:00
|
|
|
/**
|
|
|
|
* @config
|
2018-04-18 00:35:31 +02:00
|
|
|
* @var int
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
private static $min_length = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @config
|
2018-04-18 00:35:31 +02:00
|
|
|
* @var int
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
private static $min_test_score = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @config
|
2018-04-18 00:35:31 +02:00
|
|
|
* @var int
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
private static $historic_count = null;
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* @var int
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
protected $minLength = null;
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* @var int
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
protected $minScore = null;
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* @var string[]
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
protected $testNames = null;
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* @var int
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
protected $historicalPasswordCount = null;
|
2008-04-26 08:31:52 +02:00
|
|
|
|
2016-11-23 06:09:10 +01:00
|
|
|
/**
|
2018-01-24 23:10:09 +01:00
|
|
|
* @deprecated 5.0
|
2016-11-23 06:09:10 +01:00
|
|
|
* Minimum password length
|
|
|
|
*
|
|
|
|
* @param int $minLength
|
|
|
|
* @return $this
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public function minLength($minLength)
|
|
|
|
{
|
2018-01-24 23:10:09 +01:00
|
|
|
Deprecation::notice('5.0', 'Use ->setMinLength($value) instead.');
|
|
|
|
return $this->setMinLength($minLength);
|
2016-11-23 06:09:10 +01:00
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2016-11-23 06:09:10 +01:00
|
|
|
/**
|
2018-01-24 23:10:09 +01:00
|
|
|
* @deprecated 5.0
|
2016-11-23 06:09:10 +01:00
|
|
|
* Check the character strength of the password.
|
|
|
|
*
|
|
|
|
* Eg: $this->characterStrength(3, array("lowercase", "uppercase", "digits", "punctuation"))
|
|
|
|
*
|
|
|
|
* @param int $minScore The minimum number of character tests that must pass
|
2018-04-18 00:35:31 +02:00
|
|
|
* @param string[] $testNames The names of the tests to perform
|
2016-11-23 06:09:10 +01:00
|
|
|
* @return $this
|
|
|
|
*/
|
2018-01-24 23:10:09 +01:00
|
|
|
public function characterStrength($minScore, $testNames = null)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2018-01-24 23:10:09 +01:00
|
|
|
Deprecation::notice(
|
|
|
|
'5.0',
|
|
|
|
'Use ->setMinTestScore($score) and ->setTextNames($names) instead.'
|
|
|
|
);
|
2018-04-18 00:35:31 +02:00
|
|
|
$this->setMinTestScore($minScore);
|
|
|
|
if ($testNames) {
|
|
|
|
$this->setTestNames($testNames);
|
|
|
|
}
|
|
|
|
return $this;
|
2016-11-23 06:09:10 +01:00
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2016-11-23 06:09:10 +01:00
|
|
|
/**
|
2018-01-24 23:10:09 +01:00
|
|
|
* @deprecated 5.0
|
2016-11-23 06:09:10 +01:00
|
|
|
* Check a number of previous passwords that the user has used, and don't let them change to that.
|
|
|
|
*
|
|
|
|
* @param int $count
|
|
|
|
* @return $this
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public function checkHistoricalPasswords($count)
|
2018-01-24 23:10:09 +01:00
|
|
|
{
|
|
|
|
Deprecation::notice('5.0', 'Use ->setHistoricCount($value) instead.');
|
|
|
|
return $this->setHistoricCount($count);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* @return int
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
public function getMinLength()
|
|
|
|
{
|
|
|
|
if ($this->minLength !== null) {
|
|
|
|
return $this->minLength;
|
|
|
|
}
|
|
|
|
return $this->config()->get('min_length');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* @param int $minLength
|
2018-01-24 23:10:09 +01:00
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setMinLength($minLength)
|
|
|
|
{
|
|
|
|
$this->minLength = $minLength;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return integer
|
|
|
|
*/
|
|
|
|
public function getMinTestScore()
|
|
|
|
{
|
|
|
|
if ($this->minScore !== null) {
|
|
|
|
return $this->minScore;
|
|
|
|
}
|
|
|
|
return $this->config()->get('min_test_score');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* @param int $minScore
|
2018-01-24 23:10:09 +01:00
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setMinTestScore($minScore)
|
|
|
|
{
|
|
|
|
$this->minScore = $minScore;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* Gets the list of tests to use for this validator
|
|
|
|
*
|
|
|
|
* @return string[]
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
public function getTestNames()
|
|
|
|
{
|
|
|
|
if ($this->testNames !== null) {
|
|
|
|
return $this->testNames;
|
|
|
|
}
|
|
|
|
return array_keys(array_filter($this->getTests()));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* Set list of tests to use for this validator
|
|
|
|
*
|
|
|
|
* @param string[] $testNames
|
2018-01-24 23:10:09 +01:00
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setTestNames($testNames)
|
|
|
|
{
|
|
|
|
$this->testNames = $testNames;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* @return int
|
2018-01-24 23:10:09 +01:00
|
|
|
*/
|
|
|
|
public function getHistoricCount()
|
|
|
|
{
|
|
|
|
if ($this->historicalPasswordCount !== null) {
|
|
|
|
return $this->historicalPasswordCount;
|
|
|
|
}
|
|
|
|
return $this->config()->get('historic_count');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* @param int $count
|
2018-01-24 23:10:09 +01:00
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setHistoricCount($count)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2016-11-23 06:09:10 +01:00
|
|
|
$this->historicalPasswordCount = $count;
|
|
|
|
return $this;
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2018-01-24 23:10:09 +01:00
|
|
|
/**
|
2018-04-18 00:35:31 +02:00
|
|
|
* Gets all possible tests
|
|
|
|
*
|
2018-01-24 23:10:09 +01:00
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getTests()
|
|
|
|
{
|
|
|
|
return $this->config()->get('character_strength_tests');
|
|
|
|
}
|
|
|
|
|
2016-11-23 06:09:10 +01:00
|
|
|
/**
|
|
|
|
* @param String $password
|
|
|
|
* @param Member $member
|
|
|
|
* @return ValidationResult
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
public function validate($password, $member)
|
|
|
|
{
|
2016-11-23 06:09:10 +01:00
|
|
|
$valid = ValidationResult::create();
|
2013-06-08 00:48:27 +02:00
|
|
|
|
2018-01-24 23:10:09 +01:00
|
|
|
$minLength = $this->getMinLength();
|
|
|
|
if ($minLength && strlen($password) < $minLength) {
|
|
|
|
$error = _t(
|
2018-04-18 00:35:31 +02:00
|
|
|
__CLASS__ . '.TOOSHORT',
|
2018-01-24 23:10:09 +01:00
|
|
|
'Password is too short, it must be {minimum} or more characters long',
|
2019-05-13 03:11:04 +02:00
|
|
|
['minimum' => $minLength]
|
2018-01-24 23:10:09 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
$valid->addError($error, 'bad', 'TOO_SHORT');
|
2016-11-23 06:09:10 +01:00
|
|
|
}
|
2008-04-26 08:31:52 +02:00
|
|
|
|
2018-01-24 23:10:09 +01:00
|
|
|
$minTestScore = $this->getMinTestScore();
|
|
|
|
if ($minTestScore) {
|
|
|
|
$missedTests = [];
|
|
|
|
$testNames = $this->getTestNames();
|
|
|
|
$tests = $this->getTests();
|
|
|
|
|
|
|
|
foreach ($testNames as $name) {
|
|
|
|
if (preg_match($tests[$name], $password)) {
|
|
|
|
continue;
|
2016-11-23 06:09:10 +01:00
|
|
|
}
|
2018-01-24 23:10:09 +01:00
|
|
|
$missedTests[] = _t(
|
2018-04-18 00:35:31 +02:00
|
|
|
__CLASS__ . '.STRENGTHTEST' . strtoupper($name),
|
2018-01-24 23:10:09 +01:00
|
|
|
$name,
|
|
|
|
'The user needs to add this to their password for more complexity'
|
|
|
|
);
|
2016-11-23 06:09:10 +01:00
|
|
|
}
|
2013-06-08 00:48:27 +02:00
|
|
|
|
2018-04-18 00:35:31 +02:00
|
|
|
$score = count($testNames) - count($missedTests);
|
|
|
|
if ($missedTests && $score < $minTestScore) {
|
2018-01-24 23:10:09 +01:00
|
|
|
$error = _t(
|
2018-04-18 00:35:31 +02:00
|
|
|
__CLASS__ . '.LOWCHARSTRENGTH',
|
2018-01-24 23:10:09 +01:00
|
|
|
'Please increase password strength by adding some of the following characters: {chars}',
|
|
|
|
['chars' => implode(', ', $missedTests)]
|
2016-11-23 06:09:10 +01:00
|
|
|
);
|
2018-01-24 23:10:09 +01:00
|
|
|
$valid->addError($error, 'bad', 'LOW_CHARACTER_STRENGTH');
|
2016-11-23 06:09:10 +01:00
|
|
|
}
|
|
|
|
}
|
2013-06-08 00:48:27 +02:00
|
|
|
|
2018-01-24 23:10:09 +01:00
|
|
|
$historicCount = $this->getHistoricCount();
|
|
|
|
if ($historicCount) {
|
2016-11-23 06:09:10 +01:00
|
|
|
$previousPasswords = MemberPassword::get()
|
|
|
|
->where(array('"MemberPassword"."MemberID"' => $member->ID))
|
|
|
|
->sort('"Created" DESC, "ID" DESC')
|
2018-01-24 23:10:09 +01:00
|
|
|
->limit($historicCount);
|
2016-11-23 06:09:10 +01:00
|
|
|
/** @var MemberPassword $previousPassword */
|
|
|
|
foreach ($previousPasswords as $previousPassword) {
|
|
|
|
if ($previousPassword->checkPassword($password)) {
|
2018-04-18 00:35:31 +02:00
|
|
|
$error = _t(
|
|
|
|
__CLASS__ . '.PREVPASSWORD',
|
2018-01-24 23:10:09 +01:00
|
|
|
'You\'ve already used that password in the past, please choose a new password'
|
2016-11-23 06:09:10 +01:00
|
|
|
);
|
2018-01-24 23:10:09 +01:00
|
|
|
$valid->addError($error, 'bad', 'PREVIOUS_PASSWORD');
|
2016-11-23 06:09:10 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-08-15 08:53:05 +02:00
|
|
|
|
2018-01-24 23:10:09 +01:00
|
|
|
$this->extend('updateValidatePassword', $password, $member, $valid, $this);
|
|
|
|
|
2016-11-23 06:09:10 +01:00
|
|
|
return $valid;
|
|
|
|
}
|
2012-03-24 04:04:52 +01:00
|
|
|
}
|