Skip to content

Commit

Permalink
Configurable custom conditions for when to perform two-factor authent…
Browse files Browse the repository at this point in the history
…ication #49
  • Loading branch information
scheb committed Mar 6, 2021
1 parent a548890 commit bd37ea3
Show file tree
Hide file tree
Showing 13 changed files with 148 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Core features are provided by `scheb/2fa-bundle`:
- Multi-factor authentication (more than 2 steps)
- CSRF protection
- Whitelisted routes (accessible during two-factor authentication)
- Fully customizable conditions when to perform two-factor authentication
- Future proof: Supports the [authenticator-based security system](https://symfony.com/doc/current/security/experimental_authenticators.html),
which will replace the current system in Symfony 6

Expand Down
4 changes: 4 additions & 0 deletions doc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ scheb_two_factor:
# service providing your own implementation.
# Must implement Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextFactoryInterface
two_factor_token_factory: acme.custom_two_factor_token_factory

# If you need custom conditions when to perform two-factor authentication.
# Must implement Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface
two_factor_condition: acme.custom_two_factor_condition
```
```yaml
Expand Down
29 changes: 29 additions & 0 deletions doc/custom_conditions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Custom Conditions for Two-Factor Authentication
===============================================

In your application, you may have extra requirements when to perform two-factor authentication, which goes beyond what
the bundle is doing automatically. In such a case you need to implement
`\Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface`:

```php
<?php

use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface;

class MyTwoFactorCondition implements TwoFactorConditionInterface
{
public function shouldPerformTwoFactorAuthentication(AuthenticationContextInterface $context): bool
{
// Your conditions here
}
}
```

Register it as a service and configure the service name:

```yaml
# config/packages/scheb_two_factor.yaml
scheb_two_factor:
two_factor_condition: acme.custom_two_factor_condition
```
1 change: 1 addition & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This bundle provides **two-factor authentication for your Symfony application**.

- [How to create a custom two-factor authenticator](providers/custom.md)
- [How to handle multiple activated authentication methods](multi_authentication.md)
- [How to customize conditions when to require two-factor authentication](conditions.md)
- [How to configure two-factor authentication for an API](api.md)
- [How to create a custom persister](persister.md)

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 @@ -48,6 +48,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_condition')->defaultNull()->end()
->end()
;

Expand Down
8 changes: 8 additions & 0 deletions src/bundle/DependencyInjection/SchebTwoFactorExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function load(array $configs, ContainerBuilder $container): void

// Configure custom services
$this->configurePersister($container, $config);
$this->configureTwoFactorCondition($container, $config);
$this->configureIpWhitelistProvider($container, $config);
$this->configureTokenFactory($container, $config);

Expand All @@ -69,6 +70,13 @@ private function configurePersister(ContainerBuilder $container, array $config):
$container->setAlias('scheb_two_factor.persister', $config['persister']);
}

private function configureTwoFactorCondition(ContainerBuilder $container, array $config): void
{
if (null !== $config['two_factor_condition']) {
$container->setAlias('scheb_two_factor.handler_condition', $config['two_factor_condition']);
}
}

