-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 4687a45
Showing
12 changed files
with
386 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
|
||
namespace Timpack\PwnedValidator\Api; | ||
|
||
interface ValidatorInterface | ||
{ | ||
/** | ||
* @param $password | ||
* @return bool | ||
*/ | ||
public function isValid($password): bool; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
<?php | ||
|
||
namespace Timpack\PwnedValidator\Model; | ||
|
||
use Magento\Framework\App\CacheInterface; | ||
use Magento\Framework\App\Config\ScopeConfigInterface; | ||
use Magento\Framework\HTTP\ClientInterface; | ||
use Magento\Framework\Serialize\SerializerInterface; | ||
use Timpack\PwnedValidator\Api\ValidatorInterface; | ||
|
||
class Validator implements ValidatorInterface | ||
{ | ||
const PWNED_BASE_URL = 'https://api.pwnedpasswords.com'; | ||
const CONFIG_PWNED_MINIMUM_MATCHES = 'customer/pwned/minimum_matches'; | ||
|
||
/** | ||
* @var ClientInterface | ||
*/ | ||
private $httpClient; | ||
|
||
/** | ||
* @var CacheInterface | ||
*/ | ||
private $cache; | ||
|
||
/** | ||
* @var SerializerInterface | ||
*/ | ||
private $serializer; | ||
|
||
/** | ||
* @var ScopeConfigInterface | ||
*/ | ||
private $scopeConfig; | ||
|
||
/** | ||
* Validator constructor. | ||
* @param ClientInterface $httpClient | ||
* @param CacheInterface $cache | ||
* @param SerializerInterface $serializer | ||
* @param ScopeConfigInterface $scopeConfig | ||
*/ | ||
public function __construct( | ||
ClientInterface $httpClient, | ||
CacheInterface $cache, | ||
SerializerInterface $serializer, | ||
ScopeConfigInterface $scopeConfig | ||
) { | ||
$this->httpClient = $httpClient; | ||
$this->cache = $cache; | ||
$this->serializer = $serializer; | ||
$this->scopeConfig = $scopeConfig; | ||
} | ||
|
||
/** | ||
* @param $password | ||
* @return bool | ||
*/ | ||
public function isValid($password): bool | ||
{ | ||
$passwordHash = strtoupper(sha1($password)); | ||
$prefix = substr($passwordHash, 0, 5); | ||
$suffix = substr($passwordHash, 5); | ||
|
||
$minimumMatches = $this->getMinimumMatches(); | ||
$hashes = $this->query($prefix); | ||
$count = $hashes[$suffix] ?? 0; | ||
|
||
return $count < $minimumMatches; | ||
} | ||
|
||
/** | ||
* @param $prefix | ||
* @return array | ||
*/ | ||
private function query($prefix): array | ||
{ | ||
$cacheKey = 'PWNED_HASH_RANGE_' . $prefix; | ||
|
||
$cacheEntry = $this->cache->load($cacheKey); | ||
if ($cacheEntry) { | ||
return $this->serializer->unserialize($cacheEntry); | ||
} | ||
|
||
$hashes = []; | ||
|
||
$this->httpClient->get(self::PWNED_BASE_URL . '/range/' . $prefix); | ||
|
||
if ($this->httpClient->getStatus() !== 200) { | ||
return $hashes; | ||
} | ||
|
||
$body = $this->httpClient->getBody(); | ||
$results = explode("\n", $body); | ||
|
||
foreach ($results as $value) { | ||
list($hash, $count) = explode(':', $value); | ||
$hashes[$hash] = (int)$count; | ||
} | ||
|
||
$serialized = $this->serializer->serialize($hashes); | ||
$this->cache->save($serialized, $cacheKey, [], 3600 * 8); | ||
|
||
return $hashes; | ||
} | ||
|
||
/** | ||
* @return int | ||
*/ | ||
private function getMinimumMatches(): int | ||
{ | ||
return (int)$this->scopeConfig->getValue(self::CONFIG_PWNED_MINIMUM_MATCHES, 'stores'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?php | ||
|
||
namespace Timpack\PwnedValidator\Observer; | ||
|
||
use Magento\Framework\Event\Observer; | ||
use Magento\Framework\Event\ObserverInterface; | ||
use Magento\Framework\Exception\InputException; | ||
use Timpack\PwnedValidator\Api\ValidatorInterface; | ||
|
||
class Validate implements ObserverInterface | ||
{ | ||
/** | ||
* @var ValidatorInterface | ||
*/ | ||
private $validator; | ||
|
||
/** | ||
* Validate constructor. | ||
* @param ValidatorInterface $validator | ||
*/ | ||
public function __construct(ValidatorInterface $validator) | ||
{ | ||
$this->validator = $validator; | ||
} | ||
|
||
/** | ||
* @param Observer $observer | ||
* @return void | ||
* @throws InputException | ||
*/ | ||
public function execute(Observer $observer) | ||
{ | ||
$password = $observer->getData('password'); | ||
if (!$this->validator->isValid($password)) { | ||
throw new InputException(__('The password was found in public databases.')); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Magento 2 Have I Been Pwned Validator | ||
This module adds a validator which checks if the submitted password is found in public databases using the `Have I Been Pwned?` service. | ||
|
||
## Security | ||
There are no security drawbacks, because there are no actual passwords being submitted over the internet. This is possible by hashing the password using the `SHA-1` algorithm and request all hashes in the `Have I been Pwned?` databases starting with the first 5 characters of the password hash. This resultset contains a list of hashes and the amount of occurrences. | ||
|
||
This way the password stays inside the Magento process. | ||
|
||
## Installation | ||
``` | ||
composer require timpack/magento2-module-pwned-validator | ||
bin/magento setup:upgrade | ||
``` | ||
|
||
## Configuration | ||
You can configure the threshold of the validator, at which count of occurrences in the resultset the password should be considered insecure/invalid. | ||
This configuration can be found at: | ||
|
||
`Stores -> Configuration -> Customer -> Customer Configuration -> Pwned Validator -> Minimum amount of matches` | ||
|
||
## Credits | ||
This module was heavily inspired by Valorin's Pwned validator written for Laravel: [valorin/pwned-validator](https://github.com/valorin/pwned-validator) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
<?php | ||
|
||
namespace Timpack\PwnedValidator\Rewrite\Model; | ||
|
||
use Magento\Customer\Api\AddressRepositoryInterface; | ||
use Magento\Customer\Api\CustomerMetadataInterface; | ||
use Magento\Customer\Api\CustomerRepositoryInterface; | ||
use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; | ||
use Magento\Customer\Helper\View as CustomerViewHelper; | ||
use Magento\Customer\Model\Config\Share as ConfigShare; | ||
use Magento\Customer\Model\Customer as CustomerModel; | ||
use Magento\Customer\Model\Customer\CredentialsValidator; | ||
use Magento\Customer\Model\CustomerFactory; | ||
use Magento\Customer\Model\CustomerRegistry; | ||
use Magento\Customer\Model\Metadata\Validator; | ||
use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory; | ||
use Magento\Framework\Api\ExtensibleDataObjectConverter; | ||
use Magento\Framework\App\Config\ScopeConfigInterface; | ||
use Magento\Framework\DataObjectFactory as ObjectFactory; | ||
use Magento\Framework\Encryption\EncryptorInterface as Encryptor; | ||
use Magento\Framework\Event\ManagerInterface; | ||
use Magento\Framework\Intl\DateTimeFactory; | ||
use Magento\Framework\Mail\Template\TransportBuilder; | ||
use Magento\Framework\Math\Random; | ||
use Magento\Framework\Reflection\DataObjectProcessor; | ||
use Magento\Framework\Registry; | ||
use Magento\Framework\Session\SaveHandlerInterface; | ||
use Magento\Framework\Session\SessionManagerInterface; | ||
use Magento\Framework\Stdlib\DateTime; | ||
use Magento\Framework\Stdlib\StringUtils as StringHelper; | ||
use Magento\Store\Model\StoreManagerInterface; | ||
use Psr\Log\LoggerInterface as PsrLogger; | ||
|
||
class AccountManagement extends \Magento\Customer\Model\AccountManagement | ||
{ | ||
/** | ||
* @var ManagerInterface | ||
*/ | ||
private $eventManager; | ||
|
||
public function __construct( | ||
CustomerFactory $customerFactory, | ||
ManagerInterface $eventManager, | ||
StoreManagerInterface $storeManager, | ||
Random $mathRandom, | ||
Validator $validator, | ||
ValidationResultsInterfaceFactory $validationResultsDataFactory, | ||
AddressRepositoryInterface $addressRepository, | ||
CustomerMetadataInterface $customerMetadataService, | ||
CustomerRegistry $customerRegistry, | ||
PsrLogger $logger, | ||
Encryptor $encryptor, | ||
ConfigShare $configShare, | ||
StringHelper $stringHelper, | ||
CustomerRepositoryInterface $customerRepository, | ||
ScopeConfigInterface $scopeConfig, | ||
TransportBuilder $transportBuilder, | ||
DataObjectProcessor $dataProcessor, | ||
Registry $registry, | ||
CustomerViewHelper $customerViewHelper, | ||
DateTime $dateTime, | ||
CustomerModel $customerModel, | ||
ObjectFactory $objectFactory, | ||
ExtensibleDataObjectConverter $extensibleDataObjectConverter, | ||
CredentialsValidator $credentialsValidator = null, | ||
DateTimeFactory $dateTimeFactory = null, | ||
SessionManagerInterface $sessionManager = null, | ||
SaveHandlerInterface $saveHandler = null, | ||
CollectionFactory $visitorCollectionFactory = null | ||
) | ||
{ | ||
parent::__construct( | ||
$customerFactory, | ||
$eventManager, | ||
$storeManager, | ||
$mathRandom, | ||
$validator, | ||
$validationResultsDataFactory, | ||
$addressRepository, | ||
$customerMetadataService, | ||
$customerRegistry, | ||
$logger, | ||
$encryptor, | ||
$configShare, | ||
$stringHelper, | ||
$customerRepository, | ||
$scopeConfig, | ||
$transportBuilder, | ||
$dataProcessor, | ||
$registry, | ||
$customerViewHelper, | ||
$dateTime, | ||
$customerModel, | ||
$objectFactory, | ||
$extensibleDataObjectConverter, | ||
$credentialsValidator, | ||
$dateTimeFactory, | ||
$sessionManager, | ||
$saveHandler, | ||
$visitorCollectionFactory | ||
); | ||
$this->eventManager = $eventManager; | ||
} | ||
|
||
/** | ||
* @param string $password | ||
* @throws \Magento\Framework\Exception\InputException | ||
* @return void | ||
*/ | ||
protected function checkPasswordStrength($password) | ||
{ | ||
parent::checkPasswordStrength($password); | ||
$this->eventManager->dispatch('timpack_pwnedvalidator_check_password_strength', ['password' => $password]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"name": "timpack/magento2-module-pwned-validator", | ||
"description": "Add 'Have I been pwned?' validator to Magento 2.", | ||
"license": "MIT", | ||
"authors": [ | ||
{ | ||
"name": "Timon de Groot", | ||
"email": "[email protected]" | ||
} | ||
], | ||
"require": { | ||
"magento/framework": "^101.0", | ||
"magento/module-customer": "^101.0" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"Timpack\\PwnedValidator\\": "" | ||
}, | ||
"files": [ | ||
"registration.php" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?xml version="1.0" ?> | ||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> | ||
<system> | ||
<section id="customer"> | ||
<group id="pwned" showInDefault="1" showInWebsite="1" showInStore="1" translate="label" sortOrder="30"> | ||
<label>Pwned Validator</label> | ||
<field id="minimum_matches" showInDefault="1" showInWebsite="1" showInStore="1" translate="label" | ||
sortOrder="10" canRestore="1"> | ||
<label>Minimum amount of matches</label> | ||
<comment>Enter the minimum amount of matches needed to consider password unsafe/invalid.</comment> | ||
<validate>number</validate> | ||
</field> | ||
</group> | ||
</section> | ||
</system> | ||
</config> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<?xml version="1.0" ?> | ||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> | ||
<default> | ||
<customer> | ||
<pwned> | ||
<minimum_matches>1</minimum_matches> | ||
</pwned> | ||
</customer> | ||
</default> | ||
</config> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> | ||
<preference for="Timpack\PwnedValidator\Api\ValidatorInterface" type="Timpack\PwnedValidator\Model\Validator"/> | ||
<preference for="Magento\Customer\Model\AccountManagement" | ||
type="Timpack\PwnedValidator\Rewrite\Model\AccountManagement"/> | ||
<type name="Timpack\PwnedValidator\Api\ValidatorInterface"> | ||
<arguments> | ||
<argument name="httpClient" xsi:type="object">Magento\Framework\HTTP\Client\Curl</argument> | ||
</arguments> | ||
</type> | ||
</config> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?xml version="1.0" encoding="UTF-8" ?> | ||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> | ||
<event name="timpack_pwnedvalidator_check_password_strength"> | ||
<observer name="timpack_pwnedvalidator_validate_pwned" instance="Timpack\PwnedValidator\Observer\Validate"/> | ||
</event> | ||
</config> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<?xml version="1.0"?> | ||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> | ||
<module name="Timpack_PwnedValidator" setup_version="1.0.0"> | ||
<sequence> | ||
<module name="Magento_Customer"/> | ||
</sequence> | ||
</module> | ||
</config> |
Oops, something went wrong.