-
-
Notifications
You must be signed in to change notification settings - Fork 474
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Password: protect user password fields with recaptcha after 3 failed …
…attempts
- Loading branch information
Showing
16 changed files
with
479 additions
and
46 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
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,52 @@ | ||
<?php declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of Packagist. | ||
* | ||
* (c) Jordi Boggiano <[email protected]> | ||
* Nils Adermann <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace App\Form\EventSubscriber; | ||
|
||
use App\Validator\RateLimitingRecaptcha; | ||
use Beelab\Recaptcha2Bundle\Validator\Constraints\Recaptcha2; | ||
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
use Symfony\Component\Form\Form; | ||
use Symfony\Component\Form\FormEvent; | ||
use Symfony\Component\Form\FormEvents; | ||
use Symfony\Component\Validator\ConstraintViolation; | ||
|
||
/** | ||
* In case we encounter a brute force error e.g. missing/invalid recaptcha, remove all other form errors to not accidentally leak any information. | ||
*/ | ||
class FormBruteForceSubscriber implements EventSubscriberInterface | ||
{ | ||
public function onPostSubmit(FormEvent $event): void | ||
{ | ||
$form = $event->getForm(); | ||
|
||
if ($form->isRoot() && $form instanceof Form) { | ||
foreach ($form->getErrors(true) as $error) { | ||
$recaptchaMessage = (new Recaptcha2)->message; | ||
$cause = $error->getCause(); | ||
if ( | ||
$cause instanceof ConstraintViolation && ( | ||
$cause->getCode() === RateLimitingRecaptcha::INVALID_RECAPTCHA_ERROR || | ||
$error->getMessage() === $recaptchaMessage | ||
)) { | ||
$form->clearErrors(true); | ||
$error->getOrigin()?->addError($error); | ||
} | ||
} | ||
} | ||
} | ||
|
||
public static function getSubscribedEvents(): array | ||
{ | ||
return [FormEvents::POST_SUBMIT => ['onPostSubmit', -2048]]; | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
src/Form/EventSubscriber/FormInvalidPasswordSubscriber.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,47 @@ | ||
<?php declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of Packagist. | ||
* | ||
* (c) Jordi Boggiano <[email protected]> | ||
* Nils Adermann <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace App\Form\EventSubscriber; | ||
|
||
use App\Security\RecaptchaHelper; | ||
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
use Symfony\Component\Form\FormEvent; | ||
use Symfony\Component\Form\FormEvents; | ||
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword; | ||
use Symfony\Component\Validator\ConstraintViolation; | ||
|
||
class FormInvalidPasswordSubscriber implements EventSubscriberInterface | ||
{ | ||
public function __construct( | ||
private readonly RecaptchaHelper $recaptchaHelper, | ||
) {} | ||
|
||
public function onPostSubmit(FormEvent $event): void | ||
{ | ||
$form = $event->getForm(); | ||
|
||
if ($form->isRoot()) { | ||
foreach ($form->getErrors(true) as $error) { | ||
$cause = $error->getCause(); | ||
if ($cause instanceof ConstraintViolation && $cause->getCode() === UserPassword::INVALID_PASSWORD_ERROR) { | ||
$context = $this->recaptchaHelper->buildContext(); | ||
$this->recaptchaHelper->increaseCounter($context); | ||
} | ||
} | ||
} | ||
} | ||
|
||
public static function getSubscribedEvents(): array | ||
{ | ||
return [FormEvents::POST_SUBMIT => ['onPostSubmit', -1024]]; | ||
} | ||
} |
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 declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of Packagist. | ||
* | ||
* (c) Jordi Boggiano <[email protected]> | ||
* Nils Adermann <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace App\Form\Extension; | ||
|
||
use App\Form\EventSubscriber\FormBruteForceSubscriber; | ||
use App\Form\EventSubscriber\FormInvalidPasswordSubscriber; | ||
use Symfony\Component\Form\AbstractTypeExtension; | ||
use Symfony\Component\Form\Extension\Core\Type\FormType; | ||
use Symfony\Component\Form\FormBuilderInterface; | ||
|
||
class RecaptchaExtension extends AbstractTypeExtension | ||
{ | ||
public function __construct( | ||
private readonly FormInvalidPasswordSubscriber $formInvalidPasswordSubscriber, | ||
private readonly FormBruteForceSubscriber $formBruteForceSubscriber, | ||
) {} | ||
|
||
public function buildForm(FormBuilderInterface $builder, array $options): void | ||
{ | ||
$builder->addEventSubscriber($this->formInvalidPasswordSubscriber); | ||
$builder->addEventSubscriber($this->formBruteForceSubscriber); | ||
} | ||
|
||
public static function getExtendedTypes(): iterable | ||
{ | ||
return [FormType::class]; | ||
} | ||
} |
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,35 @@ | ||
<?php declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of Packagist. | ||
* | ||
* (c) Jordi Boggiano <[email protected]> | ||
* Nils Adermann <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace App\Form\Type; | ||
|
||
use App\Validator\RateLimitingRecaptcha; | ||
use Symfony\Component\Form\AbstractType; | ||
use Symfony\Component\Form\Extension\Core\Type\TextType; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
|
||
class InvisibleRecaptchaType extends AbstractType | ||
{ | ||
public function configureOptions(OptionsResolver $resolver): void | ||
{ | ||
$resolver->setDefaults([ | ||
'label' => false, | ||
'mapped' => false, | ||
'constraints' => new RateLimitingRecaptcha(), | ||
]); | ||
} | ||
|
||
public function getParent(): string | ||
{ | ||
return TextType::class; | ||
} | ||
} |
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
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
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,47 @@ | ||
<?php declare(strict_types=1); | ||
|
||
/* | ||
* This file is part of Packagist. | ||
* | ||
* (c) Jordi Boggiano <[email protected]> | ||
* Nils Adermann <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace App\Security; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
|
||
class RecaptchaContext | ||
{ | ||
private const LOGIN_BASE_KEY_IP = 'bf:login:ip:'; | ||
private const LOGIN_BASE_KEY_USER = 'bf:login:user:'; | ||
|
||
public function __construct( | ||
public readonly string $ip, | ||
public readonly string $username, | ||
public readonly ?string $recaptcha, | ||
) {} | ||
|
||
/** | ||
* @return string[] | ||
*/ | ||
public function getRedisKeys(bool $forClear = false): array | ||
{ | ||
return array_filter([ | ||
! $forClear && $this->ip ? self::LOGIN_BASE_KEY_IP . $this->ip : null, | ||
$this->username ? self::LOGIN_BASE_KEY_USER . strtolower($this->username) : null, | ||
]); | ||
} | ||
|
||
public static function fromRequest(Request $request): self | ||
{ | ||
return new self( | ||
$request->getClientIp() ?: '', | ||
(string) $request->request->get('_username'), | ||
(string) $request->request->get('g-recaptcha-response'), | ||
); | ||
} | ||
} |
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
Oops, something went wrong.