Skip to content

Commit

Permalink
Add TwoFactorProviderDecider (#215)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielburger1337 authored Jan 18, 2024
1 parent 7990826 commit 1b74ad2
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 21 deletions.
4 changes: 4 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ Bundle Configuration
# Must implement Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface
two_factor_condition: acme.custom_two_factor_condition
# If you need custom conditions to decide what two factor provider to prefer.
# Must implement Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderDeciderInterface
two_factor_provider_decider: acme.custom_two_factor_provider_decider
Firewall Configuration
----------------------

Expand Down
1 change: 1 addition & 0 deletions src/bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->scalarNode('ip_whitelist_provider')->defaultValue('scheb_two_factor.default_ip_whitelist_provider')->end()
->scalarNode('two_factor_token_factory')->defaultValue('scheb_two_factor.default_token_factory')->end()
->scalarNode('two_factor_provider_decider')->defaultValue('scheb_two_factor.default_provider_decider')->end()
->scalarNode('two_factor_condition')->defaultNull()->end()
->end();

Expand Down
9 changes: 9 additions & 0 deletions src/bundle/DependencyInjection/SchebTwoFactorExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->configureTwoFactorConditions($container, $config);
$this->configureIpWhitelistProvider($container, $config);
$this->configureTokenFactory($container, $config);
$this->configureProviderDecider($container, $config);

if (isset($config['trusted_device']['enabled']) && $this->resolveFeatureFlag($container, $config['trusted_device']['enabled'])) {
$this->configureTrustedDeviceManager($container, $config);
Expand Down Expand Up @@ -158,6 +159,14 @@ private function configureTokenFactory(ContainerBuilder $container, array $confi
$container->setAlias('scheb_two_factor.token_factory', $config['two_factor_token_factory']);
}

/**
* @param array<string,mixed> $config
*/
private function configureProviderDecider(ContainerBuilder $container, array $config): void
{
$container->setAlias('scheb_two_factor.provider_decider', $config['two_factor_provider_decider']);
}

/**
* @param array<string,mixed> $config
*/
Expand Down
4 changes: 4 additions & 0 deletions src/bundle/Resources/config/two_factor.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\DefaultTwoFactorFormRenderer;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TokenPreparationRecorder;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorFormRendererInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderDecider;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderInitiator;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderRegistry;
use Scheb\TwoFactorBundle\Security\TwoFactor\TwoFactorFirewallContext;
Expand All @@ -29,6 +30,8 @@

->set('scheb_two_factor.default_token_factory', TwoFactorTokenFactory::class)

->set('scheb_two_factor.default_provider_decider', TwoFactorProviderDecider::class)

->set('scheb_two_factor.authentication_context_factory', AuthenticationContextFactory::class)
->args([AuthenticationContext::class])

Expand Down Expand Up @@ -56,6 +59,7 @@
->args([
service('scheb_two_factor.provider_registry'),
service('scheb_two_factor.token_factory'),
service('scheb_two_factor.provider_decider'),
])

->set('scheb_two_factor.firewall_context', TwoFactorFirewallContext::class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Scheb\TwoFactorBundle\Security\TwoFactor\Provider;

use Scheb\TwoFactorBundle\Model\PreferredProviderInterface;
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;

class TwoFactorProviderDecider implements TwoFactorProviderDeciderInterface
{
/**
* @param string[] $activeProviders
*/
public function getPreferredTwoFactorProvider(array $activeProviders, TwoFactorTokenInterface $token, AuthenticationContextInterface $context): string|null
{
$user = $context->getUser();

if ($user instanceof PreferredProviderInterface) {
return $user->getPreferredTwoFactorProvider();
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Scheb\TwoFactorBundle\Security\TwoFactor\Provider;

use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;

interface TwoFactorProviderDeciderInterface
{
/**
* Return the alias of the preferred two-factor provider.
*
* @param string[] $activeProviders
*/
public function getPreferredTwoFactorProvider(array $activeProviders, TwoFactorTokenInterface $token, AuthenticationContextInterface $context): string|null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Scheb\TwoFactorBundle\Security\TwoFactor\Provider;

use Scheb\TwoFactorBundle\Model\PreferredProviderInterface;
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenFactoryInterface;
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;
Expand All @@ -18,6 +17,7 @@ class TwoFactorProviderInitiator
public function __construct(
private readonly TwoFactorProviderRegistry $providerRegistry,
private readonly TwoFactorTokenFactoryInterface $twoFactorTokenFactory,
private readonly TwoFactorProviderDeciderInterface $twoFactorProviderDecider,
) {
}

Expand Down Expand Up @@ -47,29 +47,20 @@ public function beginTwoFactorAuthentication(AuthenticationContextInterface $con
$authenticatedToken = $context->getToken();
if ($activeTwoFactorProviders) {
$twoFactorToken = $this->twoFactorTokenFactory->create($authenticatedToken, $context->getFirewallName(), $activeTwoFactorProviders);
$this->setPreferredProvider($twoFactorToken, $context->getUser()); // Prioritize the user's preferred provider

return $twoFactorToken;
}

return null;
}
$preferredProvider = $this->twoFactorProviderDecider->getPreferredTwoFactorProvider($activeTwoFactorProviders, $twoFactorToken, $context);

private function setPreferredProvider(TwoFactorTokenInterface $token, object $user): void
{
if (!($user instanceof PreferredProviderInterface)) {
return;
}
if (null !== $preferredProvider) {
try {
$twoFactorToken->preferTwoFactorProvider($preferredProvider);
} catch (UnknownTwoFactorProviderException) {
// Bad user input
}
}

$preferredProvider = $user->getPreferredTwoFactorProvider();
if (!$preferredProvider) {
return;
return $twoFactorToken;
}

try {
$token->preferTwoFactorProvider($preferredProvider);
} catch (UnknownTwoFactorProviderException) {
// Bad user input
}
return null;
}
}
12 changes: 12 additions & 0 deletions tests/DependencyInjection/SchebTwoFactorExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,17 @@ public function load_alternativeTokenFactory_replaceAlias(): void
$this->assertHasAlias('scheb_two_factor.token_factory', 'acme_test.two_factor_token_factory');
}

/**
* @test
*/
public function load_alternativeProviderDecider_replaceAlias(): void
{
$config = $this->getFullConfig();
$this->extension->load([$config], $this->container);

$this->assertHasAlias('scheb_two_factor.provider_decider', 'acme_test.two_factor_provider_decider');
}

/**
* @return array<string,null>|null
*/
Expand All @@ -638,6 +649,7 @@ private function getFullConfig(): array
- 127.0.0.1
ip_whitelist_provider: acme_test.ip_whitelist_provider
two_factor_token_factory: acme_test.two_factor_token_factory
two_factor_provider_decider: acme_test.two_factor_provider_decider
two_factor_condition: acme_test.two_factor_condition
trusted_device:
enabled: true
Expand Down
84 changes: 84 additions & 0 deletions tests/Security/TwoFactor/Provider/TwoFactorProviderDeciderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace Scheb\TwoFactorBundle\Tests\Security\TwoFactor\Provider;

use PHPUnit\Framework\MockObject\MockObject;
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderDecider;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderDeciderInterface;
use Scheb\TwoFactorBundle\Tests\Security\TwoFactor\Condition\AbstractAuthenticationContextTestCase;
use Symfony\Component\Security\Core\User\UserInterface;

class TwoFactorProviderDeciderTest extends AbstractAuthenticationContextTestCase
{
private MockObject|TwoFactorProviderDeciderInterface $twoFactorProviderDecider;
private MockObject|TwoFactorTokenInterface $twoFactorToken;

protected function setUp(): void
{
$this->twoFactorToken = $this->createMock(TwoFactorTokenInterface::class);
$this->twoFactorProviderDecider = new TwoFactorProviderDecider();
}

/**
* @test
*/
public function getPreferredTwoFactorProvider_implementsPreferredProvider_returnsPreferredProvider(): void
{
$user = $this->createUserWithPreferredProvider('preferredProvider');

$this->assertEquals(
'preferredProvider',
$this->twoFactorProviderDecider->getPreferredTwoFactorProvider([], $this->twoFactorToken, $this->createAuthContext($user)),
);
}

/**
* @test
*/
public function getPreferredTwoFactorProvider_implementsPreferredProvider_returnsNullPreferredProvider(): void
{
$user = $this->createUserWithPreferredProvider(null);

$this->assertNull(
$this->twoFactorProviderDecider->getPreferredTwoFactorProvider([], $this->twoFactorToken, $this->createAuthContext($user)),
);
}

/**
* @test
*/
public function getPreferredTwoFactorProvider_unexpectedUserObject_returnsNull(): void
{
$user = $this->createMock(UserInterface::class);

$this->assertNull(
$this->twoFactorProviderDecider->getPreferredTwoFactorProvider([], $this->twoFactorToken, $this->createAuthContext($user)),
);
}

private function createUserWithPreferredProvider(string|null $preferredProvider): MockObject|UserWithPreferredProviderInterface
{
$user = $this->createMock(UserWithPreferredProviderInterface::class);
$user
->expects($this->any())
->method('getPreferredTwoFactorProvider')
->willReturn($preferredProvider);

return $user;
}

private function createAuthContext(object $user): MockObject|AuthenticationContextInterface
{
$authContext = $this->createMock(AuthenticationContextInterface::class);
$authContext
->expects($this->any())
->method('getUser')
->willReturn($user);

return $authContext;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenFactory;
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenFactoryInterface;
use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderDeciderInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderInitiator;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderRegistry;
Expand All @@ -18,6 +19,7 @@ class TwoFactorProviderInitiatorTest extends AbstractAuthenticationContextTestCa
private MockObject|TwoFactorTokenFactoryInterface $twoFactorTokenFactory;
private MockObject|TwoFactorProviderInterface $provider1;
private MockObject|TwoFactorProviderInterface $provider2;
private MockObject|TwoFactorProviderDeciderInterface $providerDecider;
private TwoFactorProviderInitiator $initiator;

protected function setUp(): void
Expand All @@ -36,7 +38,9 @@ protected function setUp(): void

$this->twoFactorTokenFactory = $this->createMock(TwoFactorTokenFactory::class);

$this->initiator = new TwoFactorProviderInitiator($providerRegistry, $this->twoFactorTokenFactory);
$this->providerDecider = $this->createMock(TwoFactorProviderDeciderInterface::class);

$this->initiator = new TwoFactorProviderInitiator($providerRegistry, $this->twoFactorTokenFactory, $this->providerDecider);
}

private function createTwoFactorToken(): MockObject|TwoFactorTokenInterface
Expand Down Expand Up @@ -76,6 +80,14 @@ private function stubTwoFactorTokenFactoryReturns(MockObject $token): void
->willReturn($token);
}

private function stubTwoFactorProviderDeciderReturns(string|null $preferredProvider): void
{
$this->providerDecider
->expects($this->once())
->method('getPreferredTwoFactorProvider')
->willReturn($preferredProvider);
}

/**
* @test
*/
Expand Down Expand Up @@ -141,6 +153,7 @@ public function beginAuthentication_hasPreferredProvider_setThatProviderPreferre
$context = $this->createAuthenticationContext(null, $originalToken, $user);
$this->stubProvidersReturn(true, true);
$this->stubTwoFactorTokenFactoryReturns($twoFactorToken);
$this->stubTwoFactorProviderDeciderReturns('preferredProvider');

$twoFactorToken
->expects($this->once())
Expand Down

0 comments on commit 1b74ad2

Please sign in to comment.