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 naming
50224aa285 Introduce Passport & Badges to extend authenticators
9ea32c4ed3 Also use authentication failure/success handlers in FormLoginAuthenticator
0fe5083a3e Added JSON login authenticator
7ef6a7ab03 Use the firewall event dispatcher
95edc806a1 Added pre-authenticated authenticators (X.509 & REMOTE_USER)
f5e11e5f32 Reverted changes to the Guard component
ba3754a80f Differentiate between interactive and non-interactive authenticators
6b9d78d5e0 Added tests
59f49b20ca Rename AuthenticatingListener
60d396f2d1 Added automatically CSRF protected authenticators
bf1a452e94 Merge AuthenticatorManager and AuthenticatorHandler
44cc76fec2 Use one AuthenticatorManager per firewall
09bed16d3d Only load old manager if new system is disabled
ddf430fc1e Added remember me functionality
1c810d5d2a Added support for lazy firewalls
7859977324 Removed all mentions of 'guard' in the new system
999ec2795f Refactor to an event based authentication approach
b14a5e8c52 Moved new authenticator to the HTTP namespace
b923e4c4f6 Enabled remember me for the GuardManagerListener
873b949cf9 Mark new core authenticators as experimental
4c06236933 Fixes after testing in Demo application
fa4b3ec213 Implemented password migration for the new authenticators
5efa892395 Create a new core AuthenticatorInterface
50132587a1 Add provider key in PreAuthenticationGuardToken
526f75608b Added GuardManagerListener
a172bacaa6 Added FormLogin and Anonymous authenticators
9b7fddd10c Integrated GuardAuthenticationManager in the SecurityBundle
a6890dbcf0 Created HttpBasicAuthenticator and some Guard traits
c321f4d73a Created GuardAuthenticationManager to make Guard first-class Security
This commit is contained in:
Fabien Potencier 2020-04-21 14:44:22 +02:00
commit 1abdcbb205
77 changed files with 4858 additions and 83 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = [];

View File

@ -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',
];
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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());

View File

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

View File

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

View File

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

View File

@ -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)
{
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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',
];
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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']));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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