Skip to content

Commit 81f6039

Browse files
committed
Make needsPreparation a soft requirement to prevent b/c break
Prior to this change, the needsPreparation method would break existing implementations. This change makes the change not break existing TwoFactorProviders by not actually defining the `needsPreparation` in the interface. This is to be implemented in version 9 of the 2fa package.
1 parent fb09d0a commit 81f6039

File tree

4 files changed

+92
-28
lines changed

4 files changed

+92
-28
lines changed

src/bundle/Security/TwoFactor/Provider/TwoFactorProviderInitiator.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Exception\UnknownTwoFactorProviderException;
1111
use function array_walk;
1212
use function count;
13+
use function method_exists;
14+
use function trigger_error;
15+
use const E_USER_DEPRECATED;
1316

1417
/**
1518
* @final
@@ -34,7 +37,15 @@ public function beginTwoFactorAuthentication(AuthenticationContextInterface $con
3437
}
3538

3639
$activeTwoFactorProviders[] = $providerName;
37-
if ($provider->needsPreparation()) {
40+
41+
if (!method_exists($provider, 'needsPreparation')) {
42+
@trigger_error(
43+
'Two-factor provider "'.$providerName.'" does not implement needsPreparation() method. This method will be required in the next major version.',
44+
E_USER_DEPRECATED,
45+
);
46+
}
47+
48+
if (!method_exists($provider, 'needsPreparation') || $provider->needsPreparation()) {
3849
continue;
3950
}
4051

src/bundle/Security/TwoFactor/Provider/TwoFactorProviderInterface.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ public function beginAuthentication(AuthenticationContextInterface $context): bo
1515

1616
/**
1717
* Determine whether this Provider needs to be prepared (if the prepareAuthentication method needs to be called).
18+
*
19+
* In version 9, this method will be introduced, and all providers will need to implement it.
20+
* Currently, it will be called, but is not required.
21+
*
22+
* public function needsPreparation(): bool;
1823
*/
19-
public function needsPreparation(): bool;
2024

2125
/**
2226
* Do all steps necessary to prepare authentication, e.g. generate & send a code.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Scheb\TwoFactorBundle\Tests\Security\TwoFactor\Provider;
6+
7+
use RuntimeException;
8+
use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;
9+
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorFormRendererInterface;
10+
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderInterface;
11+
12+
class MockTwoFactorProvider implements TwoFactorProviderInterface
13+
{
14+
private bool $beginAuthenticationResult = false;
15+
private int $beginAuthenticationCallCount = 0;
16+
private AuthenticationContextInterface|null $lastContext = null;
17+
18+
public function __construct(private readonly bool $needsPreparation = false)
19+
{
20+
}
21+
22+
public function setBeginAuthenticationResult(bool $result): void
23+
{
24+
$this->beginAuthenticationResult = $result;
25+
}
26+
27+
public function getBeginAuthenticationCallCount(): int
28+
{
29+
return $this->beginAuthenticationCallCount;
30+
}
31+
32+
public function getLastContext(): AuthenticationContextInterface|null
33+
{
34+
return $this->lastContext;
35+
}
36+
37+
public function beginAuthentication(AuthenticationContextInterface $context): bool
38+
{
39+
++$this->beginAuthenticationCallCount;
40+
$this->lastContext = $context;
41+
42+
return $this->beginAuthenticationResult;
43+
}
44+
45+
public function needsPreparation(): bool
46+
{
47+
return $this->needsPreparation;
48+
}
49+
50+
public function prepareAuthentication(object $user): void
51+
{
52+
// Mock implementation
53+
}
54+
55+
public function validateAuthenticationCode(object $user, string $authenticationCode): bool
56+
{
57+
return false;
58+
}
59+
60+
public function getFormRenderer(): TwoFactorFormRendererInterface
61+
{
62+
throw new RuntimeException('Not implemented in mock');
63+
}
64+
}

tests/Security/TwoFactor/Provider/TwoFactorProviderInitiatorTest.php

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,21 @@
1111
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
1212
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderDeciderInterface;
1313
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderInitiator;
14-
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderInterface;
1514
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderRegistry;
1615
use Scheb\TwoFactorBundle\Tests\Security\TwoFactor\Condition\AbstractAuthenticationContextTestCase;
1716

1817
class TwoFactorProviderInitiatorTest extends AbstractAuthenticationContextTestCase
1918
{
2019
private MockObject&TwoFactorTokenFactoryInterface $twoFactorTokenFactory;
21-
private MockObject&TwoFactorProviderInterface $provider1;
22-
private MockObject&TwoFactorProviderInterface $provider2;
20+
private MockTwoFactorProvider $provider1;
21+
private MockTwoFactorProvider $provider2;
2322
private MockObject&TwoFactorProviderDeciderInterface $providerDecider;
2423
private TwoFactorProviderInitiator $initiator;
2524

2625
protected function setUp(): void
2726
{
28-
$this->provider1 = $this->createMock(TwoFactorProviderInterface::class);
29-
$this->provider1->method('needsPreparation')->willReturn(true);
30-
$this->provider2 = $this->createMock(TwoFactorProviderInterface::class);
31-
$this->provider2->method('needsPreparation')->willReturn(false);
27+
$this->provider1 = new MockTwoFactorProvider(needsPreparation: true);
28+
$this->provider2 = new MockTwoFactorProvider(needsPreparation: false);
3229

3330
$providerRegistry = $this->createMock(TwoFactorProviderRegistry::class);
3431
$providerRegistry
@@ -74,15 +71,8 @@ private function createUserWithPreferredProvider(string $preferredProvider): Moc
7471

7572
private function stubProvidersReturn(bool $provider1Returns, bool $provider2Returns): void
7673
{
77-
$this->provider1
78-
->expects($this->any())
79-
->method('beginAuthentication')
80-
->willReturn($provider1Returns);
81-
82-
$this->provider2
83-
->expects($this->any())
84-
->method('beginAuthentication')
85-
->willReturn($provider2Returns);
74+
$this->provider1->setBeginAuthenticationResult($provider1Returns);
75+
$this->provider2->setBeginAuthenticationResult($provider2Returns);
8676
}
8777

8878
private function stubTwoFactorTokenFactoryReturns(MockObject $token): void
@@ -106,17 +96,12 @@ public function beginAuthentication_multipleProviders_beginAuthenticationOnEachT
10696
{
10797
$context = $this->createAuthenticationContext();
10898

109-
$this->provider1
110-
->expects($this->once())
111-
->method('beginAuthentication')
112-
->with($context);
113-
114-
$this->provider2
115-
->expects($this->once())
116-
->method('beginAuthentication')
117-
->with($context);
118-
11999
$this->initiator->beginTwoFactorAuthentication($context);
100+
101+
$this->assertSame(1, $this->provider1->getBeginAuthenticationCallCount());
102+
$this->assertSame($context, $this->provider1->getLastContext());
103+
$this->assertSame(1, $this->provider2->getBeginAuthenticationCallCount());
104+
$this->assertSame($context, $this->provider2->getLastContext());
120105
}
121106

122107
#[Test]

0 commit comments

Comments
 (0)