Skip to content

Commit

Permalink
feature #36575 [Security] Require entry_point to be configured with m…
Browse files Browse the repository at this point in the history
…ultiple authenticators (wouterj)

This PR was squashed before being merged into the 5.1-dev branch.

Discussion
----------

[Security] Require entry_point to be configured with multiple authenticators

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | tbd

See @weaverryan's comment at symfony/symfony#33558 (comment):

> I have it on my list to look at the entrypoint stuff more closely. But my gut reaction is this: let's fix them (or try to... or maybe in a PR after this) :). What I mean is this:
>
> -    It's always been confusing that your firewall may have multiple auth mechanisms that have their own "entry point"... and one is chosen seemingly at random :). I know it's not random, but why does the entrypoint from `form_login` "win" over `http_basic` if I have both defined under my firewall?
>
> -    Since we're moving to a new system, why not throw an exception the _moment_ that a firewall has multiple entrypoints available to it. Then we _force_ the user to choose the _one_ entrypoint that should be used.

---

**Before** (one authenticator)
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      form_login: ...

# form login is your entry point
```

**After**
Same as before

---

**Before** (multiple authenticators)
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      http_basic: ...
      form_login: ...

# for some reason, FormLogin is now your entry point! (config order doesn't matter)
```

**After**
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      http_basic: ...
      form_login: ...
      entry_point: form_login
```

---

**Before** (custom entry point service)
```yaml
security:
  enable_authenticator_manager: true

  firewalls:
    main:
      http_basic: ...
      form_login: ...
      entry_point: App\Security\CustomEntryPoint
```

**After**
Same as before

Commits
-------

7e861698e7 [Security] Require entry_point to be configured with multiple authenticators
  • Loading branch information
fabpot committed Apr 30, 2020
2 parents e6e1f41 + fe3de3f commit d3a6271
Show file tree
Hide file tree
Showing 16 changed files with 73 additions and 15 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,8 @@ CHANGELOG

* Added XSD for configuration
* Added security configuration for priority-based access decision strategy
* Marked the `AbstractFactory`, `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`, `HttpBasicFactory`, `HttpBasicLdapFactory`, `JsonLoginFactory`, `JsonLoginLdapFactory`, `RememberMeFactory`, `RemoteUserFactory` and `X509Factory` as `@internal`
* Renamed method `AbstractFactory#createEntryPoint()` to `AbstractFactory#createDefaultEntryPoint()`

5.0.0
-----
Expand Down
6 changes: 4 additions & 2 deletions DependencyInjection/Security/Factory/AbstractFactory.php
Expand Up @@ -23,6 +23,8 @@
* @author Fabien Potencier <fabien@symfony.com>
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @internal
*/
abstract class AbstractFactory implements SecurityFactoryInterface
{
Expand Down Expand Up @@ -65,7 +67,7 @@ public function create(ContainerBuilder $container, string $id, array $config, s
}

// create entry point if applicable (optional)
$entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPointId);
$entryPointId = $this->createDefaultEntryPoint($container, $id, $config, $defaultEntryPointId);

return [$authProviderId, $listenerId, $entryPointId];
}
Expand Down Expand Up @@ -126,7 +128,7 @@ abstract protected function getListenerId();
*
* @return string|null the entry point id
*/
protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId)
protected function createDefaultEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId)
{
return $defaultEntryPointId;
}
Expand Down
2 changes: 2 additions & 0 deletions DependencyInjection/Security/Factory/AnonymousFactory.php
Expand Up @@ -18,6 +18,8 @@

/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @internal
*/
class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{
Expand Down
Expand Up @@ -15,6 +15,12 @@
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @internal
* @experimental in Symfony 5.1
*/
class CustomAuthenticatorFactory implements AuthenticatorFactoryInterface, SecurityFactoryInterface
{
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
Expand Down
Expand Up @@ -23,5 +23,5 @@ interface EntryPointFactoryInterface
/**
* Creates the entry point and returns the service ID.
*/
public function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId): string;
public function createEntryPoint(ContainerBuilder $container, string $id, array $config): ?string;
}
9 changes: 8 additions & 1 deletion DependencyInjection/Security/Factory/FormLoginFactory.php
Expand Up @@ -22,6 +22,8 @@
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @internal
*/
class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface, EntryPointFactoryInterface
{
Expand Down Expand Up @@ -90,7 +92,12 @@ protected function createListener(ContainerBuilder $container, string $id, array
return $listenerId;
}

public function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPoint): string
protected function createDefaultEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId)
{
return $this->createEntryPoint($container, $id, $config);
}