private function configureTrustedDeviceManager(ContainerBuilder $container, array $config): void
{
$loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
Expand Down
2 changes: 1 addition & 1 deletion src/bundle/Resources/config/security.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<services>
<service id="scheb_two_factor.security.authentication.provider.decorator" class="Scheb\TwoFactorBundle\Security\Authentication\Provider\AuthenticationProviderDecorator" abstract="true">
<argument /> <!-- Decorated authentication provider -->
<argument type="service" id="scheb_two_factor.authenticated_token_handler" />
<argument type="service" id="scheb_two_factor.first_two_factor_handler" />
<argument type="service" id="scheb_two_factor.authentication_context_factory" />
<argument type="service" id="security.firewall.map" />
<argument type="service" id="request_stack" />
Expand Down
2 changes: 1 addition & 1 deletion src/bundle/Resources/config/security_authenticator.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<service id="scheb_two_factor.security.listener.token_created" class="Scheb\TwoFactorBundle\Security\TwoFactor\Event\AuthenticationTokenListener" abstract="true">
<argument type="string" /> <!-- Firewall name -->
<argument type="service" id="scheb_two_factor.authenticated_token_handler" />
<argument type="service" id="scheb_two_factor.first_two_factor_handler" />
<argument type="service" id="scheb_two_factor.authentication_context_factory" />
<argument type="service" id="request_stack" />
</service>
Expand Down
12 changes: 12 additions & 0 deletions src/bundle/Resources/config/two_factor.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
<argument>Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContext</argument>
</service>

<!-- The first two-factor handler in the cascade of handlers, alias to easily wire up the right entry point -->
<service id="scheb_two_factor.first_two_factor_handler" alias="scheb_two_factor.condition_handler" />

<service id="scheb_two_factor.condition_handler" class="Scheb\TwoFactorBundle\Security\TwoFactor\Handler\ConditionAuthenticationHandler" lazy="true">
<argument type="service" id="scheb_two_factor.authenticated_token_handler" />
<argument type="service" id="scheb_two_factor.handler_condition" />
</service>

<service id="scheb_two_factor.default_handler_condition" class="Scheb\TwoFactorBundle\Security\TwoFactor\Condition\DefaultTwoFactorCondition" />

<service id="scheb_two_factor.handler_condition" alias="scheb_two_factor.default_handler_condition" />

<service id="scheb_two_factor.authenticated_token_handler" class="Scheb\TwoFactorBundle\Security\TwoFactor\Handler\AuthenticatedTokenHandler" lazy="true">
<argument type="service" id="scheb_two_factor.ip_whitelist_handler" />
<argument>%scheb_two_factor.security_tokens%</argument>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Scheb\TwoFactorBundle\Security\TwoFactor\Condition;

use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;

class DefaultTwoFactorCondition implements TwoFactorConditionInterface
{
public function shouldPerformTwoFactorAuthentication(AuthenticationContextInterface $context): bool
{
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Scheb\TwoFactorBundle\Security\TwoFactor\Condition;

use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;

interface TwoFactorConditionInterface
{
/**
* Check if two-factor authentication should be performed for the user under current conditions.
*/
public function shouldPerformTwoFactorAuthentication(AuthenticationContextInterface $context): bool;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Scheb\TwoFactorBundle\Security\TwoFactor\Handler;

use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Condition\TwoFactorConditionInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class ConditionAuthenticationHandler implements AuthenticationHandlerInterface
{
/**
* @var AuthenticationHandlerInterface
*/
private $authenticationHandler;

/**
* @var TwoFactorConditionInterface
*/
private $condition;

public function __construct(AuthenticationHandlerInterface $authenticationHandler, TwoFactorConditionInterface $condition)
{
$this->authenticationHandler = $authenticationHandler;
$this->condition = $condition;
}

public function beginTwoFactorAuthentication(AuthenticationContextInterface $context): TokenInterface
{
if (!$this->condition->shouldPerformTwoFactorAuthentication($context)) {
return $context->getToken();
}

return $this->authenticationHandler->beginTwoFactorAuthentication($context);
}
}
23 changes: 23 additions & 0 deletions tests/DependencyInjection/SchebTwoFactorExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,28 @@ public function load_alternativePersister_replaceAlias(): void
$this->assertHasAlias('scheb_two_factor.persister', 'acme_test.persister');
}

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

$this->assertHasAlias('scheb_two_factor.handler_condition', 'scheb_two_factor.default_handler_condition');
}

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

$this->assertHasAlias('scheb_two_factor.handler_condition', 'acme_test.two_factor_condition');
}

/**
* @test
*/
Expand Down Expand Up @@ -549,6 +571,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_condition: acme_test.two_factor_condition
trusted_device:
enabled: true
manager: acme_test.trusted_device_manager
Expand Down

0 comments on commit bd37ea3

Please sign in to comment.