feature #36575 [Security] Require entry_point to be configured with multiple 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 https://github.com/symfony/symfony/pull/33558#discussion_r403740402:

> 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
This commit is contained in:
Fabien Potencier 2020-04-30 15:22:09 +02:00
commit a114f8d227
17 changed files with 81 additions and 15 deletions

View File

@ -109,6 +109,14 @@ Routing
* Added argument `$priority` to `RouteCollection::add()`
* Deprecated the `RouteCompiler::REGEX_DELIMITER` constant
SecurityBundle
--------------
* Marked the `AbstractFactory`, `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`,
`HttpBasicFactory`, `HttpBasicLdapFactory`, `JsonLoginFactory`, `JsonLoginLdapFactory`, `RememberMeFactory`, `RemoteUserFactory`
and `X509Factory` as `@internal`. Instead of extending these classes, create your own implementation based on
`SecurityFactoryInterface`.
Security
--------

View File

@ -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
-----

View File

@ -23,6 +23,8 @@ use Symfony\Component\DependencyInjection\Reference;
* @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
{
@ -65,7 +67,7 @@ abstract class AbstractFactory implements SecurityFactoryInterface
}
// create entry point if applicable (optional)
$entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPointId);
$entryPointId = $this->createDefaultEntryPoint($container, $id, $config, $defaultEntryPointId);
return [$authProviderId, $listenerId, $entryPointId];
}
@ -126,7 +128,7 @@ abstract class AbstractFactory implements SecurityFactoryInterface
*
* @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;
}

View File

@ -18,6 +18,8 @@ use Symfony\Component\DependencyInjection\Parameter;
/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @internal
*/
class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{

View File

@ -15,6 +15,12 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
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)

View File

@ -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;
}

View File

@ -22,6 +22,8 @@ use Symfony\Component\DependencyInjection\Reference;
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @internal
*/
class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface, EntryPointFactoryInterface
{
@ -90,7 +92,12 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn
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

View File

@ -22,6 +22,8 @@ use Symfony\Component\Security\Core\Exception\LogicException;
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*
* @internal
*/
class FormLoginLdapFactory extends FormLoginFactory
{

View File

@ -23,6 +23,8 @@ use Symfony\Component\Security\Guard\Authenticator\GuardBridgeAuthenticator;
* Configures the "guard" authentication provider key under a firewall.
*
* @author Ryan Weaver <ryan@knpuniversity.com>
*
* @internal
*/
class GuardAuthenticationFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface, EntryPointFactoryInterface
{
@ -111,9 +113,15 @@ class GuardAuthenticationFactory implements SecurityFactoryInterface, Authentica
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

View File

@ -20,8 +20,10 @@ use Symfony\Component\DependencyInjection\Reference;
* 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)
{
@ -34,7 +36,10 @@ class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactory
;
// 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;
@ -77,12 +82,8 @@ class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactory
;
}
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'))

View File

@ -23,6 +23,8 @@ use Symfony\Component\Security\Core\Exception\LogicException;
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*
* @internal
*/
class HttpBasicLdapFactory extends HttpBasicFactory
{

View File

@ -19,6 +19,8 @@ use Symfony\Component\DependencyInjection\Reference;
* JsonLoginFactory creates services for JSON login authentication.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*
* @internal
*/
class JsonLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface
{

View File

@ -19,6 +19,8 @@ use Symfony\Component\Security\Core\Exception\LogicException;
/**
* JsonLoginLdapFactory creates services for json login ldap authentication.
*
* @internal
*/
class JsonLoginLdapFactory extends JsonLoginFactory
{

View File

@ -20,6 +20,9 @@ use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;
/**
* @internal
*/
class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{
protected $options = [

View File

@ -21,6 +21,8 @@ use Symfony\Component\DependencyInjection\Reference;
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Maxime Douailin <maxime.douailin@gmail.com>
*
* @internal
*/
class RemoteUserFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{

View File

@ -20,6 +20,8 @@ use Symfony\Component\DependencyInjection\Reference;
* X509Factory creates services for X509 certificate authentication.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @internal
*/
class X509Factory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{

View File

@ -39,6 +39,7 @@ use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
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;
/**
@ -519,6 +520,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
{
$listeners = [];
$hasListeners = false;
$entryPoints = [];
foreach ($this->listenerPositions as $position) {
foreach ($this->factories[$position] as $factory) {
@ -541,8 +543,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
$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);
@ -555,6 +557,19 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
}
}
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));
}