diff --git a/doc/events.rst b/doc/events.rst index 3b7d0202..b2d5048f 100644 --- a/doc/events.rst +++ b/doc/events.rst @@ -90,6 +90,15 @@ Event class: ``\Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorCodeReus Is dispatched when the code has already been used within the configured time frame. This requires a caching backend to be available +``scheb_two_factor.authentication.skipped`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Constant: ``Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvents::SKIPPED`` + +Event class: ``\Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvent`` + +Is dispatched when any ``\Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface`` prevent the two factor process to start by returning false. + Backup Code Events ------------------ diff --git a/src/bundle/Resources/config/two_factor.php b/src/bundle/Resources/config/two_factor.php index 5767b79c..193a07e9 100644 --- a/src/bundle/Resources/config/two_factor.php +++ b/src/bundle/Resources/config/two_factor.php @@ -39,6 +39,7 @@ ->lazy(true) ->args([ abstract_arg('Two-factor conditions'), + service('event_dispatcher'), ]) ->set('scheb_two_factor.authenticated_token_condition', AuthenticatedTokenCondition::class) diff --git a/src/bundle/Security/TwoFactor/Condition/TwoFactorConditionRegistry.php b/src/bundle/Security/TwoFactor/Condition/TwoFactorConditionRegistry.php index f0d858f9..a14f376b 100644 --- a/src/bundle/Security/TwoFactor/Condition/TwoFactorConditionRegistry.php +++ b/src/bundle/Security/TwoFactor/Condition/TwoFactorConditionRegistry.php @@ -5,6 +5,9 @@ namespace Scheb\TwoFactorBundle\Security\TwoFactor\Condition; use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface; +use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvent; +use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvents; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * @final @@ -14,14 +17,19 @@ class TwoFactorConditionRegistry /** * @param TwoFactorConditionInterface[] $conditions */ - public function __construct(private readonly iterable $conditions) - { + public function __construct( + private readonly iterable $conditions, + private readonly EventDispatcherInterface $eventDispatcher, + ) { } public function shouldPerformTwoFactorAuthentication(AuthenticationContextInterface $context): bool { foreach ($this->conditions as $condition) { if (!$condition->shouldPerformTwoFactorAuthentication($context)) { + $event = new TwoFactorAuthenticationEvent($context->getRequest(), $context->getToken()); + $this->eventDispatcher->dispatch($event, TwoFactorAuthenticationEvents::SKIPPED); + return false; } } diff --git a/src/bundle/Security/TwoFactor/Event/TwoFactorAuthenticationEvents.php b/src/bundle/Security/TwoFactor/Event/TwoFactorAuthenticationEvents.php index 3a830dbc..6e8bd950 100644 --- a/src/bundle/Security/TwoFactor/Event/TwoFactorAuthenticationEvents.php +++ b/src/bundle/Security/TwoFactor/Event/TwoFactorAuthenticationEvents.php @@ -51,4 +51,9 @@ class TwoFactorAuthenticationEvents * When the two-factor code has been used already. */ public const string CODE_REUSED = 'scheb_two_factor.authentication.code_reused'; + + /** + * When the two-factor process will not start because at least one 2fa condition is not met. + */ + public const string SKIPPED = 'scheb_two_factor.authentication.skipped'; } diff --git a/tests/Security/TwoFactor/Condition/TwoFactorConditionRegistryTest.php b/tests/Security/TwoFactor/Condition/TwoFactorConditionRegistryTest.php index 7f6c33a8..7ff9406d 100644 --- a/tests/Security/TwoFactor/Condition/TwoFactorConditionRegistryTest.php +++ b/tests/Security/TwoFactor/Condition/TwoFactorConditionRegistryTest.php @@ -10,10 +10,16 @@ use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface; use Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface; use Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionRegistry; +use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvent; +use Scheb\TwoFactorBundle\Security\TwoFactor\Event\TwoFactorAuthenticationEvents; +use Scheb\TwoFactorBundle\Tests\EventDispatcherTestHelper; use Scheb\TwoFactorBundle\Tests\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; class TwoFactorConditionRegistryTest extends TestCase { + use EventDispatcherTestHelper; + private MockObject&AuthenticationContextInterface $context; private MockObject&TwoFactorConditionInterface $condition1; private MockObject&TwoFactorConditionInterface $condition2; @@ -26,11 +32,15 @@ protected function setUp(): void $this->condition1 = $this->createMock(TwoFactorConditionInterface::class); $this->condition2 = $this->createMock(TwoFactorConditionInterface::class); $this->condition3 = $this->createMock(TwoFactorConditionInterface::class); - $this->registry = new TwoFactorConditionRegistry(new ArrayIterator([ - $this->condition1, - $this->condition2, - $this->condition3, - ])); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + $this->registry = new TwoFactorConditionRegistry( + new ArrayIterator([ + $this->condition1, + $this->condition2, + $this->condition3, + ]), + $this->eventDispatcher, + ); } private function conditionReturns(MockObject $condition, bool $result): void @@ -56,6 +66,7 @@ public function shouldPerformTwoFactorAuthentication_allConditionsFulfilled_chec $this->conditionReturns($this->condition2, true); $this->conditionReturns($this->condition3, true); + $this->expectNotDispatchEvent(); $returnValue = $this->registry->shouldPerformTwoFactorAuthentication($this->context); $this->assertTrue($returnValue); } @@ -67,6 +78,10 @@ public function shouldPerformTwoFactorAuthentication_conditionFails_skipFollowin $this->conditionReturns($this->condition2, false); $this->conditionNotCalled($this->condition3); + $this->expectDispatchOneEvent( + $this->isInstanceOf(TwoFactorAuthenticationEvent::class), + TwoFactorAuthenticationEvents::SKIPPED, + ); $returnValue = $this->registry->shouldPerformTwoFactorAuthentication($this->context); $this->assertFalse($returnValue); }