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:
commit
a114f8d227
@ -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
|
||||
--------
|
||||
|
||||
|
@ -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
|
||||
-----
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ use Symfony\Component\DependencyInjection\Parameter;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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'))
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -19,6 +19,8 @@ use Symfony\Component\Security\Core\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* JsonLoginLdapFactory creates services for json login ldap authentication.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class JsonLoginLdapFactory extends JsonLoginFactory
|
||||
{
|
||||
|
@ -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 = [
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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));
|
||||
}
|
||||
|
Reference in New Issue
Block a user