diff --git a/service-front/app/src/Actor/src/Form/CreateAccount.php b/service-front/app/src/Actor/src/Form/CreateAccount.php index 92fb3d7a17..7c2e466652 100644 --- a/service-front/app/src/Actor/src/Form/CreateAccount.php +++ b/service-front/app/src/Actor/src/Form/CreateAccount.php @@ -6,6 +6,7 @@ use Common\Form\AbstractForm; use Common\Form\Element\Email; +use Common\Validator\CommonPasswordValidator; use Common\Validator\EmailAddressValidator; use Laminas\Filter\StringToLower; use Laminas\Filter\StringTrim; @@ -93,6 +94,9 @@ public function getInputFilterSpecification(): array ], ], ], + [ + 'name' => CommonPasswordValidator::class, + ], [ 'name' => StringLength::class, 'options' => [ diff --git a/service-front/app/src/Actor/src/Form/PasswordChange.php b/service-front/app/src/Actor/src/Form/PasswordChange.php index 71dbd2280f..39560466cf 100644 --- a/service-front/app/src/Actor/src/Form/PasswordChange.php +++ b/service-front/app/src/Actor/src/Form/PasswordChange.php @@ -5,6 +5,7 @@ namespace Actor\Form; use Common\Form\AbstractForm; +use Common\Validator\CommonPasswordValidator; use Laminas\InputFilter\InputFilterProviderInterface; use Laminas\Validator\NotEmpty; use Laminas\Validator\StringLength; @@ -88,6 +89,9 @@ public function getInputFilterSpecification() ], ], ], + [ + 'name' => CommonPasswordValidator::class, + ], ], ], ]; diff --git a/service-front/app/src/Actor/templates/actor/form-error-messages.html.twig b/service-front/app/src/Actor/templates/actor/form-error-messages.html.twig index e7abb9728a..8ce89d3f8b 100644 --- a/service-front/app/src/Actor/templates/actor/form-error-messages.html.twig +++ b/service-front/app/src/Actor/templates/actor/form-error-messages.html.twig @@ -15,6 +15,8 @@ {% trans %}Passwords do not match{% context %}error{% notes %}Create account{% endtrans %} {% trans %}Confirm your password{% context %}error{% notes %}Create account, password reset{% endtrans %} {% trans %}You must accept the terms of use to create an account{% context %}error{% notes %}Create account{% endtrans %} +{% trans %}Password is too common{% context %}error{% notes %}Create account, change password{% endtrans %} + {# Triage page #} {% trans %}Select yes if you have a Use a lasting power of attorney account{% context %}error{% notes %}Triage page{% endtrans %} diff --git a/service-front/app/src/Common/src/Validator/CommonPasswordValidator.php b/service-front/app/src/Common/src/Validator/CommonPasswordValidator.php new file mode 100644 index 0000000000..c3fc748b91 --- /dev/null +++ b/service-front/app/src/Common/src/Validator/CommonPasswordValidator.php @@ -0,0 +1,89 @@ + 'Password is too common', + ]; + + + const TMP_ROOT_PATH = '/tmp/'; + const PWNED_PW_URL = 'https://www.ncsc.gov.uk/static-assets/documents/PwnedPasswordsTop100k.txt'; + + private string $filePathCommonPasswords; + + private string $pwnedPasswordsUrl; + + public function __construct() + { + parent::__construct(); + $this->filePathCommonPasswords = self::TMP_ROOT_PATH.'commonpasswords.txt'; + $this->pwnedPasswordsUrl = self::PWNED_PW_URL; + } + + /** + * @param mixed $value + * @return bool + */ + public function isValid($value): bool + { + $isValid = true; + $this->checkCommonPasswordsFileExists($this->filePathCommonPasswords); + + if ($this->passwordMatchesCommonPasswords($value, $this->filePathCommonPasswords)) { + $this->error(self::COMMON_PASSWORD); + $isValid = false; + } + + return $isValid; + } + + protected function passwordMatchesCommonPasswords(string $searchTerm, string $filePath): bool + { + $matches = []; + $handle = @fopen($filePath, 'r'); + if ($handle && strlen($searchTerm) > 0) { + while (!feof($handle)) { + $buffer = fgets($handle); + if (false !== strpos($buffer, $searchTerm)) { + $matches[] = $buffer; + } + } + fclose($handle); + } + //show results: + if (count($matches) > 0) { + return true; + } else { + return false; + } + } + + protected function checkCommonPasswordsFileExists(string $filePath): void + { + if (file_exists($filePath) & (time() - filemtime($filePath) < 24 * 3600)) { + return; + } else { + $fp = fopen($this->pwnedPasswordsUrl, 'r'); + if (false !== $fp) { + $written = file_put_contents( + "$filePath", + $fp + ); + if (false === $written) { + throw new RuntimeException(sprintf('Unable to download or write common password file to disk')); + } + } + } + } +} diff --git a/service-front/app/test/CommonTest/Validator/CommonPasswordValidatorTest.php b/service-front/app/test/CommonTest/Validator/CommonPasswordValidatorTest.php new file mode 100644 index 0000000000..1ee754d49a --- /dev/null +++ b/service-front/app/test/CommonTest/Validator/CommonPasswordValidatorTest.php @@ -0,0 +1,45 @@ +validator = new CommonPasswordValidator(); + } + + /** + * Verify a constraint message is triggered when value is invalid. + */ + public function testValidateOnInvalid() + { + $this->assertFalse($this->validator->isValid('Password123')); + print(count($this->validator->getMessages())); + $this->assertArrayHasKey(CommonPasswordValidator::COMMON_PASSWORD, $this->validator->getMessages()); + } + + + /** + * Verify no constraint message is triggered when value is valid. + */ + public function testValidateOnValid() + { + $this->assertTrue($this->validator->isValid('Aformidablepw876!')); + $this->assertCount(0, $this->validator->getMessages()); + + } +} \ No newline at end of file diff --git a/service-front/docker/app/Dockerfile b/service-front/docker/app/Dockerfile index 0aa8e1b627..759ecf07b2 100644 --- a/service-front/docker/app/Dockerfile +++ b/service-front/docker/app/Dockerfile @@ -37,6 +37,10 @@ FROM php-base AS app COPY service-front/app /app COPY --from=dependency /app/vendor /app/vendor +# Add common passwords file +RUN wget -q -O /tmp/commonpasswords.txt "https://www.ncsc.gov.uk/static-assets/documents/PwnedPasswordsTop100k.txt" \ + && chown www-data /tmp/commonpasswords.txt + # # Install development dependencies and tools into production image #