Skip to content

Commit

Permalink
NEW Give feedback of password strength
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Oct 3, 2024
1 parent 33929e2 commit 5404f9c
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 1 deletion.
62 changes: 62 additions & 0 deletions src/Forms/ConfirmedPasswordField.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
use SilverStripe\Security\Security;
use SilverStripe\View\HTML;
use Closure;
use SilverStripe\Control\HTTP;
use SilverStripe\Core\Validation\ConstraintValidator;
use Symfony\Component\Validator\Constraints\PasswordStrength;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Security\Validation\PasswordValidator;

/**
* Two masked input fields, checks for matching passwords.
Expand All @@ -24,6 +29,9 @@
*/
class ConfirmedPasswordField extends FormField
{
private static $allowed_actions = [
'strength',
];

/**
* Minimum character length of the password.
Expand Down Expand Up @@ -106,6 +114,8 @@ class ConfirmedPasswordField extends FormField

protected ?PasswordField $passwordField;

protected ?LiteralField $passwordStrengthField;

protected ?PasswordField $confirmPasswordfield;

protected ?HiddenField $hiddenField = null;
Expand All @@ -132,6 +142,10 @@ public function __construct(
"{$name}[_Password]",
$title
),
$this->passwordStrengthField = LiteralField::create(
"{$name}[_PasswordStrength]",
'<div class="passwordstrength"></div>'
),
$this->confirmPasswordfield = PasswordField::create(
"{$name}[_ConfirmPassword]",
(isset($titleConfirmField)) ? $titleConfirmField : _t('SilverStripe\\Security\\Member.CONFIRMPASSWORD', 'Confirm Password')
Expand All @@ -154,6 +168,50 @@ public function __construct(
$this->setValue($value);
}

/**
* Provides feedback for the current and required level of password strength
*/
public function strength(HTTPRequest $request): HTTPResponse
{
$response = HTTPResponse::create();
$json = json_decode($request->getBody(), true);
if (!$json || !array_key_exists('password', $json) || !$request->isPOST()) {
$response->setStatusCode(400);
return $response;
}
$password = $json['password'];
$validator = PasswordValidator::create();
if ($this->getRequireStrongPassword()) {
$requiredStrength = $this->getMinPasswordStrength();
} else {
$requiredStrength = $validator->getRequiredStrength();
}
$requiredLevel = $validator->getStrengthLevel($requiredStrength);
$passwordStrength = $validator->evaluateStrength($password);
$passwordLevel = $validator->getStrengthLevel($passwordStrength);
if ($passwordStrength < $requiredStrength) {
$valid = false;
$message = _t(
__CLASS__ . '.STRENGTH',
'Password strength is {passwordLevel}, must be at least {requiredLevel}',
['passwordLevel' => $passwordLevel, 'requiredLevel' => $requiredLevel]
);
} else {
$valid = true;
$message = _t(
__CLASS__ . '.STRENGTH',
'Password strength is {passwordLevel}',
['passwordLevel' => $passwordLevel]
);
}
$body = json_encode((object) [
'valid' => $valid,
'message' => $message,
]);
$response->setBody($body);
return $response;
}

public function Title()
{
// Title is displayed on nested field, not on the top level field
Expand All @@ -173,6 +231,7 @@ public function setTitle($title)
*/
public function Field($properties = [])
{
$canEvaluateStrength = PasswordValidator::singleton()->canEvaluateStrength();
// Build inner content
$fieldContent = '';
foreach ($this->getChildren() as $field) {
Expand All @@ -184,6 +243,9 @@ public function Field($properties = [])
$field->setAttribute($name, $value);
}
}
if ($canEvaluateStrength && is_a($field, PasswordField::class)) {
$field->setAttribute('data-strengthurl', $this->Link('strength'));
}

$fieldContent .= $field->FieldHolder(['AttributesHTML' => $this->getAttributesHTMLForChild($field)]);
}
Expand Down
6 changes: 6 additions & 0 deletions src/Forms/PasswordField.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

namespace SilverStripe\Forms;

use SilverStripe\Control\Director;
use SilverStripe\Security\Security;
use SilverStripe\Security\Validation\PasswordValidator;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;

/**
* Password input field.
*/
Expand Down
33 changes: 32 additions & 1 deletion src/Security/Validation/EntropyPasswordValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class EntropyPasswordValidator extends PasswordValidator
* The strength of a valid password.
* See https://symfony.com/doc/current/reference/constraints/PasswordStrength.html#minscore
*/
private static int $password_strength = PasswordStrength::STRENGTH_STRONG;
private static int $password_strength = PasswordStrength::STRENGTH_MEDIUM;

public function validate(string $password, Member $member): ValidationResult
{
Expand All @@ -30,4 +30,35 @@ public function validate(string $password, Member $member): ValidationResult
$this->extend('updateValidatePassword', $password, $member, $result, $this);
return $result;
}

public function getRequiredStrength(): int
{
return static::config()->get('password_strength');
}

public function canEvaluateStrength(): bool
{
return true;
}

public function evaluateStrength(string $password): int
{
$strengths = [
PasswordStrength::STRENGTH_WEAK,
PasswordStrength::STRENGTH_MEDIUM,
PasswordStrength::STRENGTH_STRONG,
PasswordStrength::STRENGTH_VERY_STRONG,
];
// STRENGTH_VERY_WEAK is not validatable, it's just the default value
$lastPassedStrength = PasswordStrength::STRENGTH_VERY_WEAK;
foreach ($strengths as $strength) {
$result = ConstraintValidator::validate($password, new PasswordStrength(minScore: $strength));
if ($result->isValid()) {
$lastPassedStrength = $strength;
} else {
break;
}
}
return $lastPassedStrength;
}
}
60 changes: 60 additions & 0 deletions src/Security/Validation/PasswordValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use SilverStripe\Core\Validation\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\Security\MemberPassword;
use Symfony\Component\Validator\Constraints\PasswordStrength;

/**
* Abstract validator with functionality for checking for reusing old passwords.
Expand Down Expand Up @@ -69,4 +70,63 @@ public function setHistoricCount(int $count): static
$this->historicalPasswordCount = $count;
return $this;
}

/**
* Get the required strength of a password based on the consts in
* Symfony\Component\Validator\Constraints\PasswordStrength
* Default return -1 for validators that do not support this
*
*/
public function getRequiredStrength(): int
{
return -1;
}

/**
* Check if this validator can evaluate password strength.
*/
public function canEvaluateStrength(): bool
{
return false;
}

/**
* Evaluate the strength of a password based on the consts in
* Symfony\Component\Validator\Constraints\PasswordStrength
* Default return -1 for validators that do not support this
*/
public function evaluateStrength(string $password): int
{
return -1;
}

/**
* Textual representation of an evaluated password strength
*/
public static function getStrengthLevel(int $strength): string
{
return match($strength) {
PasswordStrength::STRENGTH_VERY_WEAK => _t(
PasswordValidator::class . '.VERYWEAK',
'very weak'
),
PasswordStrength::STRENGTH_WEAK => _t(
PasswordValidator::class . '.WEAK',
'weak'
),
PasswordStrength::STRENGTH_MEDIUM => _t(
PasswordValidator::class . '.MEDIUM',
'medium'
),
PasswordStrength::STRENGTH_STRONG => _t(
PasswordValidator::class . '.STRONG',
'strong'
),
PasswordStrength::STRENGTH_VERY_STRONG => _t(
PasswordValidator::class . '.VERY_STRONG',
'very strong'
),
default => '',
};
}
}

0 comments on commit 5404f9c

Please sign in to comment.