public function createEntryPoint(ContainerBuilder $container, string $id, array $config): string
{
$entryPointId = 'security.authentication.form_entry_point.'.$id;
$container
Expand Down
2 changes: 2 additions & 0 deletions DependencyInjection/Security/Factory/FormLoginLdapFactory.php
Expand Up @@ -22,6 +22,8 @@
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*
* @internal
*/
class FormLoginLdapFactory extends FormLoginFactory
{
Expand Down
Expand Up @@ -23,6 +23,8 @@
* Configures the "guard" authentication provider key under a firewall.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*
* @internal
*/
class GuardAuthenticationFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface, EntryPointFactoryInterface
{
Expand Down Expand Up @@ -111,9 +113,15 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
return $authenticatorIds;
}

public function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId): string
public function createEntryPoint(ContainerBuilder $container, string $id, array $config): ?string
{
return $this->determineEntryPoint($defaultEntryPointId, $config);
try {
return $this->determineEntryPoint(null, $config);
} catch (\LogicException $e) {
// ignore the exception, the new system prefers setting "entry_point" over "guard.entry_point"
}

return null;
}

private function determineEntryPoint(?string $defaultEntryPointId, array $config): string
Expand Down
15 changes: 8 additions & 7 deletions DependencyInjection/Security/Factory/HttpBasicFactory.php
Expand Up @@ -20,8 +20,10 @@
* HttpBasicFactory creates services for HTTP basic authentication.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface, EntryPointFactoryInterface
{
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
{
Expand All @@ -34,7 +36,10 @@ public function create(ContainerBuilder $container, string $id, array $config, s
;

// entry point
$entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPoint);
$entryPointId = $defaultEntryPoint;
if (null === $entryPointId) {
$entryPointId = $this->createEntryPoint($container, $id, $config);
}

// listener
$listenerId = 'security.authentication.listener.basic.'.$id;
Expand Down Expand Up @@ -77,12 +82,8 @@ public function addConfiguration(NodeDefinition $node)
;
}

protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPoint)
public function createEntryPoint(ContainerBuilder $container, string $id, array $config): string
{
if (null !== $defaultEntryPoint) {
return $defaultEntryPoint;
}

$entryPointId = 'security.authentication.basic_entry_point.'.$id;
$container
->setDefinition($entryPointId, new ChildDefinition('security.authentication.basic_entry_point'))
Expand Down
2 changes: 2 additions & 0 deletions DependencyInjection/Security/Factory/HttpBasicLdapFactory.php
Expand Up @@ -23,6 +23,8 @@
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*
* @internal
*/
class HttpBasicLdapFactory extends HttpBasicFactory
{
Expand Down
2 changes: 2 additions & 0 deletions DependencyInjection/Security/Factory/JsonLoginFactory.php
Expand Up @@ -19,6 +19,8 @@
* JsonLoginFactory creates services for JSON login authentication.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @internal
*/
class JsonLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface
{
Expand Down
2 changes: 2 additions & 0 deletions DependencyInjection/Security/Factory/JsonLoginLdapFactory.php
Expand Up @@ -19,6 +19,8 @@

/**
* JsonLoginLdapFactory creates services for json login ldap authentication.
*
* @internal
*/
class JsonLoginLdapFactory extends JsonLoginFactory
{
Expand Down
3 changes: 3 additions & 0 deletions DependencyInjection/Security/Factory/RememberMeFactory.php
Expand Up @@ -20,6 +20,9 @@
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;

/**
* @internal
*/
class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{
protected $options = [
Expand Down
2 changes: 2 additions & 0 deletions DependencyInjection/Security/Factory/RemoteUserFactory.php
Expand Up @@ -21,6 +21,8 @@
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Maxime Douailin <maxime.douailin@gmail.com>
*
* @internal
*/
class RemoteUserFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{
Expand Down
2 changes: 2 additions & 0 deletions DependencyInjection/Security/Factory/X509Factory.php
Expand Up @@ -20,6 +20,8 @@
* X509Factory creates services for X509 certificate authentication.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class X509Factory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{
Expand Down
19 changes: 17 additions & 2 deletions DependencyInjection/SecurityExtension.php
Expand Up @@ -39,6 +39,7 @@
use Symfony\Component\Security\Core\User\ChainUserProvider;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Controller\UserValueResolver;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Twig\Extension\AbstractExtension;

/**
Expand Down Expand Up @@ -519,6 +520,7 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
{
$listeners = [];
$hasListeners = false;
$entryPoints = [];

foreach ($this->listenerPositions as $position) {
foreach ($this->factories[$position] as $factory) {
Expand All @@ -541,8 +543,8 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
$authenticationProviders[] = $authenticators;
}

if ($factory instanceof EntryPointFactoryInterface) {
$defaultEntryPoint = $factory->createEntryPoint($container, $id, $firewall[$key], $defaultEntryPoint);
if ($factory instanceof EntryPointFactoryInterface && ($entryPoint = $factory->createEntryPoint($container, $id, $firewall[$key], null))) {
$entryPoints[$key] = $entryPoint;
}
} else {
list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);
Expand All @@ -555,6 +557,19 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
}
}

if ($entryPoints) {
// we can be sure the authenticator system is enabled
if (null !== $defaultEntryPoint) {
return $entryPoints[$defaultEntryPoint] ?? $defaultEntryPoint;
}

if (1 === \count($entryPoints)) {
return current($entryPoints);
}

throw new InvalidConfigurationException(sprintf('Because you have multiple authenticators in firewall "%s", you need to set the "entry_point" key to one of your authenticators (%s) or a service ID implementing "%s". The "entry_point" determines what should happen (e.g. redirect to "/login") when an anonymous user tries to access a protected page.', $id, implode(', ', $entryPoints), AuthenticationEntryPointInterface::class));
}

if (false === $hasListeners) {
throw new InvalidConfigurationException(sprintf('No authentication listener registered for firewall "%s".', $id));
}
Expand Down

0 comments on commit d3a6271

Please sign in to comment.