Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
tdgroot committed Mar 11, 2018
0 parents commit 4687a45
Show file tree
Hide file tree
Showing 12 changed files with 386 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Api/ValidatorInterface.php
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;
}
114 changes: 114 additions & 0 deletions Model/Validator.php
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');
}
}
38 changes: 38 additions & 0 deletions Observer/Validate.php
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.'));
}
}
}
22 changes: 22 additions & 0 deletions README.md
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)
115 changes: 115 additions & 0 deletions Rewrite/Model/AccountManagement.php
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]);
}
}
23 changes: 23 additions & 0 deletions composer.json
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"
]
}
}
17 changes: 17 additions & 0 deletions etc/adminhtml/system.xml
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>
11 changes: 11 additions & 0 deletions etc/config.xml
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>
12 changes: 12 additions & 0 deletions etc/di.xml
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>
7 changes: 7 additions & 0 deletions etc/events.xml
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>
9 changes: 9 additions & 0 deletions etc/module.xml
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>
Loading

0 comments on commit 4687a45

Please sign in to comment.