feature #33558 [Security] AuthenticatorManager to make "authenticators" first-class security (wouterj)
This PR was squashed before being merged into the 5.1-dev branch. Discussion ---------- [Security] AuthenticatorManager to make "authenticators" first-class security | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | - | License | MIT | Doc PR | tbd The tl;dr --- The old authentication listener + authentication provider system was replaced by a new "authenticator" system (similar to Guard authentication). All existing "auth systems" (e.g. `form_login` are now written as an "authenticator" in core). Instead of each "authentication system" registering its own listener in the `Firewall`, there is now only one listener: `AuthenticatorManagerListener` * `Firewall` -> executes `AuthenticatorManagerListener` * `AuthenticatorManagerListener` -> calls `AuthenticatorManager` * `AuthenticatorManager` -> calls each authenticator This PR contains *no deprecations* and the "new system" is *marked as experimental*. This allows to continue to develop the new Security system during the 5.x release cycle without disturbing Symfony users. In 5.4, we can deprecate "old" Security and remove it completely in 6.0. Important Decisions --- * A) **The new authentication manager - `AuthenticatorManager` - now dispatches 3 important "hook" events**: * `VerifyAuthenticatorCredentialsEvent`: occurs at the point when a "password" needs to be checked. Allows us to centralize password checking, CSRF validation, password upgrading and the "user checker" logic. * `LoginSuccessEvent`: Dispatched after a successful authentication. E.g. used by remember me listener. * `LoginFailedEvent`: Dispatched after an unsuccessful authentication. Also used by remember me (and in theory could be used for login throttling). * B) **`getCredentials()`, `getUser()` and `checkCredentials()` methods from old Guard are gone: their logic is centralized**. Authenticators now have an `authenticate(Request $request): PassportInterface` method. A passport contains the user object, the credentials and any other add-in Security badges (e.g. CSRF): ```php public function authenticate(Request $request): PassportInterface { return new Passport( $user, new PasswordCredentials($request->get('_password')), [ new CsrfBadge($request->get('_token')) ] ); } ``` All badges (including the credentials) need to be resolved by listeners to `VerifyAuthenticatorCredentialsEvent`. There is build-in core support for the following badges/credentials: * `PasswordCredentials`: validated using the password encoder factory * `CustomCredentials`: allows a closure to do credentials checking * `CsrfTokenBadge`: automatic CSRF token verification * `PasswordUpgradeBadge`: enables password migration * `RememberMeBadge`: enables remember-me support for this authenticator * C) **`AuthenticatorManager` contains all logic to authenticate** As authenticators always relate to HTTP, the `AuthenticatorManager` contains all logic to authenticate. It has three methods, the most important two are: * `authenticateRequest(Request $request): TokenInterface`: Doing what is previously done by a listener and an authentication provider; * `authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = [])` for manual login in e.g. a controller. * D) **One AuthenticatorManager per firewall** In the old system, there was 1 authentication manager containing all providers and each firewall had a specific firewall listener. In the new system, each firewall has a specific authentication manager. * E) **Pre-authentication tokens are dropped.** As everything is now handled inside `AuthenticatorManager` and everything is stored in the Security `Passport`, there was no need for a token anymore (removing lots of confusion about what information is inside the token). This change deprecates 2 authentication calls: one in `AuthorizationChecker#isGranted()` and one in `AccessListener`. These seem now to be mis-used to reload users (e.g. re-authenticate the user after you change their roles). This (some "way" to change a user's roles *without* logging them out) needs to be "fixed"/added in another PR. * F) **The remember me service now uses *all* user providers** Previously, only user providers of authentication providers listening on that firewall were used. This change is due to practical reasons and we don't think it is common to have 2 user providers supporting the same user instance. In any case, you can always explicitly configure the user provider under `remember_me`. * G) **Auth Providers No Longer Clear the Token on Auth Failure** Previously, authentication providers did `$this->tokenStorage->setToken(null)` upon authentication failure. This is not yet implemented: our reasoning is that if you've authenticated successfully using e.g. the login form, why should you be logged out if you visit the same login form and enter wrong credentials? The pre-authenticated authenticators are an exception here, they do reset the token upon authentication failure, just like the old system. * H) **CSRF Generator Service ID No Longer Configurable** The old Form login authentication provider allowed you to configure the CSRF generator service ID. This is no longer possible with the automated CSRF listener. This feature was introduced in the first CSRF commit and didn't get any updates ever since, so we don't think this feature is required. This could also be accomplished by checking CSRF manually in your authenticator, instead of using the automated check. Future Considerations --- * Remove Security sub-components: Move CSRF to `Symfony\Component\Csrf` (just like mime); Deprecated Guard; Put HTTP + Core as `symfony/security`. This means moving the new classes to `Symfony\Component\Security` * Convert LDAP to the new system * This is fixed (and merged) by #36243 <s>There is a need for some listeners to listen for events on one firewall, but not another (e.g. `RememberMeListener`). This is now fixed by checking the `$providerKey`. We thought it might be nice to introduce a feature to the event dispatcher:</s> * <s>Create one event dispatcher per firewall;</s> * <s>Extend the `kernel.event_subscriber` tag, so that you can optionally specify the dispatcher service ID (to allow listening on events for a specific dispatcher);</s> * <s>Add a listener that always also triggers the events on the main event dispatcher, in case you want a listener that is listening on all firewalls.</s> * Drop the `AnonymousToken` and `AnonymousAuthenticator`: Anonymous authentication has never made much sense and complicates things (e.g. the user can be a string). For access control, an anonymous user has the same meaning as an un-authenticated one (`null`). This require changes in the `AccessListener` and `AuthorizationChecker` and probably also a new Security attribute (to replace `IS_AUTHENTICATED_ANONYMOUSLY`). Related issues: #34909, #30609 > **How to test** > 1. Install the Symfony demo application (or any Symfony application) > 2. Clone my Symfony fork (`git clone git@github.com:wouterj/symfony`) and checkout my branch (`git checkout security/deprecate-providers-listeners`) > 3. Use the link utility to link my fork to the Symfony application: `/path/to/symfony-fork/link /path/to/project` > 4. Enable the new system by setting `security.enable_authenticator_manager` to `true` Commits -------b1e040f311
Rename providerKey to firewallName for more consistent naming50224aa285
Introduce Passport & Badges to extend authenticators9ea32c4ed3
Also use authentication failure/success handlers in FormLoginAuthenticator0fe5083a3e
Added JSON login authenticator7ef6a7ab03
Use the firewall event dispatcher95edc806a1
Added pre-authenticated authenticators (X.509 & REMOTE_USER)f5e11e5f32
Reverted changes to the Guard componentba3754a80f
Differentiate between interactive and non-interactive authenticators6b9d78d5e0
Added tests59f49b20ca
Rename AuthenticatingListener60d396f2d1
Added automatically CSRF protected authenticatorsbf1a452e94
Merge AuthenticatorManager and AuthenticatorHandler44cc76fec2
Use one AuthenticatorManager per firewall09bed16d3d
Only load old manager if new system is disabledddf430fc1e
Added remember me functionality1c810d5d2a
Added support for lazy firewalls7859977324
Removed all mentions of 'guard' in the new system999ec2795f
Refactor to an event based authentication approachb14a5e8c52
Moved new authenticator to the HTTP namespaceb923e4c4f6
Enabled remember me for the GuardManagerListener873b949cf9
Mark new core authenticators as experimental4c06236933
Fixes after testing in Demo applicationfa4b3ec213
Implemented password migration for the new authenticators5efa892395
Create a new core AuthenticatorInterface50132587a1
Add provider key in PreAuthenticationGuardToken526f75608b
Added GuardManagerListenera172bacaa6
Added FormLogin and Anonymous authenticators9b7fddd10c
Integrated GuardAuthenticationManager in the SecurityBundlea6890dbcf0
Created HttpBasicAuthenticator and some Guard traitsc321f4d73a
Created GuardAuthenticationManager to make Guard first-class Security
This commit is contained in:
commit
1abdcbb205
@ -73,6 +73,7 @@ class MainConfiguration implements ConfigurationInterface
|
||||
->booleanNode('hide_user_not_found')->defaultTrue()->end()
|
||||
->booleanNode('always_authenticate_before_granting')->defaultFalse()->end()
|
||||
->booleanNode('erase_credentials')->defaultTrue()->end()
|
||||
->booleanNode('enable_authenticator_manager')->defaultFalse()->info('Enables the new Symfony Security system based on Authenticators, all used authenticators must support this before enabling this.')->end()
|
||||
->arrayNode('access_decision_manager')
|
||||
->addDefaultsIfNotSet()
|
||||
->children()
|
||||
|
@ -30,6 +30,7 @@ abstract class AbstractFactory implements SecurityFactoryInterface
|
||||
'check_path' => '/login_check',
|
||||
'use_forward' => false,
|
||||
'require_previous_session' => false,
|
||||
'login_path' => '/login',
|
||||
];
|
||||
|
||||
protected $defaultSuccessHandlerOptions = [
|
||||
|
@ -19,7 +19,7 @@ use Symfony\Component\DependencyInjection\Parameter;
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class AnonymousFactory implements SecurityFactoryInterface
|
||||
class AnonymousFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
|
||||
{
|
||||
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
|
||||
{
|
||||
@ -42,6 +42,20 @@ class AnonymousFactory implements SecurityFactoryInterface
|
||||
return [$providerId, $listenerId, $defaultEntryPoint];
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
|
||||
{
|
||||
if (null === $config['secret']) {
|
||||
$config['secret'] = new Parameter('container.build_hash');
|
||||
}
|
||||
|
||||
$authenticatorId = 'security.authenticator.anonymous.'.$firewallName;
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.anonymous'))
|
||||
->replaceArgument(0, $config['secret']);
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return 'anonymous';
|
||||
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
interface AuthenticatorFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Creates the authenticator service(s) for the provided configuration.
|
||||
*
|
||||
* @return string|string[] The authenticator service ID(s) to be used by the firewall
|
||||
*/
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId);
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
class CustomAuthenticatorFactory implements AuthenticatorFactoryInterface, SecurityFactoryInterface
|
||||
{
|
||||
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
|
||||
{
|
||||
throw new \LogicException('Custom authenticators are not supported when "security.enable_authenticator_manager" is not set to true.');
|
||||
}
|
||||
|
||||
public function getPosition(): string
|
||||
{
|
||||
return 'pre_auth';
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return 'custom_authenticator';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArrayNodeDefinition $builder
|
||||
*/
|
||||
public function addConfiguration(NodeDefinition $builder)
|
||||
{
|
||||
$builder
|
||||
->fixXmlConfig('service')
|
||||
->children()
|
||||
->arrayNode('services')
|
||||
->info('An array of service ids for all of your "authenticators"')
|
||||
->requiresAtLeastOneElement()
|
||||
->prototype('scalar')->end()
|
||||
->end()
|
||||
->end()
|
||||
;
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): array
|
||||
{
|
||||
return $config['services'];
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
interface EntryPointFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Creates the entry point and returns the service ID.
|
||||
*/
|
||||
public function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId): string;
|
||||
}
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
@ -22,7 +23,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
*/
|
||||
class FormLoginFactory extends AbstractFactory
|
||||
class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface, EntryPointFactoryInterface
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@ -30,6 +31,7 @@ class FormLoginFactory extends AbstractFactory
|
||||
$this->addOption('password_parameter', '_password');
|
||||
$this->addOption('csrf_parameter', '_csrf_token');
|
||||
$this->addOption('csrf_token_id', 'authenticate');
|
||||
$this->addOption('enable_csrf', false);
|
||||
$this->addOption('post_only', true);
|
||||
}
|
||||
|
||||
@ -61,6 +63,10 @@ class FormLoginFactory extends AbstractFactory
|
||||
|
||||
protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId)
|
||||
{
|
||||
if ($config['enable_csrf'] ?? false) {
|
||||
throw new InvalidConfigurationException('The "enable_csrf" option of "form_login" is only available when "security.enable_authenticator_manager" is set to "true", use "csrf_token_generator" instead.');
|
||||
}
|
||||
|
||||
$provider = 'security.authentication.provider.dao.'.$id;
|
||||
$container
|
||||
->setDefinition($provider, new ChildDefinition('security.authentication.provider.dao'))
|
||||
@ -84,7 +90,7 @@ class FormLoginFactory extends AbstractFactory
|
||||
return $listenerId;
|
||||
}
|
||||
|
||||
protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPoint)
|
||||
public function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPoint): string
|
||||
{
|
||||
$entryPointId = 'security.authentication.form_entry_point.'.$id;
|
||||
$container
|
||||
@ -96,4 +102,22 @@ class FormLoginFactory extends AbstractFactory
|
||||
|
||||
return $entryPointId;
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
|
||||
{
|
||||
if (isset($config['csrf_token_generator'])) {
|
||||
throw new InvalidConfigurationException('The "csrf_token_generator" option of "form_login" is only available when "security.enable_authenticator_manager" is set to "false", use "enable_csrf" instead.');
|
||||
}
|
||||
|
||||
$authenticatorId = 'security.authenticator.form_login.'.$firewallName;
|
||||
$options = array_intersect_key($config, $this->options);
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.form_login'))
|
||||
->replaceArgument(1, new Reference($userProviderId))
|
||||
->replaceArgument(2, new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config)))
|
||||
->replaceArgument(3, new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)))
|
||||
->replaceArgument(4, $options);
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class HttpBasicFactory implements SecurityFactoryInterface
|
||||
class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
|
||||
{
|
||||
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
|
||||
{
|
||||
@ -46,6 +46,17 @@ class HttpBasicFactory implements SecurityFactoryInterface
|
||||
return [$provider, $listenerId, $entryPointId];
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
|
||||
{
|
||||
$authenticatorId = 'security.authenticator.http_basic.'.$firewallName;
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.http_basic'))
|
||||
->replaceArgument(0, $config['realm'])
|
||||
->replaceArgument(1, new Reference($userProviderId));
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return 'http';
|
||||
|
@ -20,7 +20,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
*
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
*/
|
||||
class JsonLoginFactory extends AbstractFactory
|
||||
class JsonLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
@ -96,4 +96,18 @@ class JsonLoginFactory extends AbstractFactory
|
||||
|
||||
return $listenerId;
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId)
|
||||
{
|
||||
$authenticatorId = 'security.authenticator.json_login.'.$firewallName;
|
||||
$options = array_intersect_key($config, $this->options);
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.json_login'))
|
||||
->replaceArgument(1, new Reference($userProviderId))
|
||||
->replaceArgument(2, isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $firewallName, $config)) : null)
|
||||
->replaceArgument(3, isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $firewallName, $config)) : null)
|
||||
->replaceArgument(4, $options);
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;
|
||||
|
||||
class RememberMeFactory implements SecurityFactoryInterface
|
||||
class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
|
||||
{
|
||||
protected $options = [
|
||||
'name' => 'REMEMBERME',
|
||||
@ -46,29 +46,8 @@ class RememberMeFactory implements SecurityFactoryInterface
|
||||
;
|
||||
|
||||
// remember me services
|
||||
if (isset($config['service'])) {
|
||||
$templateId = $config['service'];
|
||||
$rememberMeServicesId = $templateId.'.'.$id;
|
||||
} elseif (isset($config['token_provider'])) {
|
||||
$templateId = 'security.authentication.rememberme.services.persistent';
|
||||
$rememberMeServicesId = $templateId.'.'.$id;
|
||||
} else {
|
||||
$templateId = 'security.authentication.rememberme.services.simplehash';
|
||||
$rememberMeServicesId = $templateId.'.'.$id;
|
||||
}
|
||||
|
||||
$rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId));
|
||||
$rememberMeServices->replaceArgument(1, $config['secret']);
|
||||
$rememberMeServices->replaceArgument(2, $id);
|
||||
|
||||
if (isset($config['token_provider'])) {
|
||||
$rememberMeServices->addMethodCall('setTokenProvider', [
|
||||
new Reference($config['token_provider']),
|
||||
]);
|
||||
}
|
||||
|
||||
// remember-me options
|
||||
$rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options));
|
||||
$templateId = $this->generateRememberMeServicesTemplateId($config, $id);
|
||||
$rememberMeServicesId = $templateId.'.'.$id;
|
||||
|
||||
// attach to remember-me aware listeners
|
||||
$userProviders = [];
|
||||
@ -93,17 +72,8 @@ class RememberMeFactory implements SecurityFactoryInterface
|
||||
;
|
||||
}
|
||||
}
|
||||
if ($config['user_providers']) {
|
||||
$userProviders = [];
|
||||
foreach ($config['user_providers'] as $providerName) {
|
||||
$userProviders[] = new Reference('security.user.provider.concrete.'.$providerName);
|
||||
}
|
||||
}
|
||||
if (0 === \count($userProviders)) {
|
||||
throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.');
|
||||
}
|
||||
|
||||
$rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders)));
|
||||
$this->createRememberMeServices($container, $id, $templateId, $userProviders, $config);
|
||||
|
||||
// remember-me listener
|
||||
$listenerId = 'security.authentication.listener.rememberme.'.$id;
|
||||
@ -119,6 +89,42 @@ class RememberMeFactory implements SecurityFactoryInterface
|
||||
return [$authProviderId, $listenerId, $defaultEntryPoint];
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
|
||||
{
|
||||
$templateId = $this->generateRememberMeServicesTemplateId($config, $firewallName);
|
||||
$rememberMeServicesId = $templateId.'.'.$firewallName;
|
||||
|
||||
// create remember me services (which manage the remember me cookies)
|
||||
$this->createRememberMeServices($container, $firewallName, $templateId, [new Reference($userProviderId)], $config);
|
||||
|
||||
// create remember me listener (which executes the remember me services for other authenticators and logout)
|
||||
$this->createRememberMeListener($container, $firewallName, $rememberMeServicesId);
|
||||
|
||||
// create remember me authenticator (which re-authenticates the user based on the remember me cookie)
|
||||
$authenticatorId = 'security.authenticator.remember_me.'.$firewallName;
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me'))
|
||||
->replaceArgument(0, new Reference($rememberMeServicesId))
|
||||
->replaceArgument(3, array_intersect_key($config, $this->options))
|
||||
;
|
||||
|
||||
foreach ($container->findTaggedServiceIds('security.remember_me_aware') as $serviceId => $attributes) {
|
||||
// register ContextListener
|
||||
if ('security.context_listener' === substr($serviceId, 0, 25)) {
|
||||
$container
|
||||
->getDefinition($serviceId)
|
||||
->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)])
|
||||
;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new \LogicException(sprintf('Symfony Authenticator Security dropped support for the "security.remember_me_aware" tag, service "%s" will no longer work as expected.', $serviceId));
|
||||
}
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return 'remember_me';
|
||||
@ -163,4 +169,62 @@ class RememberMeFactory implements SecurityFactoryInterface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function generateRememberMeServicesTemplateId(array $config, string $id): string
|
||||
{
|
||||
if (isset($config['service'])) {
|
||||
return $config['service'];
|
||||
}
|
||||
|
||||
if (isset($config['token_provider'])) {
|
||||
return 'security.authentication.rememberme.services.persistent';
|
||||
}
|
||||
|
||||
return 'security.authentication.rememberme.services.simplehash';
|
||||
}
|
||||
|
||||
private function createRememberMeServices(ContainerBuilder $container, string $id, string $templateId, array $userProviders, array $config): void
|
||||
{
|
||||
$rememberMeServicesId = $templateId.'.'.$id;
|
||||
|
||||
$rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId));
|
||||
$rememberMeServices->replaceArgument(1, $config['secret']);
|
||||
$rememberMeServices->replaceArgument(2, $id);
|
||||
|
||||
if (isset($config['token_provider'])) {
|
||||
$rememberMeServices->addMethodCall('setTokenProvider', [
|
||||
new Reference($config['token_provider']),
|
||||
]);
|
||||
}
|
||||
|
||||
// remember-me options
|
||||
$rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options));
|
||||
|
||||
if ($config['user_providers']) {
|
||||
$userProviders = [];
|
||||
foreach ($config['user_providers'] as $providerName) {
|
||||
$userProviders[] = new Reference('security.user.provider.concrete.'.$providerName);
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === \count($userProviders)) {
|
||||
throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.');
|
||||
}
|
||||
|
||||
$rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders)));
|
||||
}
|
||||
|
||||
private function createRememberMeListener(ContainerBuilder $container, string $id, string $rememberMeServicesId): void
|
||||
{
|
||||
$container
|
||||
->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me'))
|
||||
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id])
|
||||
->replaceArgument(0, new Reference($rememberMeServicesId))
|
||||
;
|
||||
|
||||
$container
|
||||
->setDefinition('security.logout.listener.remember_me.'.$id, new Definition(RememberMeLogoutListener::class))
|
||||
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$id])
|
||||
->addArgument(new Reference($rememberMeServicesId));
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Maxime Douailin <maxime.douailin@gmail.com>
|
||||
*/
|
||||
class RemoteUserFactory implements SecurityFactoryInterface
|
||||
class RemoteUserFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
|
||||
{
|
||||
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
|
||||
{
|
||||
@ -43,6 +43,19 @@ class RemoteUserFactory implements SecurityFactoryInterface
|
||||
return [$providerId, $listenerId, $defaultEntryPoint];
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId)
|
||||
{
|
||||
$authenticatorId = 'security.authenticator.remote_user.'.$firewallName;
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remote_user'))
|
||||
->replaceArgument(0, new Reference($userProviderId))
|
||||
->replaceArgument(2, $firewallName)
|
||||
->replaceArgument(3, $config['user'])
|
||||
;
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return 'pre_auth';
|
||||
|
@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
class X509Factory implements SecurityFactoryInterface
|
||||
class X509Factory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
|
||||
{
|
||||
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
|
||||
{
|
||||
@ -44,6 +44,20 @@ class X509Factory implements SecurityFactoryInterface
|
||||
return [$providerId, $listenerId, $defaultEntryPoint];
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId)
|
||||
{
|
||||
$authenticatorId = 'security.authenticator.x509.'.$firewallName;
|
||||
$container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.x509'))
|
||||
->replaceArgument(0, new Reference($userProviderId))
|
||||
->replaceArgument(2, $firewallName)
|
||||
->replaceArgument(3, $config['user'])
|
||||
->replaceArgument(4, $config['credentials'])
|
||||
;
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
||||
public function getPosition()
|
||||
{
|
||||
return 'pre_auth';
|
||||
|
@ -11,6 +11,8 @@
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\EntryPointFactoryInterface;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
|
||||
@ -21,9 +23,11 @@ use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\DependencyInjection\Alias;
|
||||
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
|
||||
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
@ -32,6 +36,7 @@ use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
|
||||
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 Twig\Extension\AbstractExtension;
|
||||
@ -52,6 +57,8 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
private $userProviderFactories = [];
|
||||
private $statelessFirewallKeys = [];
|
||||
|
||||
private $authenticatorManagerEnabled = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
foreach ($this->listenerPositions as $position) {
|
||||
@ -101,6 +108,12 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
$loader->load('security_listeners.xml');
|
||||
$loader->load('security_rememberme.xml');
|
||||
|
||||
if ($this->authenticatorManagerEnabled = $config['enable_authenticator_manager']) {
|
||||
$loader->load('security_authenticator.xml');
|
||||
} else {
|
||||
$loader->load('security_legacy.xml');
|
||||
}
|
||||
|
||||
if (class_exists(AbstractExtension::class)) {
|
||||
$loader->load('templating_twig.xml');
|
||||
}
|
||||
@ -142,6 +155,14 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
$container->getDefinition('security.authentication.guard_handler')
|
||||
->replaceArgument(2, $this->statelessFirewallKeys);
|
||||
|
||||
if ($this->authenticatorManagerEnabled) {
|
||||
foreach ($this->statelessFirewallKeys as $statelessFirewallId) {
|
||||
$container
|
||||
->setDefinition('security.listener.session.'.$statelessFirewallId, new ChildDefinition('security.listener.session'))
|
||||
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$statelessFirewallId]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config['encoders']) {
|
||||
$this->createEncoders($config['encoders'], $container);
|
||||
}
|
||||
@ -217,9 +238,16 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
foreach ($providerIds as $userProviderId) {
|
||||
$userProviders[] = new Reference($userProviderId);
|
||||
}
|
||||
$arguments[1] = new IteratorArgument($userProviders);
|
||||
$arguments[1] = $userProviderIteratorsArgument = new IteratorArgument($userProviders);
|
||||
$contextListenerDefinition->setArguments($arguments);
|
||||
|
||||
if (\count($userProviders) > 1) {
|
||||
$container->setDefinition('security.user_providers', new Definition(ChainUserProvider::class, [$userProviderIteratorsArgument]))
|
||||
->setPublic(false);
|
||||
} else {
|
||||
$container->setAlias('security.user_providers', new Alias(current($providerIds)))->setPublic(false);
|
||||
}
|
||||
|
||||
if (1 === \count($providerIds)) {
|
||||
$container->setAlias(UserProviderInterface::class, current($providerIds));
|
||||
}
|
||||
@ -254,14 +282,16 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
$mapDef->replaceArgument(0, ServiceLocatorTagPass::register($container, $contextRefs));
|
||||
$mapDef->replaceArgument(1, new IteratorArgument($map));
|
||||
|
||||
// add authentication providers to authentication manager
|
||||
$authenticationProviders = array_map(function ($id) {
|
||||
return new Reference($id);
|
||||
}, array_values(array_unique($authenticationProviders)));
|
||||
$container
|
||||
->getDefinition('security.authentication.manager')
|
||||
->replaceArgument(0, new IteratorArgument($authenticationProviders))
|
||||
;
|
||||
if (!$this->authenticatorManagerEnabled) {
|
||||
// add authentication providers to authentication manager
|
||||
$authenticationProviders = array_map(function ($id) {
|
||||
return new Reference($id);
|
||||
}, array_values(array_unique($authenticationProviders)));
|
||||
|
||||
$container
|
||||
->getDefinition('security.authentication.manager')
|
||||
->replaceArgument(0, new IteratorArgument($authenticationProviders));
|
||||
}
|
||||
|
||||
// register an autowire alias for the UserCheckerInterface if no custom user checker service is configured
|
||||
if (!$customUserChecker) {
|
||||
@ -406,7 +436,35 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
$configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null;
|
||||
|
||||
// Authentication listeners
|
||||
list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId);
|
||||
$firewallAuthenticationProviders = [];
|
||||
list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId);
|
||||
|
||||
if (!$this->authenticatorManagerEnabled) {
|
||||
$authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders);
|
||||
} else {
|
||||
// authenticator manager
|
||||
$authenticators = array_map(function ($id) {
|
||||
return new Reference($id);
|
||||
}, $firewallAuthenticationProviders);
|
||||
$container
|
||||
->setDefinition($managerId = 'security.authenticator.manager.'.$id, new ChildDefinition('security.authenticator.manager'))
|
||||
->replaceArgument(0, $authenticators)
|
||||
->replaceArgument(2, new Reference($firewallEventDispatcherId))
|
||||
->replaceArgument(3, $id)
|
||||
->addTag('monolog.logger', ['channel' => 'security'])
|
||||
;
|
||||
|
||||
$managerLocator = $container->getDefinition('security.authenticator.managers_locator');
|
||||
$managerLocator->replaceArgument(0, array_merge($managerLocator->getArgument(0), [$id => new ServiceClosureArgument(new Reference($managerId))]));
|
||||
|
||||
// authenticator manager listener
|
||||
$container
|
||||
->setDefinition('security.firewall.authenticator.'.$id, new ChildDefinition('security.firewall.authenticator'))
|
||||
->replaceArgument(0, new Reference($managerId))
|
||||
;
|
||||
|
||||
$listeners[] = new Reference('security.firewall.authenticator.'.$id);
|
||||
}
|
||||
|
||||
$config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint);
|
||||
|
||||
@ -467,31 +525,31 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
$key = str_replace('-', '_', $factory->getKey());
|
||||
|
||||
if (isset($firewall[$key])) {
|
||||
if (isset($firewall[$key]['provider'])) {
|
||||
if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$key]['provider'])])) {
|
||||
throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$key]['provider']));
|
||||
}
|
||||
$userProvider = $providerIds[$normalizedName];
|
||||
} elseif ('remember_me' === $key || 'anonymous' === $key) {
|
||||
// RememberMeFactory will use the firewall secret when created, AnonymousAuthenticationListener does not load users.
|
||||
$userProvider = null;
|
||||
$userProvider = $this->getUserProvider($container, $id, $firewall, $key, $defaultProvider, $providerIds, $contextListenerId);
|
||||
|
||||
if ('remember_me' === $key && $contextListenerId) {
|
||||
$container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']);
|
||||
if ($this->authenticatorManagerEnabled) {
|
||||
if (!$factory instanceof AuthenticatorFactoryInterface) {
|
||||
throw new InvalidConfigurationException(sprintf('Cannot configure AuthenticatorManager as "%s" authentication does not support it, set "security.enable_authenticator_manager" to `false`.', $key));
|
||||
}
|
||||
|
||||
$authenticators = $factory->createAuthenticator($container, $id, $firewall[$key], $userProvider);
|
||||
if (\is_array($authenticators)) {
|
||||
foreach ($authenticators as $i => $authenticator) {
|
||||
$authenticationProviders[] = $authenticator;
|
||||
}
|
||||
} else {
|
||||
$authenticationProviders[] = $authenticators;
|
||||
}
|
||||
|
||||
if ($factory instanceof EntryPointFactoryInterface) {
|
||||
$defaultEntryPoint = $factory->createEntryPoint($container, $id, $firewall[$key], $defaultEntryPoint);
|
||||
}
|
||||
} elseif ($defaultProvider) {
|
||||
$userProvider = $defaultProvider;
|
||||
} elseif (empty($providerIds)) {
|
||||
$userProvider = sprintf('security.user.provider.missing.%s', $key);
|
||||
$container->setDefinition($userProvider, (new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id));
|
||||
} else {
|
||||
throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $key, $id));
|
||||
list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);
|
||||
|
||||
$listeners[] = new Reference($listenerId);
|
||||
$authenticationProviders[] = $provider;
|
||||
}
|
||||
|
||||
list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);
|
||||
|
||||
$listeners[] = new Reference($listenerId);
|
||||
$authenticationProviders[] = $provider;
|
||||
$hasListeners = true;
|
||||
}
|
||||
}
|
||||
@ -504,6 +562,41 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
return [$listeners, $defaultEntryPoint];
|
||||
}
|
||||
|
||||
private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): string
|
||||
{
|
||||
if (isset($firewall[$factoryKey]['provider'])) {
|
||||
if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) {
|
||||
throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$factoryKey]['provider']));
|
||||
}
|
||||
|
||||
return $providerIds[$normalizedName];
|
||||
}
|
||||
|
||||
if ('remember_me' === $factoryKey && $contextListenerId) {
|
||||
$container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']);
|
||||
}
|
||||
|
||||
if ($defaultProvider) {
|
||||
return $defaultProvider;
|
||||
}
|
||||
|
||||
if (!$providerIds) {
|
||||
$userProvider = sprintf('security.user.provider.missing.%s', $factoryKey);
|
||||
$container->setDefinition(
|
||||
$userProvider,
|
||||
(new ChildDefinition('security.user.provider.missing'))->replaceArgument(0, $id)
|
||||
);
|
||||
|
||||
return $userProvider;
|
||||
}
|
||||
|
||||
if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) {
|
||||
return 'security.user_providers';
|
||||
}
|
||||
|
||||
throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $id));
|
||||
}
|
||||
|
||||
private function createEncoders(array $encoders, ContainerBuilder $container)
|
||||
{
|
||||
$encoderMap = [];
|
||||
|
@ -12,7 +12,10 @@
|
||||
namespace Symfony\Bundle\SecurityBundle\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
@ -34,6 +37,9 @@ class FirewallEventBubblingListener implements EventSubscriberInterface
|
||||
{
|
||||
return [
|
||||
LogoutEvent::class => 'bubbleEvent',
|
||||
LoginFailureEvent::class => 'bubbleEvent',
|
||||
LoginSuccessEvent::class => 'bubbleEvent',
|
||||
VerifyAuthenticatorCredentialsEvent::class => 'bubbleEvent',
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
<argument type="service" id="security.authentication.session_strategy" />
|
||||
</call>
|
||||
</service>
|
||||
|
||||
|
||||
<service id="Symfony\Component\Security\Guard\GuardAuthenticatorHandler" alias="security.authentication.guard_handler" />
|
||||
|
||||
<!-- See GuardAuthenticationFactory -->
|
||||
|
@ -45,15 +45,6 @@
|
||||
</service>
|
||||
|
||||
<!-- Authentication related services -->
|
||||
<service id="security.authentication.manager" class="Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager">
|
||||
<argument /> <!-- providers -->
|
||||
<argument>%security.authentication.manager.erase_credentials%</argument>
|
||||
<call method="setEventDispatcher">
|
||||
<argument type="service" id="event_dispatcher" />
|
||||
</call>
|
||||
</service>
|
||||
<service id="Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface" alias="security.authentication.manager" />
|
||||
|
||||
<service id="security.authentication.trust_resolver" class="Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver" />
|
||||
|
||||
<service id="security.authentication.session_strategy" class="Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy">
|
||||
|
@ -0,0 +1,154 @@
|
||||
<?xml version="1.0" ?>
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
|
||||
<services>
|
||||
<!-- Manager -->
|
||||
|
||||
<service id="security.authenticator.manager"
|
||||
class="Symfony\Component\Security\Http\Authentication\AuthenticatorManager"
|
||||
abstract="true"
|
||||
>
|
||||
<tag name="monolog.logger" channel="security" />
|
||||
<argument type="abstract">authenticators</argument>
|
||||
<argument type="service" id="security.token_storage" />
|
||||
<argument type="service" id="event_dispatcher" />
|
||||
<argument type="abstract">provider key</argument>
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
<argument>%security.authentication.manager.erase_credentials%</argument>
|
||||
</service>
|
||||
|
||||
<service id="security.authenticator.managers_locator"
|
||||
class="Symfony\Component\DependencyInjection\ServiceLocator">
|
||||
<argument type="collection" />
|
||||
</service>
|
||||
|
||||
<service id="security.user_authenticator"
|
||||
class="Symfony\Bundle\SecurityBundle\Security\UserAuthenticator">
|
||||
<argument type="service" id="security.firewall.map" />
|
||||
<argument type="service" id="security.authenticator.managers_locator" />
|
||||
<argument type="service" id="request_stack" />
|
||||
</service>
|
||||
<service id="Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface" alias="security.user_authenticator" />
|
||||
|
||||
<service id="security.authentication.manager"
|
||||
class="Symfony\Component\Security\Http\Authentication\NoopAuthenticationManager"/>
|
||||
<service id="Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface" alias="security.authentication.manager" />
|
||||
|
||||
<service id="security.firewall.authenticator"
|
||||
class="Symfony\Component\Security\Http\Firewall\AuthenticatorManagerListener"
|
||||
abstract="true">
|
||||
<argument type="abstract">authenticator manager</argument>
|
||||
</service>
|
||||
|
||||
<!-- Listeners -->
|
||||
|
||||
<service id="security.listener.verify_authenticator_credentials" class="Symfony\Component\Security\Http\EventListener\VerifyAuthenticatorCredentialsListener">
|
||||
<tag name="kernel.event_subscriber" />
|
||||
<argument type="service" id="security.encoder_factory" />
|
||||
</service>
|
||||
|
||||
<service id="security.listener.password_migrating" class="Symfony\Component\Security\Http\EventListener\PasswordMigratingListener">
|
||||
<tag name="kernel.event_subscriber" />
|
||||
<argument type="service" id="security.encoder_factory" />
|
||||
</service>
|
||||
|
||||
<service id="security.listener.user_checker" class="Symfony\Component\Security\Http\EventListener\UserCheckerListener">
|
||||
<tag name="kernel.event_subscriber" />
|
||||
<argument type="service" id="Symfony\Component\Security\Core\User\UserCheckerInterface" />
|
||||
</service>
|
||||
|
||||
<service id="security.listener.session"
|
||||
class="Symfony\Component\Security\Http\EventListener\SessionStrategyListener"
|
||||
abstract="true">
|
||||
<argument type="service" id="security.authentication.session_strategy" />
|
||||
<argument type="abstract">stateless firewall keys</argument>
|
||||
</service>
|
||||
|
||||
<service id="security.listener.csrf_protection" class="Symfony\Component\Security\Http\EventListener\CsrfProtectionListener">
|
||||
<tag name="kernel.event_subscriber" />
|
||||
<argument type="service" id="security.csrf.token_manager" />
|
||||
</service>
|
||||
|
||||
<service id="security.listener.remember_me"
|
||||
class="Symfony\Component\Security\Http\EventListener\RememberMeListener"
|
||||
abstract="true">
|
||||
<tag name="monolog.logger" channel="security" />
|
||||
<argument type="abstract">remember me services</argument>
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
</service>
|
||||
|
||||
<!-- Authenticators -->
|
||||
|
||||
<service id="security.authenticator.http_basic"
|
||||
class="Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator"
|
||||
abstract="true">
|
||||
<tag name="monolog.logger" channel="security" />
|
||||
<argument type="abstract">realm name</argument>
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
</service>
|
||||
|
||||
<service id="security.authenticator.form_login"
|
||||
class="Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator"
|
||||
abstract="true">
|
||||
<argument type="service" id="security.http_utils" />
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="abstract">authentication success handler</argument>
|
||||
<argument type="abstract">authentication failure handler</argument>
|
||||
<argument type="abstract">options</argument>
|
||||
</service>
|
||||
|
||||
<service id="security.authenticator.json_login"
|
||||
class="Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator"
|
||||
abstract="true">
|
||||
<argument type="service" id="security.http_utils" />
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="abstract">authentication success handler</argument>
|
||||
<argument type="abstract">authentication failure handler</argument>
|
||||
<argument type="abstract">options</argument>
|
||||
<argument type="service" id="property_accessor" on-invalid="null" />
|
||||
</service>
|
||||
|
||||
<service id="security.authenticator.anonymous"
|
||||
class="Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator"
|
||||
abstract="true">
|
||||
<argument type="abstract">secret</argument>
|
||||
<argument type="service" id="security.untracked_token_storage" />
|
||||
</service>
|
||||
|
||||
<service id="security.authenticator.remember_me"
|
||||
class="Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator"
|
||||
abstract="true">
|
||||
<argument type="abstract">remember me services</argument>
|
||||
<argument>%kernel.secret%</argument>
|
||||
<argument type="service" id="security.token_storage" />
|
||||
<argument type="abstract">options</argument>
|
||||
<argument type="service" id="security.authentication.session_strategy" />
|
||||
</service>
|
||||
|
||||
<service id="security.authenticator.x509"
|
||||
class="Symfony\Component\Security\Http\Authenticator\X509Authenticator"
|
||||
abstract="true">
|
||||
<tag name="monolog.logger" channel="security" />
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="service" id="security.token_storage"/>
|
||||
<argument type="abstract">firewall name</argument>
|
||||
<argument type="abstract">user key</argument>
|
||||
<argument type="abstract">credentials key</argument>
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
</service>
|
||||
|
||||
<service id="security.authenticator.remote_user"
|
||||
class="Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator"
|
||||
abstract="true">
|
||||
<tag name="monolog.logger" channel="security" />
|
||||
<argument type="abstract">user provider</argument>
|
||||
<argument type="service" id="security.token_storage"/>
|
||||
<argument type="abstract">firewall name</argument>
|
||||
<argument type="abstract">user key</argument>
|
||||
<argument type="service" id="logger" on-invalid="null" />
|
||||
</service>
|
||||
</services>
|
||||
</container>
|
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" ?>
|
||||
|
||||
<container xmlns="http://symfony.com/schema/dic/services"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
|
||||
|
||||
<services>
|
||||
<defaults public="false" />
|
||||
|
||||
<!-- Authentication related services -->
|
||||
<service id="security.authentication.manager" class="Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager">
|
||||
<argument /> <!-- providers -->
|
||||
<argument>%security.authentication.manager.erase_credentials%</argument>
|
||||
<call method="setEventDispatcher">
|
||||
<argument type="service" id="event_dispatcher" />
|
||||
</call>
|
||||
</service>
|
||||
<service id="Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface" alias="security.authentication.manager" />
|
||||
</services>
|
||||
</container>
|
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Exception\LogicException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
|
||||
/**
|
||||
* A decorator that delegates all method calls to the authenticator
|
||||
* manager of the current firewall.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in Symfony 5.1
|
||||
*/
|
||||
class UserAuthenticator implements UserAuthenticatorInterface
|
||||
{
|
||||
private $firewallMap;
|
||||
private $userAuthenticators;
|
||||
private $requestStack;
|
||||
|
||||
public function __construct(FirewallMap $firewallMap, ContainerInterface $userAuthenticators, RequestStack $requestStack)
|
||||
{
|
||||
$this->firewallMap = $firewallMap;
|
||||
$this->userAuthenticators = $userAuthenticators;
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
|
||||
public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response
|
||||
{
|
||||
return $this->getUserAuthenticator()->authenticateUser($user, $authenticator, $request);
|
||||
}
|
||||
|
||||
private function getUserAuthenticator(): UserAuthenticatorInterface
|
||||
{
|
||||
$firewallConfig = $this->firewallMap->getFirewallConfig($this->requestStack->getMasterRequest());
|
||||
if (null === $firewallConfig) {
|
||||
throw new LogicException('Cannot call authenticate on this request, as it is not behind a firewall.');
|
||||
}
|
||||
|
||||
return $this->userAuthenticators->get($firewallConfig->getName());
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainC
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory;
|
||||
@ -63,6 +64,7 @@ class SecurityBundle extends Bundle
|
||||
$extension->addSecurityListenerFactory(new RemoteUserFactory());
|
||||
$extension->addSecurityListenerFactory(new GuardAuthenticationFactory());
|
||||
$extension->addSecurityListenerFactory(new AnonymousFactory());
|
||||
$extension->addSecurityListenerFactory(new CustomAuthenticatorFactory());
|
||||
|
||||
$extension->addUserProviderFactory(new InMemoryFactory());
|
||||
$extension->addUserProviderFactory(new LdapFactory());
|
||||
|
@ -9,6 +9,7 @@ CHANGELOG
|
||||
* Hash the persistent RememberMe token value in database.
|
||||
* Added `LogoutEvent` to allow custom logout listeners.
|
||||
* Deprecated `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface` in favor of listening on the `LogoutEvent`.
|
||||
* Added experimental new security using `Http\Authenticator\AuthenticatorInterface`, `Http\Authentication\AuthenticatorManager` and `Http\Firewall\AuthenticatorManagerListener`.
|
||||
|
||||
5.0.0
|
||||
-----
|
||||
|
@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authentication;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\AuthenticationEvents;
|
||||
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
|
||||
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
use Symfony\Component\Security\Http\SecurityEvents;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
* @author Amaury Leroux de Lens <amaury@lerouxdelens.com>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class AuthenticatorManager implements AuthenticatorManagerInterface, UserAuthenticatorInterface
|
||||
{
|
||||
private $authenticators;
|
||||
private $tokenStorage;
|
||||
private $eventDispatcher;
|
||||
private $eraseCredentials;
|
||||
private $logger;
|
||||
private $firewallName;
|
||||
|
||||
/**
|
||||
* @param AuthenticatorInterface[] $authenticators
|
||||
*/
|
||||
public function __construct(iterable $authenticators, TokenStorageInterface $tokenStorage, EventDispatcherInterface $eventDispatcher, string $firewallName, ?LoggerInterface $logger = null, bool $eraseCredentials = true)
|
||||
{
|
||||
$this->authenticators = $authenticators;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->firewallName = $firewallName;
|
||||
$this->logger = $logger;
|
||||
$this->eraseCredentials = $eraseCredentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param BadgeInterface[] $badges Optionally, pass some Passport badges to use for the manual login
|
||||
*/
|
||||
public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response
|
||||
{
|
||||
// create an authenticated token for the User
|
||||
$token = $authenticator->createAuthenticatedToken($passport = new SelfValidatingPassport($user, $badges), $this->firewallName);
|
||||
|
||||
// authenticate this in the system
|
||||
return $this->handleAuthenticationSuccess($token, $passport, $request, $authenticator);
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
if (null !== $this->logger) {
|
||||
$context = ['firewall_key' => $this->firewallName];
|
||||
|
||||
if ($this->authenticators instanceof \Countable || \is_array($this->authenticators)) {
|
||||
$context['authenticators'] = \count($this->authenticators);
|
||||
}
|
||||
|
||||
$this->logger->debug('Checking for authenticator support.', $context);
|
||||
}
|
||||
|
||||
$authenticators = [];
|
||||
$lazy = true;
|
||||
foreach ($this->authenticators as $authenticator) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Checking support on authenticator.', ['firewall_key' => $this->firewallName, 'authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
|
||||
if (false !== $supports = $authenticator->supports($request)) {
|
||||
$authenticators[] = $authenticator;
|
||||
$lazy = $lazy && null === $supports;
|
||||
} elseif (null !== $this->logger) {
|
||||
$this->logger->debug('Authenticator does not support the request.', ['firewall_key' => $this->firewallName, 'authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$authenticators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$request->attributes->set('_security_authenticators', $authenticators);
|
||||
|
||||
return $lazy ? null : true;
|
||||
}
|
||||
|
||||
public function authenticateRequest(Request $request): ?Response
|
||||
{
|
||||
$authenticators = $request->attributes->get('_security_authenticators');
|
||||
$request->attributes->remove('_security_authenticators');
|
||||
if (!$authenticators) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->executeAuthenticators($authenticators, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AuthenticatorInterface[] $authenticators
|
||||
*/
|
||||
private function executeAuthenticators(array $authenticators, Request $request): ?Response
|
||||
{
|
||||
foreach ($authenticators as $authenticator) {
|
||||
// recheck if the authenticator still supports the listener. supports() is called
|
||||
// eagerly (before token storage is initialized), whereas authenticate() is called
|
||||
// lazily (after initialization). This is important for e.g. the AnonymousAuthenticator
|
||||
// as its support is relying on the (initialized) token in the TokenStorage.
|
||||
if (false === $authenticator->supports($request)) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Skipping the "{authenticator}" authenticator as it did not support the request.', ['authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$response = $this->executeAuthenticator($authenticator, $request);
|
||||
if (null !== $response) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('The "{authenticator}" authenticator set the response. Any later authenticator will not be called', ['authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function executeAuthenticator(AuthenticatorInterface $authenticator, Request $request): ?Response
|
||||
{
|
||||
try {
|
||||
// get the passport from the Authenticator
|
||||
$passport = $authenticator->authenticate($request);
|
||||
|
||||
// check the passport (e.g. password checking)
|
||||
$event = new VerifyAuthenticatorCredentialsEvent($authenticator, $passport);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
// check if all badges are resolved
|
||||
$passport->checkIfCompletelyResolved();
|
||||
|
||||
// create the authenticated token
|
||||
$authenticatedToken = $authenticator->createAuthenticatedToken($passport, $this->firewallName);
|
||||
if (true === $this->eraseCredentials) {
|
||||
$authenticatedToken->eraseCredentials();
|
||||
}
|
||||
|
||||
if (null !== $this->eventDispatcher) {
|
||||
$this->eventDispatcher->dispatch(new AuthenticationSuccessEvent($authenticatedToken), AuthenticationEvents::AUTHENTICATION_SUCCESS);
|
||||
}
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->info('Authenticator successful!', ['token' => $authenticatedToken, 'authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
|
||||
// success! (sets the token on the token storage, etc)
|
||||
$response = $this->handleAuthenticationSuccess($authenticatedToken, $passport, $request, $authenticator);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Authenticator set no success response: request continues.', ['authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (AuthenticationException $e) {
|
||||
// oh no! Authentication failed!
|
||||
$response = $this->handleAuthenticationFailure($e, $request, $authenticator);
|
||||
if ($response instanceof Response) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function handleAuthenticationSuccess(TokenInterface $authenticatedToken, PassportInterface $passport, Request $request, AuthenticatorInterface $authenticator): ?Response
|
||||
{
|
||||
$this->tokenStorage->setToken($authenticatedToken);
|
||||
|
||||
$response = $authenticator->onAuthenticationSuccess($request, $authenticatedToken, $this->firewallName);
|
||||
if ($authenticator instanceof InteractiveAuthenticatorInterface && $authenticator->isInteractive()) {
|
||||
$loginEvent = new InteractiveLoginEvent($request, $authenticatedToken);
|
||||
$this->eventDispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
|
||||
}
|
||||
|
||||
if ($passport instanceof AnonymousPassport) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch($loginSuccessEvent = new LoginSuccessEvent($authenticator, $passport, $authenticatedToken, $request, $response, $this->firewallName));
|
||||
|
||||
return $loginSuccessEvent->getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an authentication failure and returns the Response for the authenticator.
|
||||
*/
|
||||
private function handleAuthenticationFailure(AuthenticationException $authenticationException, Request $request, AuthenticatorInterface $authenticator): ?Response
|
||||
{
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->info('Authenticator failed.', ['exception' => $authenticationException, 'authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
|
||||
$response = $authenticator->onAuthenticationFailure($request, $authenticationException);
|
||||
if (null !== $response && null !== $this->logger) {
|
||||
$this->logger->debug('The "{authenticator}" authenticator set the failure response.', ['authenticator' => \get_class($authenticator)]);
|
||||
}
|
||||
|
||||
$this->eventDispatcher->dispatch($loginFailureEvent = new LoginFailureEvent($authenticationException, $authenticator, $request, $response, $this->firewallName));
|
||||
|
||||
// returning null is ok, it means they want the request to continue
|
||||
return $loginFailureEvent->getResponse();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authentication;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Http\Firewall\AbstractListener;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*
|
||||
* @experimental in Symfony 5.1
|
||||
*/
|
||||
interface AuthenticatorManagerInterface
|
||||
{
|
||||
/**
|
||||
* Called to see if authentication should be attempted on this request.
|
||||
*
|
||||
* @see AbstractListener::supports()
|
||||
*/
|
||||
public function supports(Request $request): ?bool;
|
||||
|
||||
/**
|
||||
* Tries to authenticate the request and returns a response - if any authenticator set one.
|
||||
*/
|
||||
public function authenticateRequest(Request $request): ?Response;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authentication;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
|
||||
/**
|
||||
* This class is used when the authenticator system is activated.
|
||||
*
|
||||
* This is used to not break AuthenticationChecker and ContextListener when
|
||||
* using the authenticator system. Once the authenticator system is no longer
|
||||
* experimental, this class can be used trigger deprecation notices.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class NoopAuthenticationManager implements AuthenticationManagerInterface
|
||||
{
|
||||
public function authenticate(TokenInterface $token)
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authentication;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in Symfony 5.1
|
||||
*/
|
||||
interface UserAuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* Convenience method to manually login a user and return a
|
||||
* Response *if any* for success.
|
||||
*/
|
||||
public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request): ?Response;
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\LogicException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken;
|
||||
|
||||
/**
|
||||
* An optional base class that creates the necessary tokens for you.
|
||||
*
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
abstract class AbstractAuthenticator implements AuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* Shortcut to create a PostAuthenticationToken for you, if you don't really
|
||||
* care about which authenticated token you're using.
|
||||
*
|
||||
* @return PostAuthenticationToken
|
||||
*/
|
||||
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
|
||||
{
|
||||
if (!$passport instanceof UserPassportInterface) {
|
||||
throw new LogicException(sprintf('Passport does not contain a user, overwrite "createAuthenticatedToken()" in "%s" to create a custom authenticated token.', \get_class($this)));
|
||||
}
|
||||
|
||||
return new PostAuthenticationToken($passport->getUser(), $firewallName, $passport->getUser()->getRoles());
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
|
||||
/**
|
||||
* A base class to make form login authentication easier!
|
||||
*
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* Return the URL to the login page.
|
||||
*/
|
||||
abstract protected function getLoginUrl(Request $request): string;
|
||||
|
||||
/**
|
||||
* Override to change what happens after a bad username/password is submitted.
|
||||
*/
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
if ($request->hasSession()) {
|
||||
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
|
||||
}
|
||||
|
||||
$url = $this->getLoginUrl($request);
|
||||
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to control what happens when the user hits a secure page
|
||||
* but isn't logged in yet.
|
||||
*/
|
||||
public function start(Request $request, AuthenticationException $authException = null): Response
|
||||
{
|
||||
$url = $this->getLoginUrl($request);
|
||||
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
|
||||
public function isInteractive(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
|
||||
/**
|
||||
* The base authenticator for authenticators to use pre-authenticated
|
||||
* requests (e.g. using certificates).
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @internal
|
||||
* @experimental in Symfony 5.1
|
||||
*/
|
||||
abstract class AbstractPreAuthenticatedAuthenticator implements InteractiveAuthenticatorInterface
|
||||
{
|
||||
private $userProvider;
|
||||
private $tokenStorage;
|
||||
private $firewallName;
|
||||
private $logger;
|
||||
|
||||
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->userProvider = $userProvider;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->firewallName = $firewallName;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username of the pre-authenticated user.
|
||||
*
|
||||
* This authenticator is skipped if null is returned or a custom
|
||||
* BadCredentialsException is thrown.
|
||||
*/
|
||||
abstract protected function extractUsername(Request $request): ?string;
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
try {
|
||||
$username = $this->extractUsername($request);
|
||||
} catch (BadCredentialsException $e) {
|
||||
$this->clearToken($e);
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Skipping pre-authenticated authenticator as a BadCredentialsException is thrown.', ['exception' => $e, 'authenticator' => static::class]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null === $username) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Skipping pre-authenticated authenticator no username could be extracted.', ['authenticator' => static::class]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$request->attributes->set('_pre_authenticated_username', $username);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): PassportInterface
|
||||
{
|
||||
$username = $request->attributes->get('_pre_authenticated_username');
|
||||
$user = $this->userProvider->loadUserByUsername($username);
|
||||
|
||||
return new SelfValidatingPassport($user, [new PreAuthenticatedUserBadge()]);
|
||||
}
|
||||
|
||||
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
|
||||
{
|
||||
return new PreAuthenticatedToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return null; // let the original request continue
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$this->clearToken($exception);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isInteractive(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private function clearToken(AuthenticationException $exception): void
|
||||
{
|
||||
$token = $this->tokenStorage->getToken();
|
||||
if ($token instanceof PreAuthenticatedToken && $this->firewallName === $token->getProviderKey()) {
|
||||
$this->tokenStorage->setToken(null);
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->info('Cleared pre-authenticated token due to an exception.', ['exception' => $exception]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\AnonymousPassport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class AnonymousAuthenticator implements AuthenticatorInterface
|
||||
{
|
||||
private $secret;
|
||||
private $tokenStorage;
|
||||
|
||||
public function __construct(string $secret, TokenStorageInterface $tokenStorage)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
// do not overwrite already stored tokens (i.e. from the session)
|
||||
// the `null` return value indicates that this authenticator supports lazy firewalls
|
||||
return null === $this->tokenStorage->getToken() ? null : false;
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): PassportInterface
|
||||
{
|
||||
return new AnonymousPassport();
|
||||
}
|
||||
|
||||
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
|
||||
{
|
||||
return new AnonymousToken($this->secret, 'anon.', []);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return null; // let the original request continue
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
|
||||
/**
|
||||
* The interface for all authenticators.
|
||||
*
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
* @author Amaury Leroux de Lens <amaury@lerouxdelens.com>
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
interface AuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* Does the authenticator support the given Request?
|
||||
*
|
||||
* If this returns false, the authenticator will be skipped.
|
||||
*
|
||||
* Returning null means authenticate() can be called lazily when accessing the token storage.
|
||||
*/
|
||||
public function supports(Request $request): ?bool;
|
||||
|
||||
/**
|
||||
* Create a passport for the current request.
|
||||
*
|
||||
* The passport contains the user, credentials and any additional information
|
||||
* that has to be checked by the Symfony Security system. For example, a login
|
||||
* form authenticator will probably return a passport containing the user, the
|
||||
* presented password and the CSRF token value.
|
||||
*
|
||||
* You may throw any AuthenticationException in this method in case of error (e.g.
|
||||
* a UsernameNotFoundException when the user cannot be found).
|
||||
*
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
public function authenticate(Request $request): PassportInterface;
|
||||
|
||||
/**
|
||||
* Create an authenticated token for the given user.
|
||||
*
|
||||
* If you don't care about which token class is used or don't really
|
||||
* understand what a "token" is, you can skip this method by extending
|
||||
* the AbstractAuthenticator class from your authenticator.
|
||||
*
|
||||
* @see AbstractAuthenticator
|
||||
*
|
||||
* @param PassportInterface $passport The passport returned from authenticate()
|
||||
*/
|
||||
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface;
|
||||
|
||||
/**
|
||||
* Called when authentication executed and was successful!
|
||||
*
|
||||
* This should return the Response sent back to the user, like a
|
||||
* RedirectResponse to the last page they visited.
|
||||
*
|
||||
* If you return null, the current request will continue, and the user
|
||||
* will be authenticated. This makes sense, for example, with an API.
|
||||
*/
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response;
|
||||
|
||||
/**
|
||||
* Called when authentication executed, but failed (e.g. wrong username password).
|
||||
*
|
||||
* This should return the Response sent back to the user, like a
|
||||
* RedirectResponse to the login page or a 403 response.
|
||||
*
|
||||
* If you return null, the request will continue, but the user will
|
||||
* not be authenticated. This is probably not what you want to do.
|
||||
*/
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response;
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
use Symfony\Component\Security\Http\ParameterBagUtils;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class FormLoginAuthenticator extends AbstractLoginFormAuthenticator
|
||||
{
|
||||
private $httpUtils;
|
||||
private $userProvider;
|
||||
private $successHandler;
|
||||
private $failureHandler;
|
||||
private $options;
|
||||
|
||||
public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options)
|
||||
{
|
||||
$this->httpUtils = $httpUtils;
|
||||
$this->userProvider = $userProvider;
|
||||
$this->successHandler = $successHandler;
|
||||
$this->failureHandler = $failureHandler;
|
||||
$this->options = array_merge([
|
||||
'username_parameter' => '_username',
|
||||
'password_parameter' => '_password',
|
||||
'check_path' => '/login_check',
|
||||
'post_only' => true,
|
||||
'enable_csrf' => false,
|
||||
'csrf_parameter' => '_csrf_token',
|
||||
'csrf_token_id' => 'authenticate',
|
||||
], $options);
|
||||
}
|
||||
|
||||
protected function getLoginUrl(Request $request): string
|
||||
{
|
||||
return $this->httpUtils->generateUri($request, $this->options['login_path']);
|
||||
}
|
||||
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
return ($this->options['post_only'] ? $request->isMethod('POST') : true)
|
||||
&& $this->httpUtils->checkRequestPath($request, $this->options['check_path']);
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): PassportInterface
|
||||
{
|
||||
$credentials = $this->getCredentials($request);
|
||||
$user = $this->userProvider->loadUserByUsername($credentials['username']);
|
||||
if (!$user instanceof UserInterface) {
|
||||
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
|
||||
}
|
||||
|
||||
$passport = new Passport($user, new PasswordCredentials($credentials['password']), [new RememberMeBadge()]);
|
||||
if ($this->options['enable_csrf']) {
|
||||
$passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token']));
|
||||
}
|
||||
|
||||
if ($this->userProvider instanceof PasswordUpgraderInterface) {
|
||||
$passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
|
||||
}
|
||||
|
||||
return $passport;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Passport $passport
|
||||
*/
|
||||
public function createAuthenticatedToken(PassportInterface $passport, $firewallName): TokenInterface
|
||||
{
|
||||
return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return $this->successHandler->onAuthenticationSuccess($request, $token);
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
return $this->failureHandler->onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
|
||||
private function getCredentials(Request $request): array
|
||||
{
|
||||
$credentials = [];
|
||||
$credentials['csrf_token'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']);
|
||||
|
||||
if ($this->options['post_only']) {
|
||||
$credentials['username'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['username_parameter']);
|
||||
$credentials['password'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']) ?? '';
|
||||
} else {
|
||||
$credentials['username'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['username_parameter']);
|
||||
$credentials['password'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']) ?? '';
|
||||
}
|
||||
|
||||
if (!\is_string($credentials['username']) && (!\is_object($credentials['username']) || !method_exists($credentials['username'], '__toString'))) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['username_parameter'], \gettype($credentials['username'])));
|
||||
}
|
||||
|
||||
$credentials['username'] = trim($credentials['username']);
|
||||
|
||||
if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) {
|
||||
throw new BadCredentialsException('Invalid username.');
|
||||
}
|
||||
|
||||
$request->getSession()->set(Security::LAST_USERNAME, $credentials['username']);
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class HttpBasicAuthenticator implements AuthenticatorInterface, AuthenticationEntryPointInterface
|
||||
{
|
||||
private $realmName;
|
||||
private $userProvider;
|
||||
private $logger;
|
||||
|
||||
public function __construct(string $realmName, UserProviderInterface $userProvider, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->realmName = $realmName;
|
||||
$this->userProvider = $userProvider;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function start(Request $request, AuthenticationException $authException = null)
|
||||
{
|
||||
$response = new Response();
|
||||
$response->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', $this->realmName));
|
||||
$response->setStatusCode(401);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return $request->headers->has('PHP_AUTH_USER');
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): PassportInterface
|
||||
{
|
||||
$username = $request->headers->get('PHP_AUTH_USER');
|
||||
$password = $request->headers->get('PHP_AUTH_PW', '');
|
||||
|
||||
$user = $this->userProvider->loadUserByUsername($username);
|
||||
if (!$user instanceof UserInterface) {
|
||||
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
|
||||
}
|
||||
|
||||
$passport = new Passport($user, new PasswordCredentials($password));
|
||||
if ($this->userProvider instanceof PasswordUpgraderInterface) {
|
||||
$passport->addBadge(new PasswordUpgradeBadge($password, $this->userProvider));
|
||||
}
|
||||
|
||||
return $passport;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Passport $passport
|
||||
*/
|
||||
public function createAuthenticatedToken(PassportInterface $passport, $firewallName): TokenInterface
|
||||
{
|
||||
return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $firewallName): ?Response
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->info('Basic authentication failed for user.', ['username' => $request->headers->get('PHP_AUTH_USER'), 'exception' => $exception]);
|
||||
}
|
||||
|
||||
return $this->start($request, $exception);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
/**
|
||||
* This is an extension of the authenticator interface that must
|
||||
* be used by interactive authenticators.
|
||||
*
|
||||
* Interactive login requires explicit user action (e.g. a login
|
||||
* form or HTTP basic authentication). Implementing this interface
|
||||
* will dispatcher the InteractiveLoginEvent upon successful login.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
interface InteractiveAuthenticatorInterface extends AuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* Should return true to make this authenticator perform
|
||||
* an interactive login.
|
||||
*/
|
||||
public function isInteractive(): bool;
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\PropertyAccess\Exception\AccessException;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
|
||||
/**
|
||||
* Provides a stateless implementation of an authentication via
|
||||
* a JSON document composed of a username and a password.
|
||||
*
|
||||
* @author Kévin Dunglas <dunglas@gmail.com>
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface
|
||||
{
|
||||
private $options;
|
||||
private $httpUtils;
|
||||
private $userProvider;
|
||||
private $propertyAccessor;
|
||||
private $successHandler;
|
||||
private $failureHandler;
|
||||
|
||||
public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, ?AuthenticationSuccessHandlerInterface $successHandler = null, ?AuthenticationFailureHandlerInterface $failureHandler = null, array $options = [], ?PropertyAccessorInterface $propertyAccessor = null)
|
||||
{
|
||||
$this->options = array_merge(['username_path' => 'username', 'password_path' => 'password'], $options);
|
||||
$this->httpUtils = $httpUtils;
|
||||
$this->successHandler = $successHandler;
|
||||
$this->failureHandler = $failureHandler;
|
||||
$this->userProvider = $userProvider;
|
||||
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
if (false === strpos($request->getRequestFormat(), 'json') && false === strpos($request->getContentType(), 'json')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): PassportInterface
|
||||
{
|
||||
$credentials = $this->getCredentials($request);
|
||||
$user = $this->userProvider->loadUserByUsername($credentials['username']);
|
||||
if (!$user instanceof UserInterface) {
|
||||
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
|
||||
}
|
||||
|
||||
$passport = new Passport($user, new PasswordCredentials($credentials['password']));
|
||||
if ($this->userProvider instanceof PasswordUpgraderInterface) {
|
||||
$passport->addBadge(new PasswordUpgradeBadge($credentials['password'], $this->userProvider));
|
||||
}
|
||||
|
||||
return $passport;
|
||||
}
|
||||
|
||||
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
|
||||
{
|
||||
return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
if (null === $this->successHandler) {
|
||||
return null; // let the original request continue
|
||||
}
|
||||
|
||||
return $this->successHandler->onAuthenticationSuccess($request, $token);
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
if (null === $this->failureHandler) {
|
||||
return new JsonResponse(['error' => $exception->getMessageKey()], JsonResponse::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return $this->failureHandler->onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
|
||||
public function isInteractive(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getCredentials(Request $request)
|
||||
{
|
||||
$data = json_decode($request->getContent());
|
||||
if (!$data instanceof \stdClass) {
|
||||
throw new BadRequestHttpException('Invalid JSON.');
|
||||
}
|
||||
|
||||
$credentials = [];
|
||||
try {
|
||||
$credentials['username'] = $this->propertyAccessor->getValue($data, $this->options['username_path']);
|
||||
|
||||
if (!\is_string($credentials['username'])) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['username_path']));
|
||||
}
|
||||
|
||||
if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) {
|
||||
throw new BadCredentialsException('Invalid username.');
|
||||
}
|
||||
} catch (AccessException $e) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['username_path']), $e);
|
||||
}
|
||||
|
||||
try {
|
||||
$credentials['password'] = $this->propertyAccessor->getValue($data, $this->options['password_path']);
|
||||
|
||||
if (!\is_string($credentials['password'])) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['password_path']));
|
||||
}
|
||||
} catch (AccessException $e) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['password_path']), $e);
|
||||
}
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport;
|
||||
|
||||
/**
|
||||
* A passport used during anonymous authentication.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @internal
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class AnonymousPassport implements PassportInterface
|
||||
{
|
||||
use PassportTrait;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
|
||||
|
||||
/**
|
||||
* Passport badges allow to add more information to a passport (e.g. a CSRF token).
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
interface BadgeInterface
|
||||
{
|
||||
/**
|
||||
* Checks if this badge is resolved by the security system.
|
||||
*
|
||||
* After authentication, all badges must return `true` in this method in order
|
||||
* for the authentication to succeed.
|
||||
*/
|
||||
public function isResolved(): bool;
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
|
||||
|
||||
use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener;
|
||||
|
||||
/**
|
||||
* Adds automatic CSRF tokens checking capabilities to this authenticator.
|
||||
*
|
||||
* @see CsrfProtectionListener
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in5.1
|
||||
*/
|
||||
class CsrfTokenBadge implements BadgeInterface
|
||||
{
|
||||
private $resolved = false;
|
||||
private $csrfTokenId;
|
||||
private $csrfToken;
|
||||
|
||||
/**
|
||||
* @param string $csrfTokenId An arbitrary string used to generate the value of the CSRF token.
|
||||
* Using a different string for each authenticator improves its security.
|
||||
* @param string|null $csrfToken The CSRF token presented in the request, if any
|
||||
*/
|
||||
public function __construct(string $csrfTokenId, ?string $csrfToken)
|
||||
{
|
||||
$this->csrfTokenId = $csrfTokenId;
|
||||
$this->csrfToken = $csrfToken;
|
||||
}
|
||||
|
||||
public function getCsrfTokenId(): string
|
||||
{
|
||||
return $this->csrfTokenId;
|
||||
}
|
||||
|
||||
public function getCsrfToken(): string
|
||||
{
|
||||
return $this->csrfToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function markResolved(): void
|
||||
{
|
||||
$this->resolved = true;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return $this->resolved;
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
|
||||
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
|
||||
/**
|
||||
* Adds automatic password migration, if enabled and required in the password encoder.
|
||||
*
|
||||
* @see PasswordUpgraderInterface
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class PasswordUpgradeBadge implements BadgeInterface
|
||||
{
|
||||
private $plaintextPassword;
|
||||
private $passwordUpgrader;
|
||||
|
||||
/**
|
||||
* @param string $plaintextPassword The presented password, used in the rehash
|
||||
* @param PasswordUpgraderInterface $passwordUpgrader The password upgrader, usually the UserProvider
|
||||
*/
|
||||
public function __construct(string $plaintextPassword, PasswordUpgraderInterface $passwordUpgrader)
|
||||
{
|
||||
$this->plaintextPassword = $plaintextPassword;
|
||||
$this->passwordUpgrader = $passwordUpgrader;
|
||||
}
|
||||
|
||||
public function getPlaintextPassword(): string
|
||||
{
|
||||
return $this->plaintextPassword;
|
||||
}
|
||||
|
||||
public function getPasswordUpgrader(): PasswordUpgraderInterface
|
||||
{
|
||||
return $this->passwordUpgrader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function eraseCredentials()
|
||||
{
|
||||
$this->plaintextPassword = null;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
|
||||
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractPreAuthenticatedAuthenticator;
|
||||
|
||||
/**
|
||||
* Marks the authentication as being pre-authenticated.
|
||||
*
|
||||
* This disables pre-authentication user checkers.
|
||||
*
|
||||
* @see AbstractPreAuthenticatedAuthenticator
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class PreAuthenticatedUserBadge implements BadgeInterface
|
||||
{
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
|
||||
|
||||
/**
|
||||
* Adds support for remember me to this authenticator.
|
||||
*
|
||||
* Remember me cookie will be set if *all* of the following are met:
|
||||
* A) This badge is present in the Passport
|
||||
* B) The remember_me key under your firewall is configured
|
||||
* C) The "remember me" functionality is activated. This is usually
|
||||
* done by having a _remember_me checkbox in your form, but
|
||||
* can be configured by the "always_remember_me" and "remember_me_parameter"
|
||||
* parameters under the "remember_me" firewall key
|
||||
* D) The authentication process returns a success Response object
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class RememberMeBadge implements BadgeInterface
|
||||
{
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return true; // remember me does not need to be explicitly resolved
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport\Credentials;
|
||||
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
/**
|
||||
* Credentials are a special badge used to explicitly mark the
|
||||
* credential check of an authenticator.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
interface CredentialsInterface extends BadgeInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport\Credentials;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Implements credentials checking using a custom checker function.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class CustomCredentials implements CredentialsInterface
|
||||
{
|
||||
private $customCredentialsChecker;
|
||||
private $credentials;
|
||||
private $resolved = false;
|
||||
|
||||
/**
|
||||
* @param callable $customCredentialsChecker the check function. If this function does not return `true`, a
|
||||
* BadCredentialsException is thrown. You may also throw a more
|
||||
* specific exception in the function.
|
||||
* @param $credentials
|
||||
*/
|
||||
public function __construct(callable $customCredentialsChecker, $credentials)
|
||||
{
|
||||
$this->customCredentialsChecker = $customCredentialsChecker;
|
||||
$this->credentials = $credentials;
|
||||
}
|
||||
|
||||
public function executeCustomChecker(UserInterface $user): void
|
||||
{
|
||||
$checker = $this->customCredentialsChecker;
|
||||
|
||||
if (true !== $checker($this->credentials, $user)) {
|
||||
throw new BadCredentialsException('Credentials check failed as the callable passed to CustomCredentials did not return "true".');
|
||||
}
|
||||
|
||||
$this->resolved = true;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return $this->resolved;
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport\Credentials;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* Implements password credentials.
|
||||
*
|
||||
* These plaintext passwords are checked by the UserPasswordEncoder during
|
||||
* authentication.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class PasswordCredentials implements CredentialsInterface
|
||||
{
|
||||
private $password;
|
||||
private $resolved = false;
|
||||
|
||||
public function __construct(string $password)
|
||||
{
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
if (null === $this->password) {
|
||||
throw new LogicException('The credentials are erased as another listener already verified these credentials.');
|
||||
}
|
||||
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function markResolved(): void
|
||||
{
|
||||
$this->resolved = true;
|
||||
$this->password = null;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return $this->resolved;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CredentialsInterface;
|
||||
|
||||
/**
|
||||
* The default implementation for passports.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class Passport implements UserPassportInterface
|
||||
{
|
||||
use PassportTrait;
|
||||
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @param CredentialsInterface $credentials the credentials to check for this authentication, use
|
||||
* SelfValidatingPassport if no credentials should be checked.
|
||||
* @param BadgeInterface[] $badges
|
||||
*/
|
||||
public function __construct(UserInterface $user, CredentialsInterface $credentials, array $badges = [])
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
$this->addBadge($credentials);
|
||||
foreach ($badges as $badge) {
|
||||
$this->addBadge($badge);
|
||||
}
|
||||
}
|
||||
|
||||
public function getUser(): UserInterface
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
/**
|
||||
* A Passport contains all security-related information that needs to be
|
||||
* validated during authentication.
|
||||
*
|
||||
* A passport badge can be used to add any additional information to the
|
||||
* passport.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
interface PassportInterface
|
||||
{
|
||||
/**
|
||||
* Adds a new security badge.
|
||||
*
|
||||
* A passport can hold only one instance of the same security badge.
|
||||
* This method replaces the current badge if it is already set on this
|
||||
* passport.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addBadge(BadgeInterface $badge): self;
|
||||
|
||||
public function hasBadge(string $badgeFqcn): bool;
|
||||
|
||||
public function getBadge(string $badgeFqcn): ?BadgeInterface;
|
||||
|
||||
/**
|
||||
* Checks if all badges are marked as resolved.
|
||||
*
|
||||
* @throws BadCredentialsException when a badge is not marked as resolved
|
||||
*/
|
||||
public function checkIfCompletelyResolved(): void;
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
trait PassportTrait
|
||||
{
|
||||
/**
|
||||
* @var BadgeInterface[]
|
||||
*/
|
||||
private $badges = [];
|
||||
|
||||
public function addBadge(BadgeInterface $badge): PassportInterface
|
||||
{
|
||||
$this->badges[\get_class($badge)] = $badge;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasBadge(string $badgeFqcn): bool
|
||||
{
|
||||
return isset($this->badges[$badgeFqcn]);
|
||||
}
|
||||
|
||||
public function getBadge(string $badgeFqcn): ?BadgeInterface
|
||||
{
|
||||
return $this->badges[$badgeFqcn] ?? null;
|
||||
}
|
||||
|
||||
public function checkIfCompletelyResolved(): void
|
||||
{
|
||||
foreach ($this->badges as $badge) {
|
||||
if (!$badge->isResolved()) {
|
||||
throw new BadCredentialsException(sprintf('Authentication failed security badge "%s" is not resolved, did you forget to register the correct listeners?', \get_class($badge)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* An implementation used when there are no credentials to be checked (e.g.
|
||||
* API token authentication).
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class SelfValidatingPassport extends Passport
|
||||
{
|
||||
public function __construct(UserInterface $user, array $badges = [])
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
foreach ($badges as $badge) {
|
||||
$this->addBadge($badge);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Passport;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Represents a passport for a Security User.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
interface UserPassportInterface extends PassportInterface
|
||||
{
|
||||
public function getUser(): UserInterface;
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
|
||||
/**
|
||||
* The RememberMe *Authenticator* performs remember me authentication.
|
||||
*
|
||||
* This authenticator is executed whenever a user's session
|
||||
* expired and a remember me cookie was found. This authenticator
|
||||
* then "re-authenticates" the user using the information in the
|
||||
* cookie.
|
||||
*
|
||||
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class RememberMeAuthenticator implements InteractiveAuthenticatorInterface
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $secret;
|
||||
private $tokenStorage;
|
||||
private $options = [];
|
||||
|
||||
public function __construct(RememberMeServicesInterface $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options)
|
||||
{
|
||||
$this->rememberMeServices = $rememberMeServices;
|
||||
$this->secret = $secret;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
// do not overwrite already stored tokens (i.e. from the session)
|
||||
if (null !== $this->tokenStorage->getToken()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (($cookie = $request->attributes->get(AbstractRememberMeServices::COOKIE_ATTR_NAME)) && null === $cookie->getValue()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($this->options['name']) && !$request->cookies->has($this->options['name'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// the `null` return value indicates that this authenticator supports lazy firewalls
|
||||
return null;
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): PassportInterface
|
||||
{
|
||||
$token = $this->rememberMeServices->autoLogin($request);
|
||||
|
||||
return new SelfValidatingPassport($token->getUser());
|
||||
}
|
||||
|
||||
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
|
||||
{
|
||||
return new RememberMeToken($passport->getUser(), $firewallName, $this->secret);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return null; // let the original request continue
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$this->rememberMeServices->loginFail($request, $exception);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isInteractive(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* This authenticator authenticates a remote user.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Maxime Douailin <maxime.douailin@gmail.com>
|
||||
*
|
||||
* @final
|
||||
*
|
||||
* @internal in Symfony 5.1
|
||||
*/
|
||||
class RemoteUserAuthenticator extends AbstractPreAuthenticatedAuthenticator
|
||||
{
|
||||
private $userKey;
|
||||
|
||||
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, string $userKey = 'REMOTE_USER', ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($userProvider, $tokenStorage, $firewallName, $logger);
|
||||
|
||||
$this->userKey = $userKey;
|
||||
}
|
||||
|
||||
protected function extractUsername(Request $request): ?string
|
||||
{
|
||||
if (!$request->server->has($this->userKey)) {
|
||||
throw new BadCredentialsException(sprintf('User key was not found: "%s".', $this->userKey));
|
||||
}
|
||||
|
||||
return $request->server->get($this->userKey);
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator\Token;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class PostAuthenticationToken extends AbstractToken
|
||||
{
|
||||
private $firewallName;
|
||||
|
||||
/**
|
||||
* @param string[] $roles An array of roles
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __construct(UserInterface $user, string $firewallName, array $roles)
|
||||
{
|
||||
parent::__construct($roles);
|
||||
|
||||
if (empty($firewallName)) {
|
||||
throw new \InvalidArgumentException('$firewallName must not be empty.');
|
||||
}
|
||||
|
||||
$this->setUser($user);
|
||||
$this->firewallName = $firewallName;
|
||||
|
||||
// this token is meant to be used after authentication success, so it is always authenticated
|
||||
// you could set it as non authenticated later if you need to
|
||||
$this->setAuthenticated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is meant to be only an authenticated token, where credentials
|
||||
* have already been used and are thus cleared.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCredentials()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getFirewallName(): string
|
||||
{
|
||||
return $this->firewallName;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return [$this->firewallName, parent::__serialize()];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
[$this->firewallName, $parentData] = $data;
|
||||
parent::__unserialize($parentData);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Authenticator;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* This authenticator authenticates pre-authenticated (by the
|
||||
* webserver) X.509 certificates.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* @final
|
||||
* @experimental in Symfony 5.1
|
||||
*/
|
||||
class X509Authenticator extends AbstractPreAuthenticatedAuthenticator
|
||||
{
|
||||
private $userKey;
|
||||
private $credentialsKey;
|
||||
|
||||
public function __construct(UserProviderInterface $userProvider, TokenStorageInterface $tokenStorage, string $firewallName, string $userKey = 'SSL_CLIENT_S_DN_Email', string $credentialsKey = 'SSL_CLIENT_S_DN', ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($userProvider, $tokenStorage, $firewallName, $logger);
|
||||
|
||||
$this->userKey = $userKey;
|
||||
$this->credentialsKey = $credentialsKey;
|
||||
}
|
||||
|
||||
protected function extractUsername(Request $request): string
|
||||
{
|
||||
$username = null;
|
||||
if ($request->server->has($this->userKey)) {
|
||||
$username = $request->server->get($this->userKey);
|
||||
} elseif (
|
||||
$request->server->has($this->credentialsKey)
|
||||
&& preg_match('#emailAddress=([^,/@]++@[^,/]++)#', $request->server->get($this->credentialsKey), $matches)
|
||||
) {
|
||||
$username = $matches[1];
|
||||
}
|
||||
|
||||
if (null === $username) {
|
||||
throw new BadCredentialsException(sprintf('SSL credentials not found: %s, %s', $this->userKey, $this->credentialsKey));
|
||||
}
|
||||
|
||||
return $username;
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Http\Event;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event is dispatched after an error during authentication.
|
||||
*
|
||||
* Listeners to this event can change state based on authentication
|
||||
* failure (e.g. to implement login throttling).
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class LoginFailureEvent extends Event
|
||||
{
|
||||
private $exception;
|
||||
private $authenticator;
|
||||
private $request;
|
||||
private $response;
|
||||
private $firewallName;
|
||||
|
||||
public function __construct(AuthenticationException $exception, AuthenticatorInterface $authenticator, Request $request, ?Response $response, string $firewallName)
|
||||
{
|
||||
$this->exception = $exception;
|
||||
$this->authenticator = $authenticator;
|
||||
$this->request = $request;
|
||||
$this->response = $response;
|
||||
$this->firewallName = $firewallName;
|
||||
}
|
||||
|
||||
public function getException(): AuthenticationException
|
||||
{
|
||||
return $this->exception;
|
||||
}
|
||||
|
||||
public function getAuthenticator(): AuthenticatorInterface
|
||||
{
|
||||
return $this->authenticator;
|
||||
}
|
||||
|
||||
public function getFirewallName(): string
|
||||
{
|
||||
return $this->firewallName;
|
||||
}
|
||||
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function setResponse(?Response $response)
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function getResponse(): ?Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Http\Event;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\LogicException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event is dispatched after authentication has successfully completed.
|
||||
*
|
||||
* At this stage, the authenticator created an authenticated token
|
||||
* and generated an authentication success response. Listeners to
|
||||
* this event can do actions related to successful authentication
|
||||
* (such as migrating the password).
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class LoginSuccessEvent extends Event
|
||||
{
|
||||
private $authenticator;
|
||||
private $passport;
|
||||
private $authenticatedToken;
|
||||
private $request;
|
||||
private $response;
|
||||
private $providerKey;
|
||||
|
||||
public function __construct(AuthenticatorInterface $authenticator, PassportInterface $passport, TokenInterface $authenticatedToken, Request $request, ?Response $response, string $firewallName)
|
||||
{
|
||||
$this->authenticator = $authenticator;
|
||||
$this->passport = $passport;
|
||||
$this->authenticatedToken = $authenticatedToken;
|
||||
$this->request = $request;
|
||||
$this->response = $response;
|
||||
$this->providerKey = $firewallName;
|
||||
}
|
||||
|
||||
public function getAuthenticator(): AuthenticatorInterface
|
||||
{
|
||||
return $this->authenticator;
|
||||
}
|
||||
|
||||
public function getPassport(): PassportInterface
|
||||
{
|
||||
return $this->passport;
|
||||
}
|
||||
|
||||
public function getUser(): UserInterface
|
||||
{
|
||||
if (!$this->passport instanceof UserPassportInterface) {
|
||||
throw new LogicException(sprintf('Cannot call "%s" as the authenticator ("%s") did not set a user.', __METHOD__, \get_class($this->authenticator)));
|
||||
}
|
||||
|
||||
return $this->passport->getUser();
|
||||
}
|
||||
|
||||
public function getAuthenticatedToken(): TokenInterface
|
||||
{
|
||||
return $this->authenticatedToken;
|
||||
}
|
||||
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function getFirewallName(): string
|
||||
{
|
||||
return $this->providerKey;
|
||||
}
|
||||
|
||||
public function setResponse(?Response $response): void
|
||||
{
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function getResponse(): ?Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Http\Event;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event is dispatched when the credentials have to be checked.
|
||||
*
|
||||
* Listeners to this event must validate the user and the
|
||||
* credentials (e.g. default listeners do password verification and
|
||||
* user checking)
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class VerifyAuthenticatorCredentialsEvent extends Event
|
||||
{
|
||||
private $authenticator;
|
||||
private $passport;
|
||||
|
||||
public function __construct(AuthenticatorInterface $authenticator, PassportInterface $passport)
|
||||
{
|
||||
$this->authenticator = $authenticator;
|
||||
$this->passport = $passport;
|
||||
}
|
||||
|
||||
public function getAuthenticator(): AuthenticatorInterface
|
||||
{
|
||||
return $this->authenticator;
|
||||
}
|
||||
|
||||
public function getPassport(): PassportInterface
|
||||
{
|
||||
return $this->passport;
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class CsrfProtectionListener implements EventSubscriberInterface
|
||||
{
|
||||
private $csrfTokenManager;
|
||||
|
||||
public function __construct(CsrfTokenManagerInterface $csrfTokenManager)
|
||||
{
|
||||
$this->csrfTokenManager = $csrfTokenManager;
|
||||
}
|
||||
|
||||
public function verifyCredentials(VerifyAuthenticatorCredentialsEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport->hasBadge(CsrfTokenBadge::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var CsrfTokenBadge $badge */
|
||||
$badge = $passport->getBadge(CsrfTokenBadge::class);
|
||||
if ($badge->isResolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$csrfToken = new CsrfToken($badge->getCsrfTokenId(), $badge->getCsrfToken());
|
||||
|
||||
if (false === $this->csrfTokenManager->isTokenValid($csrfToken)) {
|
||||
throw new InvalidCsrfTokenException('Invalid CSRF token.');
|
||||
}
|
||||
|
||||
$badge->markResolved();
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [VerifyAuthenticatorCredentialsEvent::class => ['verifyCredentials', 256]];
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Http\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class PasswordMigratingListener implements EventSubscriberInterface
|
||||
{
|
||||
private $encoderFactory;
|
||||
|
||||
public function __construct(EncoderFactoryInterface $encoderFactory)
|
||||
{
|
||||
$this->encoderFactory = $encoderFactory;
|
||||
}
|
||||
|
||||
public function onLoginSuccess(LoginSuccessEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport instanceof UserPassportInterface || !$passport->hasBadge(PasswordUpgradeBadge::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var PasswordUpgradeBadge $badge */
|
||||
$badge = $passport->getBadge(PasswordUpgradeBadge::class);
|
||||
$plaintextPassword = $badge->getPlaintextPassword();
|
||||
$badge->eraseCredentials();
|
||||
|
||||
if ('' === $plaintextPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $passport->getUser();
|
||||
$passwordEncoder = $this->encoderFactory->getEncoder($user);
|
||||
if (!$passwordEncoder->needsRehash($user->getPassword())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$badge->getPasswordUpgrader()->upgradePassword($user, $passwordEncoder->encodePassword($plaintextPassword, $user->getSalt()));
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [LoginSuccessEvent::class => 'onLoginSuccess'];
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Http\EventListener;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
|
||||
/**
|
||||
* The RememberMe *listener* creates and deletes remember me cookies.
|
||||
*
|
||||
* Upon login success or failure and support for remember me
|
||||
* in the firewall and authenticator, this listener will create
|
||||
* a remember me cookie.
|
||||
* Upon login failure, all remember me cookies are removed.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class RememberMeListener implements EventSubscriberInterface
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $logger;
|
||||
|
||||
public function __construct(RememberMeServicesInterface $rememberMeServices, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->rememberMeServices = $rememberMeServices;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function onSuccessfulLogin(LoginSuccessEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport->hasBadge(RememberMeBadge::class)) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Remember me skipped: your authenticator does not support it.', ['authenticator' => \get_class($event->getAuthenticator())]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $event->getResponse()) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($event->getAuthenticator())]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rememberMeServices->loginSuccess($event->getRequest(), $event->getResponse(), $event->getAuthenticatedToken());
|
||||
}
|
||||
|
||||
public function onFailedLogin(LoginFailureEvent $event): void
|
||||
{
|
||||
$this->rememberMeServices->loginFail($event->getRequest(), $event->getException());
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LoginSuccessEvent::class => 'onSuccessfulLogin',
|
||||
LoginFailureEvent::class => 'onFailedLogin',
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
|
||||
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
|
||||
|
||||
/**
|
||||
* Migrates/invalidate the session after successful login.
|
||||
*
|
||||
* This should be registered as subscriber to any "stateful" firewalls.
|
||||
*
|
||||
* @see SessionAuthenticationStrategy
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class SessionStrategyListener implements EventSubscriberInterface
|
||||
{
|
||||
private $sessionAuthenticationStrategy;
|
||||
|
||||
public function __construct(SessionAuthenticationStrategyInterface $sessionAuthenticationStrategy)
|
||||
{
|
||||
$this->sessionAuthenticationStrategy = $sessionAuthenticationStrategy;
|
||||
}
|
||||
|
||||
public function onSuccessfulLogin(LoginSuccessEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
$token = $event->getAuthenticatedToken();
|
||||
|
||||
if (!$request->hasSession() || !$request->hasPreviousSession()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sessionAuthenticationStrategy->onAuthentication($request, $token);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [LoginSuccessEvent::class => 'onSuccessfulLogin'];
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Http\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class UserCheckerListener implements EventSubscriberInterface
|
||||
{
|
||||
private $userChecker;
|
||||
|
||||
public function __construct(UserCheckerInterface $userChecker)
|
||||
{
|
||||
$this->userChecker = $userChecker;
|
||||
}
|
||||
|
||||
public function preCredentialsVerification(VerifyAuthenticatorCredentialsEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport instanceof UserPassportInterface || $passport->hasBadge(PreAuthenticatedUserBadge::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->userChecker->checkPreAuth($passport->getUser());
|
||||
}
|
||||
|
||||
public function postCredentialsVerification(LoginSuccessEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport instanceof UserPassportInterface || null === $passport->getUser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->userChecker->checkPostAuth($passport->getUser());
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
VerifyAuthenticatorCredentialsEvent::class => [['preCredentialsVerification', 256]],
|
||||
LoginSuccessEvent::class => ['postCredentialsVerification', 256],
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Http\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
|
||||
/**
|
||||
* This listeners uses the interfaces of authenticators to
|
||||
* determine how to check credentials.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@driveamber.com>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class VerifyAuthenticatorCredentialsListener implements EventSubscriberInterface
|
||||
{
|
||||
private $encoderFactory;
|
||||
|
||||
public function __construct(EncoderFactoryInterface $encoderFactory)
|
||||
{
|
||||
$this->encoderFactory = $encoderFactory;
|
||||
}
|
||||
|
||||
public function onAuthenticating(VerifyAuthenticatorCredentialsEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if ($passport instanceof UserPassportInterface && $passport->hasBadge(PasswordCredentials::class)) {
|
||||
// Use the password encoder to validate the credentials
|
||||
$user = $passport->getUser();
|
||||
/** @var PasswordCredentials $badge */
|
||||
$badge = $passport->getBadge(PasswordCredentials::class);
|
||||
|
||||
if ($badge->isResolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$presentedPassword = $badge->getPassword();
|
||||
if ('' === $presentedPassword) {
|
||||
throw new BadCredentialsException('The presented password cannot be empty.');
|
||||
}
|
||||
|
||||
if (null === $user->getPassword()) {
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
}
|
||||
|
||||
if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
|
||||
throw new BadCredentialsException('The presented password is invalid.');
|
||||
}
|
||||
|
||||
$badge->markResolved();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($passport->hasBadge(CustomCredentials::class)) {
|
||||
/** @var CustomCredentials $badge */
|
||||
$badge = $passport->getBadge(CustomCredentials::class);
|
||||
if ($badge->isResolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$badge->executeCustomChecker($passport->getUser());
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [VerifyAuthenticatorCredentialsEvent::class => ['onAuthenticating', 128]];
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Firewall;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerInterface;
|
||||
|
||||
/**
|
||||
* Firewall authentication listener that delegates to the authenticator system.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.1
|
||||
*/
|
||||
class AuthenticatorManagerListener extends AbstractListener
|
||||
{
|
||||
private $authenticatorManager;
|
||||
|
||||
public function __construct(AuthenticatorManagerInterface $authenticationManager)
|
||||
{
|
||||
$this->authenticatorManager = $authenticationManager;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return $this->authenticatorManager->supports($request);
|
||||
}
|
||||
|
||||
public function authenticate(RequestEvent $event): void
|
||||
{
|
||||
$request = $event->getRequest();
|
||||
$response = $this->authenticatorManager->authenticateRequest($request);
|
||||
if (null === $response) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authentication;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
|
||||
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
|
||||
class AuthenticatorManagerTest extends TestCase
|
||||
{
|
||||
private $tokenStorage;
|
||||
private $eventDispatcher;
|
||||
private $request;
|
||||
private $user;
|
||||
private $token;
|
||||
private $response;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tokenStorage = $this->createMock(TokenStorageInterface::class);
|
||||
$this->eventDispatcher = new EventDispatcher();
|
||||
$this->request = new Request();
|
||||
$this->user = new User('wouter', null);
|
||||
$this->token = $this->createMock(TokenInterface::class);
|
||||
$this->response = $this->createMock(Response::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSupportsData
|
||||
*/
|
||||
public function testSupports($authenticators, $result)
|
||||
{
|
||||
$manager = $this->createManager($authenticators);
|
||||
|
||||
$this->assertEquals($result, $manager->supports($this->request));
|
||||
}
|
||||
|
||||
public function provideSupportsData()
|
||||
{
|
||||
yield [[$this->createAuthenticator(null), $this->createAuthenticator(null)], null];
|
||||
yield [[$this->createAuthenticator(null), $this->createAuthenticator(false)], null];
|
||||
|
||||
yield [[$this->createAuthenticator(null), $this->createAuthenticator(true)], true];
|
||||
yield [[$this->createAuthenticator(true), $this->createAuthenticator(false)], true];
|
||||
|
||||
yield [[$this->createAuthenticator(false), $this->createAuthenticator(false)], false];
|
||||
yield [[], false];
|
||||
}
|
||||
|
||||
public function testSupportCheckedUponRequestAuthentication()
|
||||
{
|
||||
// the attribute stores the supported authenticators, returning false now
|
||||
// means support changed between calling supports() and authenticateRequest()
|
||||
// (which is the case with lazy firewalls and e.g. the AnonymousAuthenticator)
|
||||
$authenticator = $this->createAuthenticator(false);
|
||||
$this->request->attributes->set('_security_authenticators', [$authenticator]);
|
||||
|
||||
$authenticator->expects($this->never())->method('authenticate');
|
||||
|
||||
$manager = $this->createManager([$authenticator]);
|
||||
$manager->authenticateRequest($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMatchingAuthenticatorIndex
|
||||
*/
|
||||
public function testAuthenticateRequest($matchingAuthenticatorIndex)
|
||||
{
|
||||
$authenticators = [$this->createAuthenticator(0 === $matchingAuthenticatorIndex), $this->createAuthenticator(1 === $matchingAuthenticatorIndex)];
|
||||
$this->request->attributes->set('_security_authenticators', $authenticators);
|
||||
$matchingAuthenticator = $authenticators[$matchingAuthenticatorIndex];
|
||||
|
||||
$authenticators[($matchingAuthenticatorIndex + 1) % 2]->expects($this->never())->method('authenticate');
|
||||
|
||||
$matchingAuthenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user));
|
||||
|
||||
$listenerCalled = false;
|
||||
$this->eventDispatcher->addListener(VerifyAuthenticatorCredentialsEvent::class, function (VerifyAuthenticatorCredentialsEvent $event) use (&$listenerCalled, $matchingAuthenticator) {
|
||||
if ($event->getAuthenticator() === $matchingAuthenticator && $event->getPassport()->getUser() === $this->user) {
|
||||
$listenerCalled = true;
|
||||
}
|
||||
});
|
||||
$matchingAuthenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
|
||||
|
||||
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->token);
|
||||
|
||||
$manager = $this->createManager($authenticators);
|
||||
$this->assertNull($manager->authenticateRequest($this->request));
|
||||
$this->assertTrue($listenerCalled, 'The VerifyAuthenticatorCredentialsEvent listener is not called');
|
||||
}
|
||||
|
||||
public function provideMatchingAuthenticatorIndex()
|
||||
{
|
||||
yield [0];
|
||||
yield [1];
|
||||
}
|
||||
|
||||
public function testNoCredentialsValidated()
|
||||
{
|
||||
$authenticator = $this->createAuthenticator();
|
||||
$this->request->attributes->set('_security_authenticators', [$authenticator]);
|
||||
|
||||
$authenticator->expects($this->any())->method('authenticate')->willReturn(new Passport($this->user, new PasswordCredentials('pass')));
|
||||
|
||||
$authenticator->expects($this->once())
|
||||
->method('onAuthenticationFailure')
|
||||
->with($this->request, $this->isInstanceOf(BadCredentialsException::class));
|
||||
|
||||
$manager = $this->createManager([$authenticator]);
|
||||
$manager->authenticateRequest($this->request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideEraseCredentialsData
|
||||
*/
|
||||
public function testEraseCredentials($eraseCredentials)
|
||||
{
|
||||
$authenticator = $this->createAuthenticator();
|
||||
$this->request->attributes->set('_security_authenticators', [$authenticator]);
|
||||
|
||||
$authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user));
|
||||
|
||||
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
|
||||
|
||||
$this->token->expects($eraseCredentials ? $this->once() : $this->never())->method('eraseCredentials');
|
||||
|
||||
$manager = $this->createManager([$authenticator], 'main', $eraseCredentials);
|
||||
$manager->authenticateRequest($this->request);
|
||||
}
|
||||
|
||||
public function provideEraseCredentialsData()
|
||||
{
|
||||
yield [true];
|
||||
yield [false];
|
||||
}
|
||||
|
||||
public function testAuthenticateUser()
|
||||
{
|
||||
$authenticator = $this->createAuthenticator();
|
||||
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
|
||||
$authenticator->expects($this->any())->method('onAuthenticationSuccess')->willReturn($this->response);
|
||||
|
||||
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->token);
|
||||
|
||||
$manager = $this->createManager([$authenticator]);
|
||||
$manager->authenticateUser($this->user, $authenticator, $this->request);
|
||||
}
|
||||
|
||||
public function testInteractiveAuthenticator()
|
||||
{
|
||||
$authenticator = $this->createMock(InteractiveAuthenticatorInterface::class);
|
||||
$authenticator->expects($this->any())->method('isInteractive')->willReturn(true);
|
||||
$this->request->attributes->set('_security_authenticators', [$authenticator]);
|
||||
|
||||
$authenticator->expects($this->any())->method('authenticate')->willReturn(new SelfValidatingPassport($this->user));
|
||||
$authenticator->expects($this->any())->method('createAuthenticatedToken')->willReturn($this->token);
|
||||
|
||||
$this->tokenStorage->expects($this->once())->method('setToken')->with($this->token);
|
||||
|
||||
$authenticator->expects($this->any())
|
||||
->method('onAuthenticationSuccess')
|
||||
->with($this->anything(), $this->token, 'main')
|
||||
->willReturn($this->response);
|
||||
|
||||
$manager = $this->createManager([$authenticator]);
|
||||
$response = $manager->authenticateRequest($this->request);
|
||||
$this->assertSame($this->response, $response);
|
||||
}
|
||||
|
||||
private function createAuthenticator($supports = true)
|
||||
{
|
||||
$authenticator = $this->createMock(InteractiveAuthenticatorInterface::class);
|
||||
$authenticator->expects($this->any())->method('supports')->willReturn($supports);
|
||||
|
||||
return $authenticator;
|
||||
}
|
||||
|
||||
private function createManager($authenticators, $providerKey = 'main', $eraseCredentials = true)
|
||||
{
|
||||
return new AuthenticatorManager($authenticators, $this->tokenStorage, $this->eventDispatcher, $providerKey, null, $eraseCredentials);
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator;
|
||||
|
||||
class AnonymousAuthenticatorTest extends TestCase
|
||||
{
|
||||
private $tokenStorage;
|
||||
private $authenticator;
|
||||
private $request;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tokenStorage = $this->createMock(TokenStorageInterface::class);
|
||||
$this->authenticator = new AnonymousAuthenticator('s3cr3t', $this->tokenStorage);
|
||||
$this->request = new Request();
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSupportsData
|
||||
*/
|
||||
public function testSupports($tokenAlreadyAvailable, $result)
|
||||
{
|
||||
$this->tokenStorage->expects($this->any())->method('getToken')->willReturn($tokenAlreadyAvailable ? $this->createMock(TokenStorageInterface::class) : null);
|
||||
|
||||
$this->assertEquals($result, $this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function provideSupportsData()
|
||||
{
|
||||
yield [true, null];
|
||||
yield [false, false];
|
||||
}
|
||||
|
||||
public function testAuthenticatedToken()
|
||||
{
|
||||
$token = $this->authenticator->createAuthenticatedToken($this->authenticator->authenticate($this->request), 'main');
|
||||
|
||||
$this->assertTrue($token->isAuthenticated());
|
||||
$this->assertEquals('anon.', $token->getUser());
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
|
||||
class FormLoginAuthenticatorTest extends TestCase
|
||||
{
|
||||
private $userProvider;
|
||||
private $successHandler;
|
||||
private $failureHandler;
|
||||
/** @var FormLoginAuthenticator */
|
||||
private $authenticator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userProvider = $this->createMock(UserProviderInterface::class);
|
||||
$this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t'));
|
||||
$this->successHandler = $this->createMock(AuthenticationSuccessHandlerInterface::class);
|
||||
$this->failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideUsernamesForLength
|
||||
*/
|
||||
public function testHandleWhenUsernameLength($username, $ok)
|
||||
{
|
||||
if ($ok) {
|
||||
$this->expectNotToPerformAssertions();
|
||||
} else {
|
||||
$this->expectException(BadCredentialsException::class);
|
||||
$this->expectExceptionMessage('Invalid username.');
|
||||
}
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => $username, '_password' => 's$cr$t']);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator();
|
||||
$this->authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
public function provideUsernamesForLength()
|
||||
{
|
||||
yield [str_repeat('x', Security::MAX_USERNAME_LENGTH + 1), false];
|
||||
yield [str_repeat('x', Security::MAX_USERNAME_LENGTH - 1), true];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider postOnlyDataProvider
|
||||
*/
|
||||
public function testHandleNonStringUsernameWithArray($postOnly)
|
||||
{
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('The key "_username" must be a string, "array" given.');
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => []]);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator(['post_only' => $postOnly]);
|
||||
$this->authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider postOnlyDataProvider
|
||||
*/
|
||||
public function testHandleNonStringUsernameWithInt($postOnly)
|
||||
{
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('The key "_username" must be a string, "integer" given.');
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => 42]);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator(['post_only' => $postOnly]);
|
||||
$this->authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider postOnlyDataProvider
|
||||
*/
|
||||
public function testHandleNonStringUsernameWithObject($postOnly)
|
||||
{
|
||||
$this->expectException(BadRequestHttpException::class);
|
||||
$this->expectExceptionMessage('The key "_username" must be a string, "object" given.');
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => new \stdClass()]);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator(['post_only' => $postOnly]);
|
||||
$this->authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider postOnlyDataProvider
|
||||
*/
|
||||
public function testHandleNonStringUsernameWithToString($postOnly)
|
||||
{
|
||||
$usernameObject = $this->getMockBuilder(DummyUserClass::class)->getMock();
|
||||
$usernameObject->expects($this->once())->method('__toString')->willReturn('someUsername');
|
||||
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => $usernameObject, '_password' => 's$cr$t']);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator(['post_only' => $postOnly]);
|
||||
$this->authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
public function postOnlyDataProvider()
|
||||
{
|
||||
yield [true];
|
||||
yield [false];
|
||||
}
|
||||
|
||||
public function testCsrfProtection()
|
||||
{
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => 'wouter', '_password' => 's$cr$t']);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->setUpAuthenticator(['enable_csrf' => true]);
|
||||
$passport = $this->authenticator->authenticate($request);
|
||||
$this->assertTrue($passport->hasBadge(CsrfTokenBadge::class));
|
||||
}
|
||||
|
||||
public function testUpgradePassword()
|
||||
{
|
||||
$request = Request::create('/login_check', 'POST', ['_username' => 'wouter', '_password' => 's$cr$t']);
|
||||
$request->setSession($this->createSession());
|
||||
|
||||
$this->userProvider = $this->createMock([UserProviderInterface::class, PasswordUpgraderInterface::class]);
|
||||
$this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t'));
|
||||
|
||||
$this->setUpAuthenticator();
|
||||
$passport = $this->authenticator->authenticate($request);
|
||||
$this->assertTrue($passport->hasBadge(PasswordUpgradeBadge::class));
|
||||
$badge = $passport->getBadge(PasswordUpgradeBadge::class);
|
||||
$this->assertEquals('s$cr$t', $badge->getPlaintextPassword());
|
||||
}
|
||||
|
||||
private function setUpAuthenticator(array $options = [])
|
||||
{
|
||||
$this->authenticator = new FormLoginAuthenticator(new HttpUtils(), $this->userProvider, $this->successHandler, $this->failureHandler, $options);
|
||||
}
|
||||
|
||||
private function createSession()
|
||||
{
|
||||
return $this->createMock('Symfony\Component\HttpFoundation\Session\SessionInterface');
|
||||
}
|
||||
}
|
||||
|
||||
class DummyUserClass
|
||||
{
|
||||
public function __toString(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
|
||||
class HttpBasicAuthenticatorTest extends TestCase
|
||||
{
|
||||
private $userProvider;
|
||||
private $encoderFactory;
|
||||
private $encoder;
|
||||
private $authenticator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userProvider = $this->getMockBuilder(UserProviderInterface::class)->getMock();
|
||||
$this->encoderFactory = $this->getMockBuilder(EncoderFactoryInterface::class)->getMock();
|
||||
$this->encoder = $this->getMockBuilder(PasswordEncoderInterface::class)->getMock();
|
||||
$this->encoderFactory
|
||||
->expects($this->any())
|
||||
->method('getEncoder')
|
||||
->willReturn($this->encoder);
|
||||
|
||||
$this->authenticator = new HttpBasicAuthenticator('test', $this->userProvider);
|
||||
}
|
||||
|
||||
public function testExtractCredentialsAndUserFromRequest()
|
||||
{
|
||||
$request = new Request([], [], [], [], [], [
|
||||
'PHP_AUTH_USER' => 'TheUsername',
|
||||
'PHP_AUTH_PW' => 'ThePassword',
|
||||
]);
|
||||
|
||||
$this->userProvider
|
||||
->expects($this->any())
|
||||
->method('loadUserByUsername')
|
||||
->with('TheUsername')
|
||||
->willReturn($user = new User('TheUsername', 'ThePassword'));
|
||||
|
||||
$passport = $this->authenticator->authenticate($request);
|
||||
$this->assertEquals('ThePassword', $passport->getBadge(PasswordCredentials::class)->getPassword());
|
||||
|
||||
$this->assertSame($user, $passport->getUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMissingHttpBasicServerParameters
|
||||
*/
|
||||
public function testHttpBasicServerParametersMissing(array $serverParameters)
|
||||
{
|
||||
$request = new Request([], [], [], [], [], $serverParameters);
|
||||
|
||||
$this->assertFalse($this->authenticator->supports($request));
|
||||
}
|
||||
|
||||
public function provideMissingHttpBasicServerParameters()
|
||||
{
|
||||
return [
|
||||
[[]],
|
||||
[['PHP_AUTH_PW' => 'ThePassword']],
|
||||
];
|
||||
}
|
||||
|
||||
public function testUpgradePassword()
|
||||
{
|
||||
$request = new Request([], [], [], [], [], [
|
||||
'PHP_AUTH_USER' => 'TheUsername',
|
||||
'PHP_AUTH_PW' => 'ThePassword',
|
||||
]);
|
||||
|
||||
$this->userProvider = $this->createMock([UserProviderInterface::class, PasswordUpgraderInterface::class]);
|
||||
$this->userProvider->expects($this->any())->method('loadUserByUsername')->willReturn(new User('test', 's$cr$t'));
|
||||
$authenticator = new HttpBasicAuthenticator('test', $this->userProvider);
|
||||
|
||||
$passport = $authenticator->authenticate($request);
|
||||
$this->assertTrue($passport->hasBadge(PasswordUpgradeBadge::class));
|
||||
$badge = $passport->getBadge(PasswordUpgradeBadge::class);
|
||||
$this->assertEquals('ThePassword', $badge->getPlaintextPassword());
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
|
||||
class JsonLoginAuthenticatorTest extends TestCase
|
||||
{
|
||||
private $userProvider;
|
||||
/** @var JsonLoginAuthenticator */
|
||||
private $authenticator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userProvider = $this->createMock(UserProviderInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSupportData
|
||||
*/
|
||||
public function testSupport($request)
|
||||
{
|
||||
$this->setUpAuthenticator();
|
||||
|
||||
$this->assertTrue($this->authenticator->supports($request));
|
||||
}
|
||||
|
||||
public function provideSupportData()
|
||||
{
|
||||
yield [new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}')];
|
||||
|
||||
$request = new Request([], [], [], [], [], [], '{"username": "dunglas", "password": "foo"}');
|
||||
$request->setRequestFormat('json-ld');
|
||||
yield [$request];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSupportsWithCheckPathData
|
||||
*/
|
||||
public function testSupportsWithCheckPath($request, $result)
|
||||
{
|
||||
$this->setUpAuthenticator(['check_path' => '/api/login']);
|
||||
|
||||
$this->assertSame($result, $this->authenticator->supports($request));
|
||||
}
|
||||
|
||||
public function provideSupportsWithCheckPathData()
|
||||
{
|
||||
yield [Request::create('/api/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), true];
|
||||
yield [Request::create('/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), false];
|
||||
}
|
||||
|
||||
public function testAuthenticate()
|
||||
{
|
||||
$this->setUpAuthenticator();
|
||||
|
||||
$this->userProvider->expects($this->once())->method('loadUserByUsername')->with('dunglas')->willReturn(new User('dunglas', 'pa$$'));
|
||||
|
||||
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}');
|
||||
$passport = $this->authenticator->authenticate($request);
|
||||
$this->assertEquals('foo', $passport->getBadge(PasswordCredentials::class)->getPassword());
|
||||
}
|
||||
|
||||
public function testAuthenticateWithCustomPath()
|
||||
{
|
||||
$this->setUpAuthenticator([
|
||||
'username_path' => 'authentication.username',
|
||||
'password_path' => 'authentication.password',
|
||||
]);
|
||||
|
||||
$this->userProvider->expects($this->once())->method('loadUserByUsername')->with('dunglas')->willReturn(new User('dunglas', 'pa$$'));
|
||||
|
||||
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"authentication": {"username": "dunglas", "password": "foo"}}');
|
||||
$passport = $this->authenticator->authenticate($request);
|
||||
$this->assertEquals('foo', $passport->getBadge(PasswordCredentials::class)->getPassword());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideInvalidAuthenticateData
|
||||
*/
|
||||
public function testAuthenticateInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class)
|
||||
{
|
||||
$this->expectException($exceptionType);
|
||||
$this->expectExceptionMessage($errorMessage);
|
||||
|
||||
$this->setUpAuthenticator();
|
||||
|
||||
$this->authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
public function provideInvalidAuthenticateData()
|
||||
{
|
||||
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']);
|
||||
yield [$request, 'Invalid JSON.'];
|
||||
|
||||
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"usr": "dunglas", "password": "foo"}');
|
||||
yield [$request, 'The key "username" must be provided'];
|
||||
|
||||
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "pass": "foo"}');
|
||||
yield [$request, 'The key "password" must be provided'];
|
||||
|
||||
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": 1, "password": "foo"}');
|
||||
yield [$request, 'The key "username" must be a string.'];
|
||||
|
||||
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": 1}');
|
||||
yield [$request, 'The key "password" must be a string.'];
|
||||
|
||||
$username = str_repeat('x', Security::MAX_USERNAME_LENGTH + 1);
|
||||
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], sprintf('{"username": "%s", "password": 1}', $username));
|
||||
yield [$request, 'Invalid username.', BadCredentialsException::class];
|
||||
}
|
||||
|
||||
private function setUpAuthenticator(array $options = [])
|
||||
{
|
||||
$this->authenticator = new JsonLoginAuthenticator(new HttpUtils(), $this->userProvider, null, null, $options);
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator;
|
||||
use Symfony\Component\Security\Http\RememberMe\AbstractRememberMeServices;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
|
||||
class RememberMeAuthenticatorTest extends TestCase
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $tokenStorage;
|
||||
private $authenticator;
|
||||
private $request;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->rememberMeServices = $this->createMock(RememberMeServicesInterface::class);
|
||||
$this->tokenStorage = $this->createMock(TokenStorage::class);
|
||||
$this->authenticator = new RememberMeAuthenticator($this->rememberMeServices, 's3cr3t', $this->tokenStorage, [
|
||||
'name' => '_remember_me_cookie',
|
||||
]);
|
||||
$this->request = new Request();
|
||||
$this->request->cookies->set('_remember_me_cookie', $val = $this->generateCookieValue());
|
||||
$this->request->attributes->set(AbstractRememberMeServices::COOKIE_ATTR_NAME, new Cookie('_remember_me_cookie', $val));
|
||||
}
|
||||
|
||||
public function testSupportsTokenStorageWithToken()
|
||||
{
|
||||
$this->tokenStorage->expects($this->any())->method('getToken')->willReturn(TokenInterface::class);
|
||||
|
||||
$this->assertFalse($this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function testSupportsRequestWithoutAttribute()
|
||||
{
|
||||
$this->request->attributes->remove(AbstractRememberMeServices::COOKIE_ATTR_NAME);
|
||||
|
||||
$this->assertNull($this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function testSupportsRequestWithoutCookie()
|
||||
{
|
||||
$this->request->cookies->remove('_remember_me_cookie');
|
||||
|
||||
$this->assertFalse($this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function testSupports()
|
||||
{
|
||||
$this->assertNull($this->authenticator->supports($this->request));
|
||||
}
|
||||
|
||||
public function testAuthenticate()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->once())
|
||||
->method('autoLogin')
|
||||
->with($this->request)
|
||||
->willReturn(new RememberMeToken($user = new User('wouter', 'test'), 'main', 'secret'));
|
||||
|
||||
$passport = $this->authenticator->authenticate($this->request);
|
||||
|
||||
$this->assertSame($user, $passport->getUser());
|
||||
}
|
||||
|
||||
private function generateCookieValue()
|
||||
{
|
||||
return base64_encode(implode(AbstractRememberMeServices::COOKIE_DELIMITER, ['part1', 'part2']));
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator;
|
||||
|
||||
class RemoteUserAuthenticatorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideAuthenticators
|
||||
*/
|
||||
public function testSupport(UserProviderInterface $userProvider, RemoteUserAuthenticator $authenticator, $parameterName)
|
||||
{
|
||||
$request = $this->createRequest([$parameterName => 'TheUsername']);
|
||||
|
||||
$this->assertTrue($authenticator->supports($request));
|
||||
}
|
||||
|
||||
public function testSupportNoUser()
|
||||
{
|
||||
$authenticator = new RemoteUserAuthenticator($this->createMock(UserProviderInterface::class), new TokenStorage(), 'main');
|
||||
|
||||
$this->assertFalse($authenticator->supports($this->createRequest([])));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideAuthenticators
|
||||
*/
|
||||
public function testAuthenticate(UserProviderInterface $userProvider, RemoteUserAuthenticator $authenticator, $parameterName)
|
||||
{
|
||||
$request = $this->createRequest([$parameterName => 'TheUsername']);
|
||||
|
||||
$authenticator->supports($request);
|
||||
|
||||
$userProvider->expects($this->once())
|
||||
->method('loadUserByUsername')
|
||||
->with('TheUsername')
|
||||
->willReturn($user = new User('TheUsername', null));
|
||||
|
||||
$passport = $authenticator->authenticate($request);
|
||||
$this->assertEquals($user, $passport->getUser());
|
||||
}
|
||||
|
||||
public function provideAuthenticators()
|
||||
{
|
||||
$userProvider = $this->createMock(UserProviderInterface::class);
|
||||
yield [$userProvider, new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main'), 'REMOTE_USER'];
|
||||
|
||||
$userProvider = $this->createMock(UserProviderInterface::class);
|
||||
yield [$userProvider, new RemoteUserAuthenticator($userProvider, new TokenStorage(), 'main', 'CUSTOM_USER_PARAMETER'), 'CUSTOM_USER_PARAMETER'];
|
||||
}
|
||||
|
||||
private function createRequest(array $server)
|
||||
{
|
||||
return new Request([], [], [], [], [], $server);
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\Authenticator;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\X509Authenticator;
|
||||
|
||||
class X509AuthenticatorTest extends TestCase
|
||||
{
|
||||
private $userProvider;
|
||||
private $authenticator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userProvider = $this->createMock(UserProviderInterface::class);
|
||||
$this->authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideServerVars
|
||||
*/
|
||||
public function testAuthentication($user, $credentials)
|
||||
{
|
||||
$serverVars = [];
|
||||
if ('' !== $user) {
|
||||
$serverVars['SSL_CLIENT_S_DN_Email'] = $user;
|
||||
}
|
||||
if ('' !== $credentials) {
|
||||
$serverVars['SSL_CLIENT_S_DN'] = $credentials;
|
||||
}
|
||||
|
||||
$request = $this->createRequest($serverVars);
|
||||
$this->assertTrue($this->authenticator->supports($request));
|
||||
|
||||
$this->userProvider->expects($this->once())
|
||||
->method('loadUserByUsername')
|
||||
->with($user)
|
||||
->willReturn(new User($user, null));
|
||||
|
||||
$this->authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
public static function provideServerVars()
|
||||
{
|
||||
yield ['TheUser', 'TheCredentials'];
|
||||
yield ['TheUser', ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideServerVarsNoUser
|
||||
*/
|
||||
public function testAuthenticationNoUser($emailAddress, $credentials)
|
||||
{
|
||||
$request = $this->createRequest(['SSL_CLIENT_S_DN' => $credentials]);
|
||||
|
||||
$this->assertTrue($this->authenticator->supports($request));
|
||||
|
||||
$this->userProvider->expects($this->once())
|
||||
->method('loadUserByUsername')
|
||||
->with($emailAddress)
|
||||
->willReturn(new User($emailAddress, null));
|
||||
|
||||
$this->authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
public static function provideServerVarsNoUser()
|
||||
{
|
||||
yield ['cert@example.com', 'CN=Sample certificate DN/emailAddress=cert@example.com'];
|
||||
yield ['cert+something@example.com', 'CN=Sample certificate DN/emailAddress=cert+something@example.com'];
|
||||
yield ['cert@example.com', 'CN=Sample certificate DN,emailAddress=cert@example.com'];
|
||||
yield ['cert+something@example.com', 'CN=Sample certificate DN,emailAddress=cert+something@example.com'];
|
||||
yield ['cert+something@example.com', 'emailAddress=cert+something@example.com,CN=Sample certificate DN'];
|
||||
yield ['cert+something@example.com', 'emailAddress=cert+something@example.com'];
|
||||
yield ['firstname.lastname@mycompany.co.uk', 'emailAddress=firstname.lastname@mycompany.co.uk,CN=Firstname.Lastname,OU=london,OU=company design and engineering,OU=Issuer London,OU=Roaming,OU=Interactive,OU=Users,OU=Standard,OU=Business,DC=england,DC=core,DC=company,DC=co,DC=uk'];
|
||||
}
|
||||
|
||||
public function testSupportNoData()
|
||||
{
|
||||
$request = $this->createRequest([]);
|
||||
|
||||
$this->assertFalse($this->authenticator->supports($request));
|
||||
}
|
||||
|
||||
public function testAuthenticationCustomUserKey()
|
||||
{
|
||||
$authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main', 'TheUserKey');
|
||||
|
||||
$request = $this->createRequest([
|
||||
'TheUserKey' => 'TheUser',
|
||||
]);
|
||||
$this->assertTrue($authenticator->supports($request));
|
||||
|
||||
$this->userProvider->expects($this->once())
|
||||
->method('loadUserByUsername')
|
||||
->with('TheUser')
|
||||
->willReturn(new User('TheUser', null));
|
||||
|
||||
$authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
public function testAuthenticationCustomCredentialsKey()
|
||||
{
|
||||
$authenticator = new X509Authenticator($this->userProvider, new TokenStorage(), 'main', 'SSL_CLIENT_S_DN_Email', 'TheCertKey');
|
||||
|
||||
$request = $this->createRequest([
|
||||
'TheCertKey' => 'CN=Sample certificate DN/emailAddress=cert@example.com',
|
||||
]);
|
||||
$this->assertTrue($authenticator->supports($request));
|
||||
|
||||
$this->userProvider->expects($this->once())
|
||||
->method('loadUserByUsername')
|
||||
->with('cert@example.com')
|
||||
->willReturn(new User('cert@example.com', null));
|
||||
|
||||
$authenticator->authenticate($request);
|
||||
}
|
||||
|
||||
private function createRequest(array $server)
|
||||
{
|
||||
return new Request([], [], [], [], [], $server);
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\CsrfProtectionListener;
|
||||
|
||||
class CsrfProtectionListenerTest extends TestCase
|
||||
{
|
||||
private $csrfTokenManager;
|
||||
private $listener;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
|
||||
$this->listener = new CsrfProtectionListener($this->csrfTokenManager);
|
||||
}
|
||||
|
||||
public function testNoCsrfTokenBadge()
|
||||
{
|
||||
$this->csrfTokenManager->expects($this->never())->method('isTokenValid');
|
||||
|
||||
$event = $this->createEvent($this->createPassport(null));
|
||||
$this->listener->verifyCredentials($event);
|
||||
}
|
||||
|
||||
public function testValidCsrfToken()
|
||||
{
|
||||
$this->csrfTokenManager->expects($this->any())
|
||||
->method('isTokenValid')
|
||||
->with(new CsrfToken('authenticator_token_id', 'abc123'))
|
||||
->willReturn(true);
|
||||
|
||||
$event = $this->createEvent($this->createPassport(new CsrfTokenBadge('authenticator_token_id', 'abc123')));
|
||||
$this->listener->verifyCredentials($event);
|
||||
|
||||
$this->expectNotToPerformAssertions();
|
||||
}
|
||||
|
||||
public function testInvalidCsrfToken()
|
||||
{
|
||||
$this->expectException(InvalidCsrfTokenException::class);
|
||||
$this->expectExceptionMessage('Invalid CSRF token.');
|
||||
|
||||
$this->csrfTokenManager->expects($this->any())
|
||||
->method('isTokenValid')
|
||||
->with(new CsrfToken('authenticator_token_id', 'abc123'))
|
||||
->willReturn(false);
|
||||
|
||||
$event = $this->createEvent($this->createPassport(new CsrfTokenBadge('authenticator_token_id', 'abc123')));
|
||||
$this->listener->verifyCredentials($event);
|
||||
}
|
||||
|
||||
private function createEvent($passport)
|
||||
{
|
||||
return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport);
|
||||
}
|
||||
|
||||
private function createPassport(?CsrfTokenBadge $badge)
|
||||
{
|
||||
$passport = new SelfValidatingPassport(new User('wouter', 'pass'));
|
||||
if ($badge) {
|
||||
$passport->addBadge($badge);
|
||||
}
|
||||
|
||||
return $passport;
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener;
|
||||
|
||||
class PasswordMigratingListenerTest extends TestCase
|
||||
{
|
||||
private $encoderFactory;
|
||||
private $listener;
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->encoderFactory = $this->createMock(EncoderFactoryInterface::class);
|
||||
$this->listener = new PasswordMigratingListener($this->encoderFactory);
|
||||
$this->user = $this->createMock(UserInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideUnsupportedEvents
|
||||
*/
|
||||
public function testUnsupportedEvents($event)
|
||||
{
|
||||
$this->encoderFactory->expects($this->never())->method('getEncoder');
|
||||
|
||||
$this->listener->onLoginSuccess($event);
|
||||
}
|
||||
|
||||
public function provideUnsupportedEvents()
|
||||
{
|
||||
// no password upgrade badge
|
||||
yield [$this->createEvent(new SelfValidatingPassport($this->createMock(UserInterface::class)))];
|
||||
|
||||
// blank password
|
||||
yield [$this->createEvent(new SelfValidatingPassport($this->createMock(UserInterface::class), [new PasswordUpgradeBadge('', $this->createPasswordUpgrader())]))];
|
||||
|
||||
// no user
|
||||
yield [$this->createEvent($this->createMock(PassportInterface::class))];
|
||||
}
|
||||
|
||||
public function testUpgrade()
|
||||
{
|
||||
$encoder = $this->createMock(PasswordEncoderInterface::class);
|
||||
$encoder->expects($this->any())->method('needsRehash')->willReturn(true);
|
||||
$encoder->expects($this->any())->method('encodePassword')->with('pa$$word', null)->willReturn('new-encoded-password');
|
||||
|
||||
$this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->user)->willReturn($encoder);
|
||||
|
||||
$this->user->expects($this->any())->method('getPassword')->willReturn('old-encoded-password');
|
||||
|
||||
$passwordUpgrader = $this->createPasswordUpgrader();
|
||||
$passwordUpgrader->expects($this->once())
|
||||
->method('upgradePassword')
|
||||
->with($this->user, 'new-encoded-password')
|
||||
;
|
||||
|
||||
$event = $this->createEvent(new SelfValidatingPassport($this->user, [new PasswordUpgradeBadge('pa$$word', $passwordUpgrader)]));
|
||||
$this->listener->onLoginSuccess($event);
|
||||
}
|
||||
|
||||
private function createPasswordUpgrader()
|
||||
{
|
||||
return $this->createMock(PasswordUpgraderInterface::class);
|
||||
}
|
||||
|
||||
private function createEvent(PassportInterface $passport)
|
||||
{
|
||||
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), new Request(), null, 'main');
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\RememberMeListener;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
|
||||
class RememberMeListenerTest extends TestCase
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $listener;
|
||||
private $request;
|
||||
private $response;
|
||||
private $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->rememberMeServices = $this->createMock(RememberMeServicesInterface::class);
|
||||
$this->listener = new RememberMeListener($this->rememberMeServices);
|
||||
$this->request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->getMock();
|
||||
$this->response = $this->createMock(Response::class);
|
||||
$this->token = $this->createMock(TokenInterface::class);
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWithoutSupportingAuthenticator()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->never())->method('loginSuccess');
|
||||
|
||||
$event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, new SelfValidatingPassport(new User('wouter', null)));
|
||||
$this->listener->onSuccessfulLogin($event);
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWithoutSuccessResponse()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->never())->method('loginSuccess');
|
||||
|
||||
$event = $this->createLoginSuccessfulEvent('main_firewall', null);
|
||||
$this->listener->onSuccessfulLogin($event);
|
||||
}
|
||||
|
||||
public function testSuccessfulLogin()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->once())->method('loginSuccess')->with($this->request, $this->response, $this->token);
|
||||
|
||||
$event = $this->createLoginSuccessfulEvent('main_firewall', $this->response);
|
||||
$this->listener->onSuccessfulLogin($event);
|
||||
}
|
||||
|
||||
public function testCredentialsInvalid()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->once())->method('loginFail')->with($this->request, $this->isInstanceOf(AuthenticationException::class));
|
||||
|
||||
$event = $this->createLoginFailureEvent('main_firewall');
|
||||
$this->listener->onFailedLogin($event);
|
||||
}
|
||||
|
||||
private function createLoginSuccessfulEvent($providerKey, $response, PassportInterface $passport = null)
|
||||
{
|
||||
if (null === $passport) {
|
||||
$passport = new SelfValidatingPassport(new User('test', null), [new RememberMeBadge()]);
|
||||
}
|
||||
|
||||
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->token, $this->request, $response, $providerKey);
|
||||
}
|
||||
|
||||
private function createLoginFailureEvent($providerKey)
|
||||
{
|
||||
return new LoginFailureEvent(new AuthenticationException(), $this->createMock(AuthenticatorInterface::class), $this->request, null, $providerKey);
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\SessionStrategyListener;
|
||||
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
|
||||
|
||||
class SessionStrategyListenerTest extends TestCase
|
||||
{
|
||||
private $sessionAuthenticationStrategy;
|
||||
private $listener;
|
||||
private $request;
|
||||
private $token;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->sessionAuthenticationStrategy = $this->createMock(SessionAuthenticationStrategyInterface::class);
|
||||
$this->listener = new SessionStrategyListener($this->sessionAuthenticationStrategy);
|
||||
$this->request = new Request();
|
||||
$this->token = $this->createMock(TokenInterface::class);
|
||||
}
|
||||
|
||||
public function testRequestWithSession()
|
||||
{
|
||||
$this->configurePreviousSession();
|
||||
|
||||
$this->sessionAuthenticationStrategy->expects($this->once())->method('onAuthentication')->with($this->request, $this->token);
|
||||
|
||||
$this->listener->onSuccessfulLogin($this->createEvent('main_firewall'));
|
||||
}
|
||||
|
||||
public function testRequestWithoutPreviousSession()
|
||||
{
|
||||
$this->sessionAuthenticationStrategy->expects($this->never())->method('onAuthentication')->with($this->request, $this->token);
|
||||
|
||||
$this->listener->onSuccessfulLogin($this->createEvent('main_firewall'));
|
||||
}
|
||||
|
||||
public function testStatelessFirewalls()
|
||||
{
|
||||
$this->sessionAuthenticationStrategy->expects($this->never())->method('onAuthentication');
|
||||
|
||||
$listener = new SessionStrategyListener($this->sessionAuthenticationStrategy, ['api_firewall']);
|
||||
$listener->onSuccessfulLogin($this->createEvent('api_firewall'));
|
||||
}
|
||||
|
||||
private function createEvent($providerKey)
|
||||
{
|
||||
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), new SelfValidatingPassport(new User('test', null)), $this->token, $this->request, null, $providerKey);
|
||||
}
|
||||
|
||||
private function configurePreviousSession()
|
||||
{
|
||||
$session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')->getMock();
|
||||
$session->expects($this->any())
|
||||
->method('getName')
|
||||
->willReturn('test_session_name');
|
||||
$this->request->setSession($session);
|
||||
$this->request->cookies->set('test_session_name', 'session_cookie_val');
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PreAuthenticatedUserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\UserCheckerListener;
|
||||
|
||||
class UserCheckerListenerTest extends TestCase
|
||||
{
|
||||
private $userChecker;
|
||||
private $listener;
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->userChecker = $this->createMock(UserCheckerInterface::class);
|
||||
$this->listener = new UserCheckerListener($this->userChecker);
|
||||
$this->user = new User('test', null);
|
||||
}
|
||||
|
||||
public function testPreAuth()
|
||||
{
|
||||
$this->userChecker->expects($this->once())->method('checkPreAuth')->with($this->user);
|
||||
|
||||
$this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent());
|
||||
}
|
||||
|
||||
public function testPreAuthNoUser()
|
||||
{
|
||||
$this->userChecker->expects($this->never())->method('checkPreAuth');
|
||||
|
||||
$this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent($this->createMock(PassportInterface::class)));
|
||||
}
|
||||
|
||||
public function testPreAuthenticatedBadge()
|
||||
{
|
||||
$this->userChecker->expects($this->never())->method('checkPreAuth');
|
||||
|
||||
$this->listener->preCredentialsVerification($this->createVerifyAuthenticatorCredentialsEvent(new SelfValidatingPassport($this->user, [new PreAuthenticatedUserBadge()])));
|
||||
}
|
||||
|
||||
public function testPostAuthValidCredentials()
|
||||
{
|
||||
$this->userChecker->expects($this->once())->method('checkPostAuth')->with($this->user);
|
||||
|
||||
$this->listener->postCredentialsVerification($this->createLoginSuccessEvent());
|
||||
}
|
||||
|
||||
public function testPostAuthNoUser()
|
||||
{
|
||||
$this->userChecker->expects($this->never())->method('checkPostAuth');
|
||||
|
||||
$this->listener->postCredentialsVerification($this->createLoginSuccessEvent($this->createMock(PassportInterface::class)));
|
||||
}
|
||||
|
||||
private function createVerifyAuthenticatorCredentialsEvent($passport = null)
|
||||
{
|
||||
if (null === $passport) {
|
||||
$passport = new SelfValidatingPassport($this->user);
|
||||
}
|
||||
|
||||
return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport);
|
||||
}
|
||||
|
||||
private function createLoginSuccessEvent($passport = null)
|
||||
{
|
||||
if (null === $passport) {
|
||||
$passport = new SelfValidatingPassport($this->user);
|
||||
}
|
||||
|
||||
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), new Request(), null, 'main');
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\EventListener;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
|
||||
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\CustomCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\VerifyAuthenticatorCredentialsListener;
|
||||
|
||||
class VerifyAuthenticatorCredentialsListenerTest extends TestCase
|
||||
{
|
||||
private $encoderFactory;
|
||||
private $listener;
|
||||
private $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->encoderFactory = $this->createMock(EncoderFactoryInterface::class);
|
||||
$this->listener = new VerifyAuthenticatorCredentialsListener($this->encoderFactory);
|
||||
$this->user = new User('wouter', 'encoded-password');
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider providePasswords
|
||||
*/
|
||||
public function testPasswordAuthenticated($password, $passwordValid, $result)
|
||||
{
|
||||
$encoder = $this->createMock(PasswordEncoderInterface::class);
|
||||
$encoder->expects($this->any())->method('isPasswordValid')->with('encoded-password', $password)->willReturn($passwordValid);
|
||||
|
||||
$this->encoderFactory->expects($this->any())->method('getEncoder')->with($this->identicalTo($this->user))->willReturn($encoder);
|
||||
|
||||
if (false === $result) {
|
||||
$this->expectException(BadCredentialsException::class);
|
||||
$this->expectExceptionMessage('The presented password is invalid.');
|
||||
}
|
||||
|
||||
$credentials = new PasswordCredentials($password);
|
||||
$this->listener->onAuthenticating($this->createEvent(new Passport($this->user, $credentials)));
|
||||
|
||||
if (true === $result) {
|
||||
$this->assertTrue($credentials->isResolved());
|
||||
}
|
||||
}
|
||||
|
||||
public function providePasswords()
|
||||
{
|
||||
yield ['ThePa$$word', true, true];
|
||||
yield ['Invalid', false, false];
|
||||
}
|
||||
|
||||
public function testEmptyPassword()
|
||||
{
|
||||
$this->expectException(BadCredentialsException::class);
|
||||
$this->expectExceptionMessage('The presented password cannot be empty.');
|
||||
|
||||
$this->encoderFactory->expects($this->never())->method('getEncoder');
|
||||
|
||||
$event = $this->createEvent(new Passport($this->user, new PasswordCredentials('')));
|
||||
$this->listener->onAuthenticating($event);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideCustomAuthenticatedResults
|
||||
*/
|
||||
public function testCustomAuthenticated($result)
|
||||
{
|
||||
$this->encoderFactory->expects($this->never())->method('getEncoder');
|
||||
|
||||
if (false === $result) {
|
||||
$this->expectException(BadCredentialsException::class);
|
||||
}
|
||||
|
||||
$credentials = new CustomCredentials(function () use ($result) {
|
||||
return $result;
|
||||
}, ['password' => 'foo']);
|
||||
$this->listener->onAuthenticating($this->createEvent(new Passport($this->user, $credentials)));
|
||||
|
||||
if (true === $result) {
|
||||
$this->assertTrue($credentials->isResolved());
|
||||
}
|
||||
}
|
||||
|
||||
public function provideCustomAuthenticatedResults()
|
||||
{
|
||||
yield [true];
|
||||
yield [false];
|
||||
}
|
||||
|
||||
public function testNoCredentialsBadgeProvided()
|
||||
{
|
||||
$this->encoderFactory->expects($this->never())->method('getEncoder');
|
||||
|
||||
$event = $this->createEvent(new SelfValidatingPassport($this->user));
|
||||
$this->listener->onAuthenticating($event);
|
||||
}
|
||||
|
||||
private function createEvent($passport)
|
||||
{
|
||||
return new VerifyAuthenticatorCredentialsEvent($this->createMock(AuthenticatorInterface::class), $passport);
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@
|
||||
"require": {
|
||||
"php": "^7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1",
|
||||
"symfony/security-core": "^4.4.8|^5.0.8",
|
||||
"symfony/security-core": "^5.1",
|
||||
"symfony/http-foundation": "^4.4.7|^5.0.7",
|
||||
"symfony/http-kernel": "^4.4|^5.0",
|
||||
"symfony/polyfill-php80": "^1.15",
|
||||
|
Reference in New Issue
Block a user