[Security] Rework the remember me system
This commit is contained in:
parent
0f96ac7484
commit
15670419d4
@ -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\Bridge\Doctrine\SchemaListener;
|
||||
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
|
||||
use Doctrine\ORM\Tools\ToolEvents;
|
||||
use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider;
|
||||
use Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
|
||||
/**
|
||||
* Automatically adds the rememberme table needed for the {@see DoctrineTokenProvider}.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
final class RememberMeTokenProviderDoctrineSchemaSubscriber implements EventSubscriber
|
||||
{
|
||||
private $rememberMeHandlers;
|
||||
|
||||
/**
|
||||
* @param iterable|RememberMeHandlerInterface[] $rememberMeHandlers
|
||||
*/
|
||||
public function __construct(iterable $rememberMeHandlers)
|
||||
{
|
||||
$this->rememberMeHandlers = $rememberMeHandlers;
|
||||
}
|
||||
|
||||
public function postGenerateSchema(GenerateSchemaEventArgs $event): void
|
||||
{
|
||||
$dbalConnection = $event->getEntityManager()->getConnection();
|
||||
|
||||
foreach ($this->rememberMeHandlers as $rememberMeHandler) {
|
||||
if (
|
||||
$rememberMeHandler instanceof PersistentRememberMeHandler
|
||||
&& ($tokenProvider = $rememberMeHandler->getTokenProvider()) instanceof DoctrineTokenProvider
|
||||
) {
|
||||
$tokenProvider->configureSchema($event->getSchema(), $dbalConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getSubscribedEvents(): array
|
||||
{
|
||||
if (!class_exists(ToolEvents::class)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
ToolEvents::postGenerateSchema,
|
||||
];
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ namespace Symfony\Bridge\Doctrine\Security\RememberMe;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver\Result as DriverResult;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface;
|
||||
@ -21,7 +22,7 @@ use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInter
|
||||
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
|
||||
|
||||
/**
|
||||
* This class provides storage for the tokens that is set in "remember me"
|
||||
* This class provides storage for the tokens that is set in "remember-me"
|
||||
* cookies. This way no password secrets will be stored in the cookies on
|
||||
* the client machine, and thus the security is improved.
|
||||
*
|
||||
@ -53,8 +54,7 @@ class DoctrineTokenProvider implements TokenProviderInterface
|
||||
public function loadTokenBySeries(string $series)
|
||||
{
|
||||
// the alias for lastUsed works around case insensitivity in PostgreSQL
|
||||
$sql = 'SELECT class, username, value, lastUsed AS last_used'
|
||||
.' FROM rememberme_token WHERE series=:series';
|
||||
$sql = 'SELECT class, username, value, lastUsed AS last_used FROM rememberme_token WHERE series=:series';
|
||||
$paramValues = ['series' => $series];
|
||||
$paramTypes = ['series' => \PDO::PARAM_STR];
|
||||
$stmt = $this->conn->executeQuery($sql, $paramValues, $paramTypes);
|
||||
@ -87,8 +87,7 @@ class DoctrineTokenProvider implements TokenProviderInterface
|
||||
*/
|
||||
public function updateToken(string $series, string $tokenValue, \DateTime $lastUsed)
|
||||
{
|
||||
$sql = 'UPDATE rememberme_token SET value=:value, lastUsed=:lastUsed'
|
||||
.' WHERE series=:series';
|
||||
$sql = 'UPDATE rememberme_token SET value=:value, lastUsed=:lastUsed WHERE series=:series';
|
||||
$paramValues = [
|
||||
'value' => $tokenValue,
|
||||
'lastUsed' => $lastUsed,
|
||||
@ -114,9 +113,7 @@ class DoctrineTokenProvider implements TokenProviderInterface
|
||||
*/
|
||||
public function createNewToken(PersistentTokenInterface $token)
|
||||
{
|
||||
$sql = 'INSERT INTO rememberme_token'
|
||||
.' (class, username, series, value, lastUsed)'
|
||||
.' VALUES (:class, :username, :series, :value, :lastUsed)';
|
||||
$sql = 'INSERT INTO rememberme_token (class, username, series, value, lastUsed) VALUES (:class, :username, :series, :value, :lastUsed)';
|
||||
$paramValues = [
|
||||
'class' => $token->getClass(),
|
||||
// @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0
|
||||
@ -138,4 +135,32 @@ class DoctrineTokenProvider implements TokenProviderInterface
|
||||
$this->conn->executeUpdate($sql, $paramValues, $paramTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the Table to the Schema if "remember me" uses this Connection.
|
||||
*/
|
||||
public function configureSchema(Schema $schema, Connection $forConnection): void
|
||||
{
|
||||
// only update the schema for this connection
|
||||
if ($forConnection !== $this->conn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($schema->hasTable('rememberme_token')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addTableToSchema($schema);
|
||||
}
|
||||
|
||||
private function addTableToSchema(Schema $schema): void
|
||||
{
|
||||
$table = $schema->createTable('rememberme_token');
|
||||
$table->addColumn('series', Types::STRING, ['length' => 88]);
|
||||
$table->addColumn('value', Types::STRING, ['length' => 88]);
|
||||
$table->addColumn('lastUsed', Types::DATETIME_MUTABLE);
|
||||
$table->addColumn('class', Types::STRING, ['length' => 100]);
|
||||
$table->addColumn('username', Types::STRING, ['length' => 200]);
|
||||
$table->setPrimaryKey(['series']);
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,7 @@ class UnusedTagsPass implements CompilerPassInterface
|
||||
'security.authenticator.login_linker',
|
||||
'security.expression_language_provider',
|
||||
'security.remember_me_aware',
|
||||
'security.remember_me_handler',
|
||||
'security.voter',
|
||||
'serializer.encoder',
|
||||
'serializer.normalizer',
|
||||
|
@ -21,6 +21,7 @@ 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\LogoutEvent;
|
||||
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
|
||||
use Symfony\Component\Security\Http\SecurityEvents;
|
||||
|
||||
/**
|
||||
@ -44,6 +45,7 @@ class RegisterGlobalSecurityEventListenersPass implements CompilerPassInterface
|
||||
AuthenticationTokenCreatedEvent::class,
|
||||
AuthenticationSuccessEvent::class,
|
||||
InteractiveLoginEvent::class,
|
||||
TokenDeauthenticatedEvent::class,
|
||||
|
||||
// When events are registered by their name
|
||||
AuthenticationEvents::AUTHENTICATION_SUCCESS,
|
||||
|
@ -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\Bundle\SecurityBundle\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\RememberMe\DecoratedRememberMeHandler;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
/**
|
||||
* Replaces the DecoratedRememberMeHandler services with the real definition.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ReplaceDecoratedRememberMeHandlerPass implements CompilerPassInterface
|
||||
{
|
||||
private const HANDLER_TAG = 'security.remember_me_handler';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
$handledFirewalls = [];
|
||||
foreach ($container->findTaggedServiceIds(self::HANDLER_TAG) as $definitionId => $rememberMeHandlerTags) {
|
||||
$definition = $container->findDefinition($definitionId);
|
||||
if (DecoratedRememberMeHandler::class !== $definition->getClass()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get the actual custom remember me handler definition (passed to the decorator)
|
||||
$realRememberMeHandler = $container->findDefinition((string) $definition->getArgument(0));
|
||||
if (null === $realRememberMeHandler) {
|
||||
throw new \LogicException(sprintf('Invalid service definition for custom remember me handler; no service found with ID "%s".', (string) $definition->getArgument(0)));
|
||||
}
|
||||
|
||||
foreach ($rememberMeHandlerTags as $rememberMeHandlerTag) {
|
||||
// some custom handlers may be used on multiple firewalls in the same application
|
||||
if (\in_array($rememberMeHandlerTag['firewall'], $handledFirewalls, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rememberMeHandler = clone $realRememberMeHandler;
|
||||
$rememberMeHandler->addTag(self::HANDLER_TAG, $rememberMeHandlerTag);
|
||||
$container->setDefinition('security.authenticator.remember_me_handler.'.$rememberMeHandlerTag['firewall'], $rememberMeHandler);
|
||||
|
||||
$handledFirewalls[] = $rememberMeHandlerTag['firewall'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -113,18 +113,24 @@ class LoginLinkFactory extends AbstractFactory implements AuthenticatorFactoryIn
|
||||
->replaceArgument(1, $config['lifetime']);
|
||||
}
|
||||
|
||||
$signatureHasherId = 'security.authenticator.login_link_signature_hasher.'.$firewallName;
|
||||
$container
|
||||
->setDefinition($signatureHasherId, new ChildDefinition('security.authenticator.abstract_login_link_signature_hasher'))
|
||||
->replaceArgument(1, $config['signature_properties'])
|
||||
->replaceArgument(3, $expiredStorageId ? new Reference($expiredStorageId) : null)
|
||||
->replaceArgument(4, $config['max_uses'] ?? null)
|
||||
;
|
||||
|
||||
$linkerId = 'security.authenticator.login_link_handler.'.$firewallName;
|
||||
$linkerOptions = [
|
||||
'route_name' => $config['check_route'],
|
||||
'lifetime' => $config['lifetime'],
|
||||
'max_uses' => $config['max_uses'] ?? null,
|
||||
];
|
||||
$container
|
||||
->setDefinition($linkerId, new ChildDefinition('security.authenticator.abstract_login_link_handler'))
|
||||
->replaceArgument(1, new Reference($userProviderId))
|
||||
->replaceArgument(3, $config['signature_properties'])
|
||||
->replaceArgument(5, $linkerOptions)
|
||||
->replaceArgument(6, $expiredStorageId ? new Reference($expiredStorageId) : null)
|
||||
->replaceArgument(2, new Reference($signatureHasherId))
|
||||
->replaceArgument(3, $linkerOptions)
|
||||
->addTag('security.authenticator.login_linker', ['firewall' => $firewallName])
|
||||
;
|
||||
|
||||
|
@ -11,11 +11,16 @@
|
||||
|
||||
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
|
||||
|
||||
use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider;
|
||||
use Symfony\Bundle\SecurityBundle\RememberMe\DecoratedRememberMeHandler;
|
||||
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
|
||||
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;
|
||||
@ -94,31 +99,66 @@ class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactor
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
|
||||
{
|
||||
$templateId = $this->generateRememberMeServicesTemplateId($config, $firewallName);
|
||||
$rememberMeServicesId = $templateId.'.'.$firewallName;
|
||||
if (!$container->hasDefinition('security.authenticator.remember_me')) {
|
||||
$loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../../Resources/config'));
|
||||
$loader->load('security_authenticator_remember_me.php');
|
||||
}
|
||||
|
||||
// create remember me services (which manage the remember me cookies)
|
||||
$this->createRememberMeServices($container, $firewallName, $templateId, [new Reference($userProviderId)], $config);
|
||||
// create remember me handler (which manage the remember-me cookies)
|
||||
$rememberMeHandlerId = 'security.authenticator.remember_me_handler.'.$firewallName;
|
||||
if (isset($config['service']) && isset($config['token_provider'])) {
|
||||
throw new InvalidConfigurationException(sprintf('You cannot use both "service" and "token_provider" in "security.firewalls.%s.remember_me".', $firewallName));
|
||||
}
|
||||
|
||||
if (isset($config['service'])) {
|
||||
$container->register($rememberMeHandlerId, DecoratedRememberMeHandler::class)
|
||||
->addArgument(new Reference($config['service']))
|
||||
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
|
||||
} elseif (isset($config['token_provider'])) {
|
||||
$tokenProviderId = $this->createTokenProvider($container, $firewallName, $config['token_provider']);
|
||||
$container->setDefinition($rememberMeHandlerId, new ChildDefinition('security.authenticator.persistent_remember_me_handler'))
|
||||
->replaceArgument(0, new Reference($tokenProviderId))
|
||||
->replaceArgument(2, new Reference($userProviderId))
|
||||
->replaceArgument(4, $config)
|
||||
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
|
||||
} else {
|
||||
$signatureHasherId = 'security.authenticator.remember_me_signature_hasher.'.$firewallName;
|
||||
$container->setDefinition($signatureHasherId, new ChildDefinition('security.authenticator.remember_me_signature_hasher'))
|
||||
->replaceArgument(1, $config['signature_properties'])
|
||||
;
|
||||
|
||||
$container->setDefinition($rememberMeHandlerId, new ChildDefinition('security.authenticator.signature_remember_me_handler'))
|
||||
->replaceArgument(0, new Reference($signatureHasherId))
|
||||
->replaceArgument(1, new Reference($userProviderId))
|
||||
->replaceArgument(3, $config)
|
||||
->addTag('security.remember_me_handler', ['firewall' => $firewallName]);
|
||||
}
|
||||
|
||||
// create check remember me conditions listener (which checks if a remember-me cookie is supported and requested)
|
||||
$rememberMeConditionsListenerId = 'security.listener.check_remember_me_conditions.'.$firewallName;
|
||||
$container->setDefinition($rememberMeConditionsListenerId, new ChildDefinition('security.listener.check_remember_me_conditions'))
|
||||
->replaceArgument(0, array_intersect_key($config, ['always_remember_me' => true, 'remember_me_parameter' => true]))
|
||||
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName])
|
||||
;
|
||||
|
||||
// create remember me listener (which executes the remember me services for other authenticators and logout)
|
||||
$this->createRememberMeListener($container, $firewallName, $rememberMeServicesId);
|
||||
$rememberMeListenerId = 'security.listener.remember_me.'.$firewallName;
|
||||
$container->setDefinition($rememberMeListenerId, new ChildDefinition('security.listener.remember_me'))
|
||||
->replaceArgument(0, new Reference($rememberMeHandlerId))
|
||||
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName])
|
||||
;
|
||||
|
||||
// create remember me authenticator (which re-authenticates the user based on the remember me cookie)
|
||||
// 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, $container->getDefinition($rememberMeServicesId)->getArgument(3))
|
||||
->replaceArgument(0, new Reference($rememberMeHandlerId))
|
||||
->replaceArgument(3, $config['name'] ?? $this->options['name'])
|
||||
;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -148,7 +188,6 @@ class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactor
|
||||
$builder
|
||||
->scalarNode('secret')->isRequired()->cannotBeEmpty()->end()
|
||||
->scalarNode('service')->end()
|
||||
->scalarNode('token_provider')->end()
|
||||
->arrayNode('user_providers')
|
||||
->beforeNormalization()
|
||||
->ifString()->then(function ($v) { return [$v]; })
|
||||
@ -156,7 +195,26 @@ class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactor
|
||||
->prototype('scalar')->end()
|
||||
->end()
|
||||
->booleanNode('catch_exceptions')->defaultTrue()->end()
|
||||
;
|
||||
->arrayNode('signature_properties')
|
||||
->prototype('scalar')->end()
|
||||
->requiresAtLeastOneElement()
|
||||
->info('An array of properties on your User that are used to sign the remember-me cookie. If any of these change, all existing cookies will become invalid.')
|
||||
->example(['email', 'password'])
|
||||
->end()
|
||||
->arrayNode('token_provider')
|
||||
->beforeNormalization()
|
||||
->ifString()->then(function ($v) { return ['service' => $v]; })
|
||||
->end()
|
||||
->children()
|
||||
->scalarNode('service')->info('The service ID of a custom rememberme token provider.')->end()
|
||||
->arrayNode('doctrine')
|
||||
->canBeEnabled()
|
||||
->children()
|
||||
->scalarNode('connection')->defaultNull()->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end();
|
||||
|
||||
foreach ($this->options as $name => $value) {
|
||||
if ('secure' === $name) {
|
||||
@ -195,9 +253,8 @@ class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactor
|
||||
$rememberMeServices->replaceArgument(2, $id);
|
||||
|
||||
if (isset($config['token_provider'])) {
|
||||
$rememberMeServices->addMethodCall('setTokenProvider', [
|
||||
new Reference($config['token_provider']),
|
||||
]);
|
||||
$tokenProviderId = $this->createTokenProvider($container, $id, $config['token_provider']);
|
||||
$rememberMeServices->addMethodCall('setTokenProvider', [new Reference($tokenProviderId)]);
|
||||
}
|
||||
|
||||
// remember-me options
|
||||
@ -222,17 +279,29 @@ class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactor
|
||||
$rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders)));
|
||||
}
|
||||
|
||||
private function createRememberMeListener(ContainerBuilder $container, string $id, string $rememberMeServicesId): void
|
||||
private function createTokenProvider(ContainerBuilder $container, string $firewallName, array $config): string
|
||||
{
|
||||
$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))
|
||||
;
|
||||
$tokenProviderId = $config['service'] ?? false;
|
||||
if ($config['doctrine']['enabled'] ?? false) {
|
||||
if (!class_exists(DoctrineTokenProvider::class)) {
|
||||
throw new InvalidConfigurationException('Cannot use the "doctrine" token provider for "remember_me" because the Doctrine Bridge is not installed. Try running "composer require symfony/doctrine-bridge".');
|
||||
}
|
||||
|
||||
$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));
|
||||
if (null === $config['doctrine']['connection']) {
|
||||
$connectionId = 'database_connection';
|
||||
} else {
|
||||
$connectionId = 'doctrine.dbal.'.$config['doctrine']['connection'].'_connection';
|
||||
}
|
||||
|
||||
$tokenProviderId = 'security.remember_me.doctrine_token_provider.'.$firewallName;
|
||||
$container->register($tokenProviderId, DoctrineTokenProvider::class)
|
||||
->addArgument(new Reference($connectionId));
|
||||
}
|
||||
|
||||
if (!$tokenProviderId) {
|
||||
throw new InvalidConfigurationException(sprintf('No token provider was set for firewall "%s". Either configure a service ID or set "remember_me.token_provider.doctrine" to true.', $firewallName));
|
||||
}
|
||||
|
||||
return $tokenProviderId;
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\Hasher\Pbkdf2PasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\Hasher\PlaintextPasswordHasher;
|
||||
@ -392,7 +393,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
// Context serializer listener
|
||||
if (false === $firewall['stateless']) {
|
||||
$contextKey = $firewall['context'] ?? $id;
|
||||
$listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey));
|
||||
$listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey, $this->authenticatorManagerEnabled ? $firewallEventDispatcherId : null));
|
||||
$sessionStrategyId = 'security.authentication.session_strategy';
|
||||
|
||||
if ($this->authenticatorManagerEnabled) {
|
||||
@ -557,7 +558,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null];
|
||||
}
|
||||
|
||||
private function createContextListener(ContainerBuilder $container, string $contextKey)
|
||||
private function createContextListener(ContainerBuilder $container, string $contextKey, ?string $firewallEventDispatcherId)
|
||||
{
|
||||
if (isset($this->contextListeners[$contextKey])) {
|
||||
return $this->contextListeners[$contextKey];
|
||||
@ -566,6 +567,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
|
||||
$listenerId = 'security.context_listener.'.\count($this->contextListeners);
|
||||
$listener = $container->setDefinition($listenerId, new ChildDefinition('security.context_listener'));
|
||||
$listener->replaceArgument(2, $contextKey);
|
||||
if (null !== $firewallEventDispatcherId) {
|
||||
$listener->replaceArgument(4, new Reference($firewallEventDispatcherId));
|
||||
$listener->addTag('kernel.event_listener', ['event' => KernelEvents::RESPONSE, 'method' => 'onKernelResponse']);
|
||||
}
|
||||
|
||||
return $this->contextListeners[$contextKey] = $listenerId;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Bundle\SecurityBundle\LoginLink;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security\FirewallAwareTrait;
|
||||
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
@ -26,43 +27,24 @@ use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
|
||||
*/
|
||||
class FirewallAwareLoginLinkHandler implements LoginLinkHandlerInterface
|
||||
{
|
||||
private $firewallMap;
|
||||
private $loginLinkHandlerLocator;
|
||||
private $requestStack;
|
||||
use FirewallAwareTrait;
|
||||
|
||||
private const FIREWALL_OPTION = 'login_link';
|
||||
|
||||
public function __construct(FirewallMap $firewallMap, ContainerInterface $loginLinkHandlerLocator, RequestStack $requestStack)
|
||||
{
|
||||
$this->firewallMap = $firewallMap;
|
||||
$this->loginLinkHandlerLocator = $loginLinkHandlerLocator;
|
||||
$this->locator = $loginLinkHandlerLocator;
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
|
||||
public function createLoginLink(UserInterface $user, Request $request = null): LoginLinkDetails
|
||||
{
|
||||
return $this->getLoginLinkHandler()->createLoginLink($user, $request);
|
||||
return $this->getForFirewall()->createLoginLink($user, $request);
|
||||
}
|
||||
|
||||
public function consumeLoginLink(Request $request): UserInterface
|
||||
{
|
||||
return $this->getLoginLinkHandler()->consumeLoginLink($request);
|
||||
}
|
||||
|
||||
private function getLoginLinkHandler(): LoginLinkHandlerInterface
|
||||
{
|
||||
if (null === $request = $this->requestStack->getCurrentRequest()) {
|
||||
throw new \LogicException('Cannot determine the correct LoginLinkHandler to use: there is no active Request and so, the firewall cannot be determined. Try using the specific login link handler service.');
|
||||
}
|
||||
|
||||
$firewall = $this->firewallMap->getFirewallConfig($request);
|
||||
if (!$firewall) {
|
||||
throw new \LogicException('No login link handler found as the current route is not covered by a firewall.');
|
||||
}
|
||||
|
||||
$firewallName = $firewall->getName();
|
||||
if (!$this->loginLinkHandlerLocator->has($firewallName)) {
|
||||
throw new \LogicException(sprintf('No login link handler found. Did you add a login_link key under your "%s" firewall?', $firewallName));
|
||||
}
|
||||
|
||||
return $this->loginLinkHandlerLocator->get($firewallName);
|
||||
return $this->getForFirewall()->consumeLoginLink($request);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
<?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\RememberMe;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeDetails;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
|
||||
/**
|
||||
* Used as a "workaround" for tagging aliases in the RememberMeFactory.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class DecoratedRememberMeHandler implements RememberMeHandlerInterface
|
||||
{
|
||||
private $handler;
|
||||
|
||||
public function __construct(RememberMeHandlerInterface $handler)
|
||||
{
|
||||
$this->handler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function createRememberMeCookie(UserInterface $user): void
|
||||
{
|
||||
$this->handler->createRememberMeCookie($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): UserInterface
|
||||
{
|
||||
return $this->handler->consumeRememberMeCookie($rememberMeDetails);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function clearRememberMeCookie(): void
|
||||
{
|
||||
$this->handler->clearRememberMeCookie();
|
||||
}
|
||||
}
|
@ -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\RememberMe;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security\FirewallAwareTrait;
|
||||
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeDetails;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
|
||||
/**
|
||||
* Decorates {@see RememberMeHandlerInterface} for the current firewall.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
final class FirewallAwareRememberMeHandler implements RememberMeHandlerInterface
|
||||
{
|
||||
use FirewallAwareTrait;
|
||||
|
||||
private const FIREWALL_OPTION = 'remember_me';
|
||||
|
||||
public function __construct(FirewallMap $firewallMap, ContainerInterface $rememberMeHandlerLocator, RequestStack $requestStack)
|
||||
{
|
||||
$this->firewallMap = $firewallMap;
|
||||
$this->locator = $rememberMeHandlerLocator;
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
|
||||
public function createRememberMeCookie(UserInterface $user): void
|
||||
{
|
||||
$this->getForFirewall()->createRememberMeCookie($user);
|
||||
}
|
||||
|
||||
public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): UserInterface
|
||||
{
|
||||
return $this->getForFirewall()->consumeRememberMeCookie($rememberMeDetails);
|
||||
}
|
||||
|
||||
public function clearRememberMeCookie(): void
|
||||
{
|
||||
$this->getForFirewall()->clearRememberMeCookie();
|
||||
}
|
||||
}
|
@ -334,9 +334,12 @@
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="remember_me">
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="user-provider" type="xsd:string" />
|
||||
</xsd:choice>
|
||||
<xsd:sequence minOccurs="0">
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="user-provider" type="xsd:string" />
|
||||
</xsd:choice>
|
||||
<xsd:element name="token-provider" type="remember_me_token_provider" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
<xsd:attribute name="lifetime" type="xsd:integer" />
|
||||
<xsd:attribute name="path" type="xsd:string" />
|
||||
@ -352,6 +355,18 @@
|
||||
<xsd:attribute name="samesite" type="remember_me_samesite" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="remember_me_token_provider">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="doctrine" type="remember_me_token_provider_doctrine" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="service" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:complexType name="remember_me_token_provider_doctrine">
|
||||
<xsd:attribute name="enabled" type="xsd:boolean" />
|
||||
<xsd:attribute name="connection" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
|
||||
<xsd:simpleType name="remember_me_secure">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="true" />
|
||||
|
@ -20,14 +20,12 @@ use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\HttpBasicAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\RemoteUserAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\X509Authenticator;
|
||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||
use Symfony\Component\Security\Http\EventListener\CheckCredentialsListener;
|
||||
use Symfony\Component\Security\Http\EventListener\LoginThrottlingListener;
|
||||
use Symfony\Component\Security\Http\EventListener\PasswordMigratingListener;
|
||||
use Symfony\Component\Security\Http\EventListener\RememberMeListener;
|
||||
use Symfony\Component\Security\Http\EventListener\SessionStrategyListener;
|
||||
use Symfony\Component\Security\Http\EventListener\UserCheckerListener;
|
||||
use Symfony\Component\Security\Http\EventListener\UserProviderListener;
|
||||
@ -107,14 +105,6 @@ return static function (ContainerConfigurator $container) {
|
||||
service('security.authentication.session_strategy'),
|
||||
])
|
||||
|
||||
->set('security.listener.remember_me', RememberMeListener::class)
|
||||
->abstract()
|
||||
->args([
|
||||
abstract_arg('remember me services'),
|
||||
service('logger')->nullOnInvalid(),
|
||||
])
|
||||
->tag('monolog.logger', ['channel' => 'security'])
|
||||
|
||||
->set('security.listener.login_throttling', LoginThrottlingListener::class)
|
||||
->abstract()
|
||||
->args([
|
||||
@ -154,16 +144,6 @@ return static function (ContainerConfigurator $container) {
|
||||
])
|
||||
->call('setTranslator', [service('translator')->ignoreOnInvalid()])
|
||||
|
||||
->set('security.authenticator.remember_me', RememberMeAuthenticator::class)
|
||||
->abstract()
|
||||
->args([
|
||||
abstract_arg('remember me services'),
|
||||
param('kernel.secret'),
|
||||
service('security.token_storage'),
|
||||
abstract_arg('options'),
|
||||
service('security.authentication.session_strategy'),
|
||||
])
|
||||
|
||||
->set('security.authenticator.x509', X509Authenticator::class)
|
||||
->abstract()
|
||||
->args([
|
||||
|
@ -12,8 +12,9 @@
|
||||
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\LoginLink\FirewallAwareLoginLinkHandler;
|
||||
use Symfony\Component\Security\Core\Signature\ExpiredSignatureStorage;
|
||||
use Symfony\Component\Security\Core\Signature\SignatureHasher;
|
||||
use Symfony\Component\Security\Http\Authenticator\LoginLinkAuthenticator;
|
||||
use Symfony\Component\Security\Http\LoginLink\ExpiredLoginLinkStorage;
|
||||
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandler;
|
||||
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;
|
||||
|
||||
@ -34,14 +35,20 @@ return static function (ContainerConfigurator $container) {
|
||||
->args([
|
||||
service('router'),
|
||||
abstract_arg('user provider'),
|
||||
abstract_arg('signature hasher'),
|
||||
abstract_arg('options'),
|
||||
])
|
||||
|
||||
->set('security.authenticator.abstract_login_link_signature_hasher', SignatureHasher::class)
|
||||
->args([
|
||||
service('property_accessor'),
|
||||
abstract_arg('signature properties'),
|
||||
'%kernel.secret%',
|
||||
abstract_arg('options'),
|
||||
abstract_arg('expired login link storage'),
|
||||
abstract_arg('expired signature storage'),
|
||||
abstract_arg('max signature uses'),
|
||||
])
|
||||
|
||||
->set('security.authenticator.expired_login_link_storage', ExpiredLoginLinkStorage::class)
|
||||
->set('security.authenticator.expired_login_link_storage', ExpiredSignatureStorage::class)
|
||||
->abstract()
|
||||
->args([
|
||||
abstract_arg('cache pool service'),
|
||||
|
@ -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\DependencyInjection\Loader\Configurator;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\RememberMe\FirewallAwareRememberMeHandler;
|
||||
use Symfony\Component\Security\Core\Signature\SignatureHasher;
|
||||
use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator;
|
||||
use Symfony\Component\Security\Http\EventListener\CheckRememberMeConditionsListener;
|
||||
use Symfony\Component\Security\Http\EventListener\RememberMeListener;
|
||||
use Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
use Symfony\Component\Security\Http\RememberMe\SignatureRememberMeHandler;
|
||||
|
||||
return static function (ContainerConfigurator $container) {
|
||||
$container->services()
|
||||
->set('security.authenticator.remember_me_signature_hasher', SignatureHasher::class)
|
||||
->args([
|
||||
service('property_accessor'),
|
||||
abstract_arg('signature properties'),
|
||||
'%kernel.secret%',
|
||||
null,
|
||||
null,
|
||||
])
|
||||
|
||||
->set('security.authenticator.signature_remember_me_handler', SignatureRememberMeHandler::class)
|
||||
->abstract()
|
||||
->args([
|
||||
abstract_arg('signature hasher'),
|
||||
abstract_arg('user provider'),
|
||||
service('request_stack'),
|
||||
abstract_arg('options'),
|
||||
service('logger')->nullOnInvalid(),
|
||||
])
|
||||
->tag('monolog.logger', ['channel' => 'security'])
|
||||
|
||||
->set('security.authenticator.persistent_remember_me_handler', PersistentRememberMeHandler::class)
|
||||
->abstract()
|
||||
->args([
|
||||
abstract_arg('token provider'),
|
||||
param('kernel.secret'),
|
||||
abstract_arg('user provider'),
|
||||
service('request_stack'),
|
||||
abstract_arg('options'),
|
||||
service('logger')->nullOnInvalid(),
|
||||
])
|
||||
->tag('monolog.logger', ['channel' => 'security'])
|
||||
|
||||
->set('security.authenticator.firewall_aware_remember_me_handler', FirewallAwareRememberMeHandler::class)
|
||||
->args([
|
||||
service('security.firewall.map'),
|
||||
tagged_locator('security.remember_me_handler', 'firewall'),
|
||||
service('request_stack'),
|
||||
])
|
||||
->alias(RememberMeHandlerInterface::class, 'security.authenticator.firewall_aware_remember_me_handler')
|
||||
|
||||
->set('security.listener.check_remember_me_conditions', CheckRememberMeConditionsListener::class)
|
||||
->abstract()
|
||||
->args([
|
||||
abstract_arg('options'),
|
||||
service('logger')->nullOnInvalid(),
|
||||
])
|
||||
|
||||
->set('security.listener.remember_me', RememberMeListener::class)
|
||||
->abstract()
|
||||
->args([
|
||||
abstract_arg('remember me handler'),
|
||||
service('logger')->nullOnInvalid(),
|
||||
])
|
||||
->tag('monolog.logger', ['channel' => 'security'])
|
||||
|
||||
->set('security.authenticator.remember_me', RememberMeAuthenticator::class)
|
||||
->abstract()
|
||||
->args([
|
||||
abstract_arg('remember me handler'),
|
||||
param('kernel.secret'),
|
||||
service('security.token_storage'),
|
||||
abstract_arg('options'),
|
||||
service('logger')->nullOnInvalid(),
|
||||
])
|
||||
->tag('monolog.logger', ['channel' => 'security'])
|
||||
;
|
||||
};
|
@ -0,0 +1,52 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Provides basic functionality for services mapped by the firewall name
|
||||
* in a container locator.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait FirewallAwareTrait
|
||||
{
|
||||
private $locator;
|
||||
private $requestStack;
|
||||
private $firewallMap;
|
||||
|
||||
private function getForFirewall(): object
|
||||
{
|
||||
$serviceIdentifier = str_replace('FirewallAware', '', static::class);
|
||||
if (null === $request = $this->requestStack->getCurrentRequest()) {
|
||||
throw new \LogicException('Cannot determine the correct '.$serviceIdentifier.' to use: there is no active Request and so, the firewall cannot be determined. Try using a specific '.$serviceIdentifier().' service.');
|
||||
}
|
||||
|
||||
$firewall = $this->firewallMap->getFirewallConfig($request);
|
||||
if (!$firewall) {
|
||||
throw new \LogicException('No '.$serviceIdentifier.' found as the current route is not covered by a firewall.');
|
||||
}
|
||||
|
||||
$firewallName = $firewall->getName();
|
||||
if (!$this->locator->has($firewallName)) {
|
||||
$message = 'No '.$serviceIdentifier.' found for this firewall.';
|
||||
if (\defined(static::class.'::FIREWALL_OPTION')) {
|
||||
$message .= sprintf('Did you forget to add a "'.static::FIREWALL_OPTION.'" key under your "%s" firewall?', $firewallName);
|
||||
}
|
||||
|
||||
throw new \LogicException($message);
|
||||
}
|
||||
|
||||
return $this->locator->get($firewallName);
|
||||
}
|
||||
}
|
@ -15,11 +15,9 @@ 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;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
/**
|
||||
* A decorator that delegates all method calls to the authenticator
|
||||
@ -32,14 +30,12 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
*/
|
||||
class UserAuthenticator implements UserAuthenticatorInterface
|
||||
{
|
||||
private $firewallMap;
|
||||
private $userAuthenticators;
|
||||
private $requestStack;
|
||||
use FirewallAwareTrait;
|
||||
|
||||
public function __construct(FirewallMap $firewallMap, ContainerInterface $userAuthenticators, RequestStack $requestStack)
|
||||
{
|
||||
$this->firewallMap = $firewallMap;
|
||||
$this->userAuthenticators = $userAuthenticators;
|
||||
$this->locator = $userAuthenticators;
|
||||
$this->requestStack = $requestStack;
|
||||
}
|
||||
|
||||
@ -48,16 +44,6 @@ class UserAuthenticator implements UserAuthenticatorInterface
|
||||
*/
|
||||
public function authenticateUser(UserInterface $user, AuthenticatorInterface $authenticator, Request $request, array $badges = []): ?Response
|
||||
{
|
||||
return $this->getUserAuthenticator()->authenticateUser($user, $authenticator, $request, $badges);
|
||||
}
|
||||
|
||||
private function getUserAuthenticator(): UserAuthenticatorInterface
|
||||
{
|
||||
$firewallConfig = $this->firewallMap->getFirewallConfig($this->requestStack->getMainRequest());
|
||||
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());
|
||||
return $this->getForFirewall()->authenticateUser($user, $authenticator, $request, $badges);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterEntryPoin
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterGlobalSecurityEventListenersPass;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterLdapLocatorPass;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\ReplaceDecoratedRememberMeHandlerPass;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory;
|
||||
@ -83,6 +84,7 @@ class SecurityBundle extends Bundle
|
||||
$container->addCompilerPass(new RegisterGlobalSecurityEventListenersPass(), PassConfig::TYPE_BEFORE_REMOVING, -200);
|
||||
// execute after ResolveChildDefinitionsPass optimization pass, to ensure class names are set
|
||||
$container->addCompilerPass(new SortFirewallListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);
|
||||
$container->addCompilerPass(new ReplaceDecoratedRememberMeHandlerPass(), PassConfig::TYPE_OPTIMIZE);
|
||||
|
||||
$container->addCompilerPass(new AddEventAliasesPass(array_merge(
|
||||
AuthenticationEvents::ALIASES,
|
||||
|
@ -51,13 +51,15 @@ abstract class CompleteConfigurationTest extends TestCase
|
||||
$this->assertEquals(3600, (string) $expiredStorage->getArgument(1));
|
||||
|
||||
$linker = $container->getDefinition($linkerId = 'security.authenticator.login_link_handler.main');
|
||||
$this->assertEquals(['id', 'email'], $linker->getArgument(3));
|
||||
$this->assertEquals([
|
||||
'route_name' => 'login_check',
|
||||
'lifetime' => 3600,
|
||||
'max_uses' => 1,
|
||||
], $linker->getArgument(5));
|
||||
$this->assertEquals($expiredStorageId, (string) $linker->getArgument(6));
|
||||
], $linker->getArgument(3));
|
||||
|
||||
$hasher = $container->getDefinition((string) $linker->getArgument(2));
|
||||
$this->assertEquals(['id', 'email'], $hasher->getArgument(1));
|
||||
$this->assertEquals($expiredStorageId, (string) $hasher->getArgument(3));
|
||||
$this->assertEquals(1, $hasher->getArgument(4));
|
||||
|
||||
$authenticator = $container->getDefinition('security.authenticator.login_link.main');
|
||||
$this->assertEquals($linkerId, (string) $authenticator->getArgument(0));
|
||||
|
@ -388,6 +388,27 @@ class SecurityExtensionTest extends TestCase
|
||||
$this->assertEquals($secure, $definition->getArgument(3)['secure']);
|
||||
}
|
||||
|
||||
public function testCustomRememberMeHandler()
|
||||
{
|
||||
$container = $this->getRawContainer();
|
||||
|
||||
$container->register('custom_remember_me', \stdClass::class);
|
||||
$container->loadFromExtension('security', [
|
||||
'enable_authenticator_manager' => true,
|
||||
'firewalls' => [
|
||||
'default' => [
|
||||
'remember_me' => ['secret' => 'very', 'service' => 'custom_remember_me'],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$container->compile();
|
||||
|
||||
$handler = $container->getDefinition('security.authenticator.remember_me_handler.default');
|
||||
$this->assertEquals(\stdClass::class, $handler->getClass());
|
||||
$this->assertEquals([['firewall' => 'default']], $handler->getTag('security.remember_me_handler'));
|
||||
}
|
||||
|
||||
public function sessionConfigurationProvider()
|
||||
{
|
||||
return [
|
||||
@ -661,13 +682,13 @@ class SecurityExtensionTest extends TestCase
|
||||
$security = new SecurityExtension();
|
||||
$container->registerExtension($security);
|
||||
|
||||
$bundle = new SecurityBundle();
|
||||
$bundle->build($container);
|
||||
|
||||
$container->getCompilerPassConfig()->setOptimizationPasses([new ResolveChildDefinitionsPass()]);
|
||||
$container->getCompilerPassConfig()->setRemovingPasses([]);
|
||||
$container->getCompilerPassConfig()->setAfterRemovingPasses([]);
|
||||
|
||||
$bundle = new SecurityBundle();
|
||||
$bundle->build($container);
|
||||
|
||||
return $container;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,23 @@
|
||||
<?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\Tests\Functional\Bundle\RememberMeBundle\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class ProfileController
|
||||
{
|
||||
public function __invoke(UserInterface $user)
|
||||
{
|
||||
return new Response($user->getUserIdentifier());
|
||||
}
|
||||
}
|
@ -9,10 +9,10 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle;
|
||||
|
||||
return [
|
||||
new FrameworkBundle(),
|
||||
new SecurityBundle(),
|
||||
];
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class RememberMeBundle extends Bundle
|
||||
{
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
<?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\Tests\Functional\Bundle\RememberMeBundle\Security;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
|
||||
use Symfony\Component\Security\Core\Exception\TokenNotFoundException;
|
||||
|
||||
class StaticTokenProvider implements TokenProviderInterface
|
||||
{
|
||||
private static $db = [];
|
||||
private static $kernelClass;
|
||||
|
||||
public function __construct($kernel)
|
||||
{
|
||||
// only reset the "internal db" for new tests
|
||||
if (self::$kernelClass !== \get_class($kernel)) {
|
||||
self::$kernelClass = \get_class($kernel);
|
||||
self::$db = [];
|
||||
}
|
||||
}
|
||||
|
||||
public function loadTokenBySeries(string $series)
|
||||
{
|
||||
$token = self::$db[$series] ?? false;
|
||||
if (!$token) {
|
||||
throw new TokenNotFoundException();
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function deleteTokenBySeries(string $series)
|
||||
{
|
||||
unset(self::$db[$series]);
|
||||
}
|
||||
|
||||
public function updateToken(string $series, string $tokenValue, \DateTime $lastUsed)
|
||||
{
|
||||
$token = $this->loadTokenBySeries($series);
|
||||
$refl = new \ReflectionClass($token);
|
||||
$tokenValueProp = $refl->getProperty('tokenValue');
|
||||
$tokenValueProp->setAccessible(true);
|
||||
$tokenValueProp->setValue($token, $tokenValue);
|
||||
|
||||
$lastUsedProp = $refl->getProperty('lastUsed');
|
||||
$lastUsedProp->setAccessible(true);
|
||||
$lastUsedProp->setValue($token, $lastUsed);
|
||||
|
||||
self::$db[$series] = $token;
|
||||
}
|
||||
|
||||
public function createNewToken(PersistentTokenInterface $token)
|
||||
{
|
||||
self::$db[$token->getSeries()] = $token;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
<?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\Tests\Functional\Bundle\RememberMeBundle\Security;
|
||||
|
||||
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
class UserChangingUserProvider implements UserProviderInterface
|
||||
{
|
||||
private $inner;
|
||||
|
||||
public function __construct(InMemoryUserProvider $inner)
|
||||
{
|
||||
$this->inner = $inner;
|
||||
}
|
||||
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
return $this->inner->loadUserByUsername($username);
|
||||
}
|
||||
|
||||
public function loadUserByIdentifier(string $userIdentifier): UserInterface
|
||||
{
|
||||
return $this->inner->loadUserByIdentifier($userIdentifier);
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user)
|
||||
{
|
||||
$user = $this->inner->refreshUser($user);
|
||||
|
||||
$alterUser = \Closure::bind(function (User $user) { $user->password = 'foo'; }, null, User::class);
|
||||
$alterUser($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function supportsClass($class)
|
||||
{
|
||||
return $this->inner->supportsClass($class);
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
<?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\Tests\Functional;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
|
||||
use Symfony\Component\Security\Core\User\User;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
class ClearRememberMeTest extends AbstractWebTestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideClientOptions
|
||||
*/
|
||||
public function testUserChangeClearsCookie(array $options)
|
||||
{
|
||||
$client = $this->createClient($options);
|
||||
|
||||
$client->request('POST', '/login', [
|
||||
'_username' => 'johannes',
|
||||
'_password' => 'test',
|
||||
]);
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
$cookieJar = $client->getCookieJar();
|
||||
$this->assertNotNull($cookieJar->get('REMEMBERME'));
|
||||
|
||||
$client->request('GET', '/foo');
|
||||
$this->assertRedirect($client->getResponse(), '/login');
|
||||
$this->assertNull($cookieJar->get('REMEMBERME'));
|
||||
}
|
||||
|
||||
public function provideClientOptions()
|
||||
{
|
||||
yield [['test_case' => 'ClearRememberMe', 'root_config' => 'config.yml', 'enable_authenticator_manager' => true]];
|
||||
yield [['test_case' => 'ClearRememberMe', 'root_config' => 'legacy_config.yml', 'enable_authenticator_manager' => false]];
|
||||
}
|
||||
}
|
||||
|
||||
class RememberMeFooController
|
||||
{
|
||||
public function __invoke(UserInterface $user)
|
||||
{
|
||||
return new Response($user->getUserIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
class RememberMeUserProvider implements UserProviderInterface
|
||||
{
|
||||
private $inner;
|
||||
|
||||
public function __construct(InMemoryUserProvider $inner)
|
||||
{
|
||||
$this->inner = $inner;
|
||||
}
|
||||
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
return $this->loadUserByIdentifier($username);
|
||||
}
|
||||
|
||||
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||
{
|
||||
return $this->inner->loadUserByIdentifier($identifier);
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user)
|
||||
{
|
||||
$user = $this->inner->refreshUser($user);
|
||||
|
||||
$alterUser = \Closure::bind(function (User $user) { $user->password = 'foo'; }, null, User::class);
|
||||
$alterUser($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function supportsClass($class)
|
||||
{
|
||||
return $this->inner->supportsClass($class);
|
||||
}
|
||||
}
|
@ -20,29 +20,6 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
class LogoutTest extends AbstractWebTestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideSecuritySystems
|
||||
*/
|
||||
public function testSessionLessRememberMeLogout(array $options)
|
||||
{
|
||||
$client = $this->createClient($options + ['test_case' => 'RememberMeLogout', 'root_config' => 'config.yml']);
|
||||
|
||||
$client->request('POST', '/login', [
|
||||
'_username' => 'johannes',
|
||||
'_password' => 'test',
|
||||
]);
|
||||
|
||||
$cookieJar = $client->getCookieJar();
|
||||
$cookieJar->expire(session_name());
|
||||
|
||||
$this->assertNotNull($cookieJar->get('REMEMBERME'));
|
||||
$this->assertSame('lax', $cookieJar->get('REMEMBERME')->getSameSite());
|
||||
|
||||
$client->request('GET', '/logout');
|
||||
|
||||
$this->assertNull($cookieJar->get('REMEMBERME'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSecuritySystems
|
||||
*/
|
||||
|
@ -0,0 +1,95 @@
|
||||
<?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\Tests\Functional;
|
||||
|
||||
class RememberMeTest extends AbstractWebTestCase
|
||||
{
|
||||
public function provideRememberMeSystems()
|
||||
{
|
||||
foreach ($this->provideSecuritySystems() as $securitySystem) {
|
||||
yield [$securitySystem[0] + ['root_config' => 'config_session.yml']];
|
||||
yield [$securitySystem[0] + ['root_config' => 'config_persistent.yml']];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideRememberMeSystems
|
||||
*/
|
||||
public function testRememberMe(array $options)
|
||||
{
|
||||
$client = $this->createClient(array_merge_recursive(['root_config' => 'config.yml', 'test_case' => 'RememberMe'], $options));
|
||||
|
||||
$client->request('POST', '/login', [
|
||||
'_username' => 'johannes',
|
||||
'_password' => 'test',
|
||||
]);
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
|
||||
$client->request('GET', '/profile');
|
||||
$this->assertSame('johannes', $client->getResponse()->getContent());
|
||||
|
||||
// clear session, this should trigger remember me on the next request
|
||||
$client->getCookieJar()->expire('MOCKSESSID');
|
||||
|
||||
$client->request('GET', '/profile');
|
||||
$this->assertSame('johannes', $client->getResponse()->getContent(), 'Not logged in after resetting session.');
|
||||
|
||||
// logout, this should clear the remember-me cookie
|
||||
$client->request('GET', '/logout');
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode(), 'Logout unsuccessful.');
|
||||
$this->assertNull($client->getCookieJar()->get('REMEMBERME'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSecuritySystems
|
||||
*/
|
||||
public function testUserChangeClearsCookie(array $options)
|
||||
{
|
||||
$client = $this->createClient(['test_case' => 'RememberMe', 'root_config' => 'clear_on_change_config.yml'] + $options);
|
||||
|
||||
$client->request('POST', '/login', [
|
||||
'_username' => 'johannes',
|
||||
'_password' => 'test',
|
||||
]);
|
||||
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode());
|
||||
$cookieJar = $client->getCookieJar();
|
||||
$this->assertNotNull($cookieJar->get('REMEMBERME'));
|
||||
|
||||
$client->request('GET', '/profile');
|
||||
$this->assertRedirect($client->getResponse(), '/login');
|
||||
$this->assertNull($cookieJar->get('REMEMBERME'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSecuritySystems
|
||||
*/
|
||||
public function testSessionLessRememberMeLogout(array $options)
|
||||
{
|
||||
$client = $this->createClient(['test_case' => 'RememberMe', 'root_config' => 'stateless_config.yml'] + $options);
|
||||
|
||||
$client->request('POST', '/login', [
|
||||
'_username' => 'johannes',
|
||||
'_password' => 'test',
|
||||
]);
|
||||
|
||||
$cookieJar = $client->getCookieJar();
|
||||
$cookieJar->expire(session_name());
|
||||
|
||||
$this->assertNotNull($cookieJar->get('REMEMBERME'));
|
||||
$this->assertSame('lax', $cookieJar->get('REMEMBERME')->getSameSite());
|
||||
|
||||
$client->request('GET', '/logout');
|
||||
$this->assertSame(302, $client->getResponse()->getStatusCode(), 'Logout unsuccessful.');
|
||||
$this->assertNull($cookieJar->get('REMEMBERME'));
|
||||
}
|
||||
}
|
@ -36,10 +36,13 @@ class AppKernel extends Kernel
|
||||
$this->testCase = $testCase;
|
||||
|
||||
$fs = new Filesystem();
|
||||
if (!$fs->isAbsolutePath($rootConfig) && !is_file($rootConfig = __DIR__.'/'.$testCase.'/'.$rootConfig)) {
|
||||
throw new \InvalidArgumentException(sprintf('The root config "%s" does not exist.', $rootConfig));
|
||||
foreach ((array) $rootConfig as $config) {
|
||||
if (!$fs->isAbsolutePath($config) && !is_file($config = __DIR__.'/'.$testCase.'/'.$config)) {
|
||||
throw new \InvalidArgumentException(sprintf('The root config "%s" does not exist.', $config));
|
||||
}
|
||||
|
||||
$this->rootConfig[] = $config;
|
||||
}
|
||||
$this->rootConfig = $rootConfig;
|
||||
$this->authenticatorManagerEnabled = $authenticatorManagerEnabled;
|
||||
|
||||
parent::__construct($environment, $debug);
|
||||
@ -50,7 +53,7 @@ class AppKernel extends Kernel
|
||||
*/
|
||||
public function getContainerClass(): string
|
||||
{
|
||||
return parent::getContainerClass().substr(md5($this->rootConfig.$this->authenticatorManagerEnabled), -16);
|
||||
return parent::getContainerClass().substr(md5(implode('', $this->rootConfig).$this->authenticatorManagerEnabled), -16);
|
||||
}
|
||||
|
||||
public function registerBundles(): iterable
|
||||
@ -79,7 +82,9 @@ class AppKernel extends Kernel
|
||||
|
||||
public function registerContainerConfiguration(LoaderInterface $loader)
|
||||
{
|
||||
$loader->load($this->rootConfig);
|
||||
foreach ($this->rootConfig as $config) {
|
||||
$loader->load($config);
|
||||
}
|
||||
|
||||
if ($this->authenticatorManagerEnabled) {
|
||||
$loader->load(function ($container) {
|
||||
|
@ -1,30 +0,0 @@
|
||||
imports:
|
||||
- { resource: ./../config/framework.yml }
|
||||
|
||||
security:
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\InMemoryUser: plaintext
|
||||
|
||||
providers:
|
||||
in_memory:
|
||||
memory:
|
||||
users:
|
||||
johannes: { password: test, roles: [ROLE_USER] }
|
||||
|
||||
firewalls:
|
||||
default:
|
||||
form_login:
|
||||
check_path: login
|
||||
remember_me: true
|
||||
remember_me:
|
||||
always_remember_me: true
|
||||
secret: key
|
||||
|
||||
access_control:
|
||||
- { path: ^/foo, roles: ROLE_USER }
|
||||
|
||||
services:
|
||||
Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeUserProvider:
|
||||
public: true
|
||||
decorates: security.user.provider.concrete.in_memory
|
||||
arguments: ['@Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeUserProvider.inner']
|
@ -1,7 +0,0 @@
|
||||
imports:
|
||||
- { resource: ./config.yml }
|
||||
|
||||
security:
|
||||
firewalls:
|
||||
default:
|
||||
anonymous: ~
|
@ -1,7 +0,0 @@
|
||||
login:
|
||||
path: /login
|
||||
|
||||
foo:
|
||||
path: /foo
|
||||
defaults:
|
||||
_controller: Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeFooController
|
@ -11,10 +11,10 @@
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
|
||||
use Symfony\Bundle\SecurityBundle\SecurityBundle;
|
||||
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle;
|
||||
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\RememberMeBundle;
|
||||
|
||||
return [
|
||||
new FrameworkBundle(),
|
||||
new SecurityBundle(),
|
||||
new TestBundle(),
|
||||
new RememberMeBundle(),
|
||||
];
|
@ -0,0 +1,9 @@
|
||||
imports:
|
||||
- { resource: ./config.yml }
|
||||
- { resource: ./config_session.yml }
|
||||
|
||||
services:
|
||||
Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Security\UserChangingUserProvider:
|
||||
public: true
|
||||
decorates: security.user.provider.concrete.in_memory
|
||||
arguments: ['@.inner']
|
@ -1,12 +1,6 @@
|
||||
imports:
|
||||
- { resource: ./../config/framework.yml }
|
||||
|
||||
framework:
|
||||
session:
|
||||
storage_factory_id: session.storage.factory.mock_file
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
|
||||
security:
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\InMemoryUser: plaintext
|
||||
@ -19,12 +13,10 @@ security:
|
||||
|
||||
firewalls:
|
||||
default:
|
||||
logout: ~
|
||||
form_login:
|
||||
check_path: login
|
||||
remember_me: true
|
||||
require_previous_session: false
|
||||
remember_me:
|
||||
always_remember_me: true
|
||||
secret: key
|
||||
logout: ~
|
||||
stateless: true
|
||||
|
||||
access_control:
|
||||
- { path: ^/profile, roles: ROLE_USER }
|
@ -0,0 +1,12 @@
|
||||
services:
|
||||
app.static_token_provider:
|
||||
class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Security\StaticTokenProvider
|
||||
arguments: ['@kernel']
|
||||
|
||||
security:
|
||||
firewalls:
|
||||
default:
|
||||
remember_me:
|
||||
always_remember_me: true
|
||||
secret: key
|
||||
token_provider: app.static_token_provider
|
@ -0,0 +1,6 @@
|
||||
security:
|
||||
firewalls:
|
||||
default:
|
||||
remember_me:
|
||||
always_remember_me: true
|
||||
secret: key
|
@ -0,0 +1,9 @@
|
||||
login:
|
||||
path: /login
|
||||
|
||||
logout:
|
||||
path: /logout
|
||||
|
||||
profile:
|
||||
path: /profile
|
||||
controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\RememberMeBundle\Controller\ProfileController
|
@ -0,0 +1,13 @@
|
||||
imports:
|
||||
- { resource: ./config.yml }
|
||||
- { resource: ./config_session.yml }
|
||||
|
||||
framework:
|
||||
session:
|
||||
cookie_secure: auto
|
||||
cookie_samesite: lax
|
||||
|
||||
security:
|
||||
firewalls:
|
||||
default:
|
||||
stateless: true
|
@ -1,5 +0,0 @@
|
||||
login:
|
||||
path: /login
|
||||
|
||||
logout:
|
||||
path: /logout
|
@ -4,6 +4,11 @@ CHANGELOG
|
||||
5.3
|
||||
---
|
||||
|
||||
* Add `RememberMeConditionsListener` to check if remember me is requested and supported, and set priority of `RememberMeListener` to -63
|
||||
* Add `RememberMeHandlerInterface` and implementations, used as a replacement of `RememberMeServicesInterface` when using the AuthenticatorManager
|
||||
* Add `TokenDeauthenticatedEvent` that is dispatched when the current security token is deauthenticated
|
||||
* [BC break] Change constructor signature of `LoginLinkHandler` to `__construct(UrlGeneratorInterface $urlGenerator, UserProviderInterface $userProvider, SignatureHasher $signatureHashUtil, array $options)`
|
||||
* Add `Core\Signature\SignatureHasher` and moved `Http\LoginLink\ExpiredLoginLinkStorage` to `Core\Signature\ExpiredLoginLinkStorage`
|
||||
* Deprecate `PersistentTokenInterface::getUsername()` in favor of `PersistentTokenInterface::getUserIdentifier()`
|
||||
* Deprecate `UsernameNotFoundException` in favor of `UserNotFoundException` and `getUsername()`/`setUsername()` in favor of `getUserIdentifier()`/`setUserIdentifier()`
|
||||
* Deprecate `UserProviderInterface::loadUserByUsername()` in favor of `UserProviderInterface::loadUserByIdentifier()`
|
||||
|
@ -0,0 +1,21 @@
|
||||
<?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\Core\Signature\Exception;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class ExpiredSignatureException extends RuntimeException
|
||||
{
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<?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\Core\Signature\Exception;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
class InvalidSignatureException extends RuntimeException
|
||||
{
|
||||
}
|
@ -9,16 +9,18 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\LoginLink;
|
||||
namespace Symfony\Component\Security\Core\Signature;
|
||||
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
/**
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*
|
||||
* @experimental in 5.2
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class ExpiredLoginLinkStorage
|
||||
final class ExpiredSignatureStorage
|
||||
{
|
||||
private $cache;
|
||||
private $lifetime;
|
@ -0,0 +1,99 @@
|
||||
<?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\Core\Signature;
|
||||
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException;
|
||||
use Symfony\Component\Security\Core\Signature\Exception\InvalidSignatureException;
|
||||
use Symfony\Component\Security\Core\Signature\ExpiredSignatureStorage;
|
||||
|
||||
/**
|
||||
* Creates and validates secure hashes used in login links and remember-me cookies.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
*/
|
||||
class SignatureHasher
|
||||
{
|
||||
private $propertyAccessor;
|
||||
private $signatureProperties;
|
||||
private $secret;
|
||||
private $expiredSignaturesStorage;
|
||||
private $maxUses;
|
||||
|
||||
/**
|
||||
* @param array $signatureProperties properties of the User; the hash is invalidated if these properties change
|
||||
* @param ExpiredSignatureStorage|null $expiredSignaturesStorage if provided, secures a sequence of hashes that are expired
|
||||
* @param int|null $maxUses used together with $expiredSignatureStorage to allow a maximum usage of a hash
|
||||
*/
|
||||
public function __construct(PropertyAccessorInterface $propertyAccessor, array $signatureProperties, string $secret, ?ExpiredSignatureStorage $expiredSignaturesStorage = null, ?int $maxUses = null)
|
||||
{
|
||||
$this->propertyAccessor = $propertyAccessor;
|
||||
$this->signatureProperties = $signatureProperties;
|
||||
$this->secret = $secret;
|
||||
$this->expiredSignaturesStorage = $expiredSignaturesStorage;
|
||||
$this->maxUses = $maxUses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the hash using the provided user and expire time.
|
||||
*
|
||||
* @param int $expires the expiry time as a unix timestamp
|
||||
* @param string $hash the plaintext hash provided by the request
|
||||
*
|
||||
* @throws InvalidSignatureException If the signature does not match the provided parameters
|
||||
* @throws ExpiredSignatureException If the signature is no longer valid
|
||||
*/
|
||||
public function verifySignatureHash(UserInterface $user, int $expires, string $hash): void
|
||||
{
|
||||
if (!hash_equals($hash, $this->computeSignatureHash($user, $expires))) {
|
||||
throw new InvalidSignatureException('Invalid or expired signature.');
|
||||
}
|
||||
|
||||
if ($expires < time()) {
|
||||
throw new ExpiredSignatureException('Signature has expired.');
|
||||
}
|
||||
|
||||
if ($this->expiredSignaturesStorage && $this->maxUses) {
|
||||
if ($this->expiredSignaturesStorage->countUsages($hash) >= $this->maxUses) {
|
||||
throw new ExpiredSignatureException(sprintf('Signature can only be used "%d" times.', $this->maxUses));
|
||||
}
|
||||
|
||||
$this->expiredSignaturesStorage->incrementUsages($hash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the secure hash for the provided user and expire time.
|
||||
*
|
||||
* @param int $expires the expiry time as a unix timestamp
|
||||
*/
|
||||
public function computeSignatureHash(UserInterface $user, int $expires): string
|
||||
{
|
||||
$signatureFields = [base64_encode(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername()), $expires];
|
||||
|
||||
foreach ($this->signatureProperties as $property) {
|
||||
$value = $this->propertyAccessor->getValue($user, $property) ?? '';
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
$value = $value->format('c');
|
||||
}
|
||||
|
||||
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
|
||||
throw new \InvalidArgumentException(sprintf('The property path "%s" on the user object "%s" must return a value that can be cast to a string, but "%s" was returned.', $property, \get_class($user), get_debug_type($value)));
|
||||
}
|
||||
$signatureFields[] = base64_encode($value);
|
||||
}
|
||||
|
||||
return base64_encode(hash_hmac('sha256', implode(':', $signatureFields), $this->secret));
|
||||
}
|
||||
}
|
@ -9,18 +9,18 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Security\Http\Tests\LoginLink;
|
||||
namespace Symfony\Component\Security\Core\Tests\Signature;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Security\Http\LoginLink\ExpiredLoginLinkStorage;
|
||||
use Symfony\Component\Security\Core\Signature\ExpiredSignatureStorage;
|
||||
|
||||
class ExpiredLoginLinkStorageTest extends TestCase
|
||||
class ExpiredSignatureStorageTest extends TestCase
|
||||
{
|
||||
public function testUsage()
|
||||
{
|
||||
$cache = new ArrayAdapter();
|
||||
$storage = new ExpiredLoginLinkStorage($cache, 600);
|
||||
$storage = new ExpiredSignatureStorage($cache, 600);
|
||||
|
||||
$this->assertSame(0, $storage->countUsages('hash+more'));
|
||||
$storage->incrementUsages('hash+more');
|
@ -14,14 +14,9 @@ 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
|
||||
* The presence of this badge doesn't create the remember-me cookie. The actual
|
||||
* cookie is only created if this badge is enabled. By default, this is done
|
||||
* by the {@see RememberMeConditionsListener} if all conditions are met.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
@ -30,6 +25,40 @@ namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge;
|
||||
*/
|
||||
class RememberMeBadge implements BadgeInterface
|
||||
{
|
||||
private $enabled = false;
|
||||
|
||||
/**
|
||||
* Enables remember-me cookie creation.
|
||||
*
|
||||
* In most cases, {@see RememberMeConditionsListener} enables this
|
||||
* automatically if always_remember_me is true or the remember_me_parameter
|
||||
* exists in the request.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function enable(): self
|
||||
{
|
||||
$this->enabled = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables remember-me cookie creation.
|
||||
*
|
||||
* The default is disabled, this can be called to suppress creation
|
||||
* after it was enabled.
|
||||
*/
|
||||
public function disable(): void
|
||||
{
|
||||
$this->enabled = false;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return true; // remember me does not need to be explicitly resolved
|
||||
|
@ -11,22 +11,28 @@
|
||||
|
||||
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\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\Core\Exception\CookieTheftException;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeDetails;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
use Symfony\Component\Security\Http\RememberMe\ResponseListener;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* expired and a remember-me cookie was found. This authenticator
|
||||
* then "re-authenticates" the user using the information in the
|
||||
* cookie.
|
||||
*
|
||||
@ -37,17 +43,19 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
*/
|
||||
class RememberMeAuthenticator implements InteractiveAuthenticatorInterface
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $rememberMeHandler;
|
||||
private $secret;
|
||||
private $tokenStorage;
|
||||
private $options = [];
|
||||
private $cookieName;
|
||||
private $logger;
|
||||
|
||||
public function __construct(RememberMeServicesInterface $rememberMeServices, string $secret, TokenStorageInterface $tokenStorage, array $options)
|
||||
public function __construct(RememberMeHandlerInterface $rememberMeHandler, string $secret, TokenStorageInterface $tokenStorage, string $cookieName, LoggerInterface $logger = null)
|
||||
{
|
||||
$this->rememberMeServices = $rememberMeServices;
|
||||
$this->rememberMeHandler = $rememberMeHandler;
|
||||
$this->secret = $secret;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->options = $options;
|
||||
$this->cookieName = $cookieName;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
@ -57,19 +65,17 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the attribute is set, this is a lazy firewall. The previous
|
||||
// support call already indicated support, so return null and avoid
|
||||
// recreating the cookie
|
||||
if ($request->attributes->has('_remember_me_token')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$token = $this->rememberMeServices->autoLogin($request);
|
||||
if (null === $token) {
|
||||
if (($cookie = $request->attributes->get(ResponseListener::COOKIE_ATTR_NAME)) && null === $cookie->getValue()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$request->attributes->set('_remember_me_token', $token);
|
||||
if (!$request->cookies->has($this->cookieName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Remember-me cookie detected.');
|
||||
}
|
||||
|
||||
// the `null` return value indicates that this authenticator supports lazy firewalls
|
||||
return null;
|
||||
@ -77,13 +83,16 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface
|
||||
|
||||
public function authenticate(Request $request): PassportInterface
|
||||
{
|
||||
$token = $request->attributes->get('_remember_me_token');
|
||||
if (null === $token) {
|
||||
throw new \LogicException('No remember me token is set.');
|
||||
$rawCookie = $request->cookies->get($this->cookieName);
|
||||
if (!$rawCookie) {
|
||||
throw new \LogicException('No remember-me cookie is found.');
|
||||
}
|
||||
|
||||
// @deprecated since 5.3, change to $token->getUserIdentifier() in 6.0
|
||||
return new SelfValidatingPassport(new UserBadge(method_exists($token, 'getUserIdentifier') ? $token->getUserIdentifier() : $token->getUsername(), [$token, 'getUser']));
|
||||
$rememberMeCookie = RememberMeDetails::fromRawCookie($rawCookie);
|
||||
|
||||
return new SelfValidatingPassport(new UserBadge($rememberMeCookie->getUserIdentifier(), function () use ($rememberMeCookie) {
|
||||
return $this->rememberMeHandler->consumeRememberMeCookie($rememberMeCookie);
|
||||
}));
|
||||
}
|
||||
|
||||
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
|
||||
@ -98,7 +107,15 @@ class RememberMeAuthenticator implements InteractiveAuthenticatorInterface
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$this->rememberMeServices->loginFail($request, $exception);
|
||||
if (null !== $this->logger) {
|
||||
if ($exception instanceof UsernameNotFoundException) {
|
||||
$this->logger->info('User for remember-me cookie not found.', ['exception' => $exception]);
|
||||
} elseif ($exception instanceof UnsupportedUserException) {
|
||||
$this->logger->warning('User class for remember-me cookie not supported.', ['exception' => $exception]);
|
||||
} elseif (!$exception instanceof CookieTheftException) {
|
||||
$this->logger->debug('Remember me authentication failed.', ['exception' => $exception]);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -15,7 +15,11 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Deauthentication happens in case the user has changed when trying to refresh the token.
|
||||
* Deauthentication happens in case the user has changed when trying to
|
||||
* refresh the token.
|
||||
*
|
||||
* Use {@see TokenDeauthenticatedEvent} if you want to cover all cases where
|
||||
* a session is deauthenticated.
|
||||
*
|
||||
* @author Hamza Amrouche <hamza.simperfit@gmail.com>
|
||||
*/
|
||||
|
@ -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\Event;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* This event is dispatched when the current security token is deauthenticated
|
||||
* when trying to reference the token.
|
||||
*
|
||||
* This includes changes in the user ({@see DeauthenticatedEvent}), but
|
||||
* also cases where there is no user provider available to refresh the user.
|
||||
*
|
||||
* Use this event if you want to trigger some actions whenever a user is
|
||||
* deauthenticated and redirected back to the authentication entry point
|
||||
* (e.g. clearing all remember-me cookies).
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*/
|
||||
final class TokenDeauthenticatedEvent extends Event
|
||||
{
|
||||
private $originalToken;
|
||||
private $request;
|
||||
|
||||
public function __construct(TokenInterface $originalToken, Request $request)
|
||||
{
|
||||
$this->originalToken = $originalToken;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function getOriginalToken(): TokenInterface
|
||||
{
|
||||
return $this->originalToken;
|
||||
}
|
||||
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
<?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 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\Event\LogoutEvent;
|
||||
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
|
||||
use Symfony\Component\Security\Http\ParameterBagUtils;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
|
||||
/**
|
||||
* Checks if all conditions are met for remember me.
|
||||
*
|
||||
* The conditions that must be met for this listener to enable remember me:
|
||||
* 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 (or "always_remember_me"
|
||||
* is enabled)
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @final
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
class CheckRememberMeConditionsListener implements EventSubscriberInterface
|
||||
{
|
||||
private $options;
|
||||
private $logger;
|
||||
|
||||
public function __construct(array $options = [], ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->options = $options + ['always_remember_me' => false, 'remember_me_parameter' => '_remember_me'];
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function onSuccessfulLogin(LoginSuccessEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport->hasBadge(RememberMeBadge::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var RememberMeBadge $badge */
|
||||
$badge = $passport->getBadge(RememberMeBadge::class);
|
||||
if (!$this->options['always_remember_me']) {
|
||||
$parameter = ParameterBagUtils::getRequestParameterValue($event->getRequest(), $this->options['remember_me_parameter']);
|
||||
if (!('true' === $parameter || 'on' === $parameter || '1' === $parameter || 'yes' === $parameter || true === $parameter)) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Remember me disabled; request does not contain remember me parameter ("{parameter}").', ['parameter' => $this->options['remember_me_parameter']]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$badge->enable();
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [LoginSuccessEvent::class => ['onSuccessfulLogin', -32]];
|
||||
}
|
||||
}
|
@ -16,15 +16,18 @@ 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;
|
||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
|
||||
use Symfony\Component\Security\Http\ParameterBagUtils;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
|
||||
/**
|
||||
* The RememberMe *listener* creates and deletes remember me cookies.
|
||||
* 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.
|
||||
* a remember-me cookie.
|
||||
* Upon login failure, all remember-me cookies are removed.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
@ -33,12 +36,12 @@ use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
*/
|
||||
class RememberMeListener implements EventSubscriberInterface
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $rememberMeHandler;
|
||||
private $logger;
|
||||
|
||||
public function __construct(RememberMeServicesInterface $rememberMeServices, ?LoggerInterface $logger = null)
|
||||
public function __construct(RememberMeHandlerInterface $rememberMeHandler, ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->rememberMeServices = $rememberMeServices;
|
||||
$this->rememberMeHandler = $rememberMeHandler;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
@ -53,27 +56,38 @@ class RememberMeListener implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $event->getResponse()) {
|
||||
// Make sure any old remember-me cookies are cancelled
|
||||
$this->rememberMeHandler->clearRememberMeCookie();
|
||||
|
||||
/** @var RememberMeBadge $badge */
|
||||
$badge = $passport->getBadge(RememberMeBadge::class);
|
||||
if (!$badge->isEnabled()) {
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Remember me skipped: the authenticator did not set a success response.', ['authenticator' => \get_class($event->getAuthenticator())]);
|
||||
$this->logger->debug('Remember me skipped: the RememberMeBadge is not enabled.');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->rememberMeServices->loginSuccess($event->getRequest(), $event->getResponse(), $event->getAuthenticatedToken());
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Remember-me was requested; setting cookie.');
|
||||
}
|
||||
|
||||
$this->rememberMeHandler->createRememberMeCookie($event->getUser());
|
||||
}
|
||||
|
||||
public function onFailedLogin(LoginFailureEvent $event): void
|
||||
public function clearCookie(): void
|
||||
{
|
||||
$this->rememberMeServices->loginFail($event->getRequest(), $event->getException());
|
||||
$this->rememberMeHandler->clearRememberMeCookie();
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LoginSuccessEvent::class => 'onSuccessfulLogin',
|
||||
LoginFailureEvent::class => 'onFailedLogin',
|
||||
LoginSuccessEvent::class => ['onSuccessfulLogin', -64],
|
||||
LoginFailureEvent::class => 'clearCookie',
|
||||
LogoutEvent::class => 'clearCookie',
|
||||
TokenDeauthenticatedEvent::class => 'clearCookie',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Event\DeauthenticatedEvent;
|
||||
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
@ -94,6 +95,8 @@ class ContextListener extends AbstractListener
|
||||
$request = $event->getRequest();
|
||||
$session = $request->hasPreviousSession() && $request->hasSession() ? $request->getSession() : null;
|
||||
|
||||
$request->attributes->set('_security_firewall_run', true);
|
||||
|
||||
if (null !== $session) {
|
||||
$usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : 0;
|
||||
$usageIndexReference = \PHP_INT_MIN;
|
||||
@ -128,10 +131,17 @@ class ContextListener extends AbstractListener
|
||||
}
|
||||
|
||||
if ($token instanceof TokenInterface) {
|
||||
$originalToken = $token;
|
||||
$token = $this->refreshUser($token);
|
||||
|
||||
if (!$token && $this->rememberMeServices) {
|
||||
$this->rememberMeServices->loginFail($request);
|
||||
if (!$token) {
|
||||
if ($this->dispatcher) {
|
||||
$this->dispatcher->dispatch(new TokenDeauthenticatedEvent($originalToken, $request));
|
||||
}
|
||||
|
||||
if ($this->rememberMeServices) {
|
||||
$this->rememberMeServices->loginFail($request);
|
||||
}
|
||||
}
|
||||
} elseif (null !== $token) {
|
||||
if (null !== $this->logger) {
|
||||
@ -159,11 +169,13 @@ class ContextListener extends AbstractListener
|
||||
|
||||
$request = $event->getRequest();
|
||||
|
||||
if (!$request->hasSession()) {
|
||||
if (!$request->hasSession() || !$request->attributes->get('_security_firewall_run', false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatcher->removeListener(KernelEvents::RESPONSE, [$this, 'onKernelResponse']);
|
||||
if ($this->dispatcher) {
|
||||
$this->dispatcher->removeListener(KernelEvents::RESPONSE, [$this, 'onKernelResponse']);
|
||||
}
|
||||
$this->registered = false;
|
||||
$session = $request->getSession();
|
||||
$sessionId = $session->getId();
|
||||
@ -260,7 +272,7 @@ class ContextListener extends AbstractListener
|
||||
$this->logger->debug('Token was deauthenticated after trying to refresh it.');
|
||||
}
|
||||
|
||||
if (null !== $this->dispatcher) {
|
||||
if ($this->dispatcher) {
|
||||
$this->dispatcher->dispatch(new DeauthenticatedEvent($token, $newToken), DeauthenticatedEvent::class);
|
||||
}
|
||||
|
||||
|
@ -11,10 +11,12 @@
|
||||
|
||||
namespace Symfony\Component\Security\Http\LoginLink\Exception;
|
||||
|
||||
use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException;
|
||||
|
||||
/**
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
class ExpiredLoginLinkException extends \Exception implements InvalidLoginLinkExceptionInterface
|
||||
class ExpiredLoginLinkException extends ExpiredSignatureException implements InvalidLoginLinkExceptionInterface
|
||||
{
|
||||
}
|
||||
|
@ -15,6 +15,6 @@ namespace Symfony\Component\Security\Http\LoginLink\Exception;
|
||||
* @author Ryan Weaver <ryan@symfonycasts.com>
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
class InvalidLoginLinkException extends \Exception implements InvalidLoginLinkExceptionInterface
|
||||
class InvalidLoginLinkException extends \RuntimeException implements InvalidLoginLinkExceptionInterface
|
||||
{
|
||||
}
|
||||
|
@ -12,10 +12,12 @@
|
||||
namespace Symfony\Component\Security\Http\LoginLink;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Routing\RequestContext;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException;
|
||||
use Symfony\Component\Security\Core\Signature\Exception\InvalidSignatureException;
|
||||
use Symfony\Component\Security\Core\Signature\SignatureHasher;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\LoginLink\Exception\ExpiredLoginLinkException;
|
||||
@ -29,25 +31,18 @@ final class LoginLinkHandler implements LoginLinkHandlerInterface
|
||||
{
|
||||
private $urlGenerator;
|
||||
private $userProvider;
|
||||
private $propertyAccessor;
|
||||
private $signatureProperties;
|
||||
private $secret;
|
||||
private $options;
|
||||
private $expiredStorage;
|
||||
private $signatureHashUtil;
|
||||
|
||||
public function __construct(UrlGeneratorInterface $urlGenerator, UserProviderInterface $userProvider, PropertyAccessorInterface $propertyAccessor, array $signatureProperties, string $secret, array $options, ?ExpiredLoginLinkStorage $expiredStorage)
|
||||
public function __construct(UrlGeneratorInterface $urlGenerator, UserProviderInterface $userProvider, SignatureHasher $signatureHashUtil, array $options)
|
||||
{
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->userProvider = $userProvider;
|
||||
$this->propertyAccessor = $propertyAccessor;
|
||||
$this->signatureProperties = $signatureProperties;
|
||||
$this->secret = $secret;
|
||||
$this->signatureHashUtil = $signatureHashUtil;
|
||||
$this->options = array_merge([
|
||||
'route_name' => null,
|
||||
'lifetime' => 600,
|
||||
'max_uses' => null,
|
||||
], $options);
|
||||
$this->expiredStorage = $expiredStorage;
|
||||
}
|
||||
|
||||
public function createLoginLink(UserInterface $user, Request $request = null): LoginLinkDetails
|
||||
@ -59,7 +54,7 @@ final class LoginLinkHandler implements LoginLinkHandlerInterface
|
||||
// @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0
|
||||
'user' => method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(),
|
||||
'expires' => $expires,
|
||||
'hash' => $this->computeSignatureHash($user, $expires),
|
||||
'hash' => $this->signatureHashUtil->computeSignatureHash($user, $expires),
|
||||
];
|
||||
|
||||
if ($request) {
|
||||
@ -105,43 +100,15 @@ final class LoginLinkHandler implements LoginLinkHandlerInterface
|
||||
|
||||
$hash = $request->get('hash');
|
||||
$expires = $request->get('expires');
|
||||
if (false === hash_equals($hash, $this->computeSignatureHash($user, $expires))) {
|
||||
throw new InvalidLoginLinkException('Invalid or expired signature.');
|
||||
}
|
||||
|
||||
if ($expires < time()) {
|
||||
throw new ExpiredLoginLinkException('Login link has expired.');
|
||||
}
|
||||
|
||||
if ($this->expiredStorage && $this->options['max_uses']) {
|
||||
$hash = $request->get('hash');
|
||||
if ($this->expiredStorage->countUsages($hash) >= $this->options['max_uses']) {
|
||||
throw new ExpiredLoginLinkException(sprintf('Login link can only be used "%d" times.', $this->options['max_uses']));
|
||||
}
|
||||
|
||||
$this->expiredStorage->incrementUsages($hash);
|
||||
try {
|
||||
$this->signatureHashUtil->verifySignatureHash($user, $expires, $hash);
|
||||
} catch (ExpiredSignatureException $e) {
|
||||
throw new ExpiredLoginLinkException(ucfirst(str_ireplace('signature', 'login link', $e->getMessage())), 0, $e);
|
||||
} catch (InvalidSignatureException $e) {
|
||||
throw new InvalidLoginLinkException(ucfirst(str_ireplace('signature', 'login link', $e->getMessage())), 0, $e);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function computeSignatureHash(UserInterface $user, int $expires): string
|
||||
{
|
||||
// @deprecated since 5.3, change to $user->getUserIdentifier() in 6.0
|
||||
$signatureFields = [base64_encode(method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername()), $expires];
|
||||
|
||||
foreach ($this->signatureProperties as $property) {
|
||||
$value = $this->propertyAccessor->getValue($user, $property) ?? '';
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
$value = $value->format('c');
|
||||
}
|
||||
|
||||
if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) {
|
||||
throw new \InvalidArgumentException(sprintf('The property path "%s" on the user object "%s" must return a value that can be cast to a string, but "%s" was returned.', $property, \get_class($user), get_debug_type($value)));
|
||||
}
|
||||
$signatureFields[] = base64_encode($value);
|
||||
}
|
||||
|
||||
return base64_encode(hash_hmac('sha256', implode(':', $signatureFields), $this->secret));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,131 @@
|
||||
<?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\RememberMe;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
abstract class AbstractRememberMeHandler implements RememberMeHandlerInterface
|
||||
{
|
||||
private $userProvider;
|
||||
protected $requestStack;
|
||||
protected $options;
|
||||
protected $logger;
|
||||
|
||||
public function __construct(UserProviderInterface $userProvider, RequestStack $requestStack, array $options = [], ?LoggerInterface $logger = null)
|
||||
{
|
||||
$this->userProvider = $userProvider;
|
||||
$this->requestStack = $requestStack;
|
||||
$this->options = $options + [
|
||||
'name' => 'REMEMBERME',
|
||||
'lifetime' => 31536000,
|
||||
'path' => '/',
|
||||
'domain' => null,
|
||||
'secure' => false,
|
||||
'httponly' => true,
|
||||
'samesite' => null,
|
||||
'always_remember_me' => false,
|
||||
'remember_me_parameter' => '_remember_me',
|
||||
];
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the RememberMeDetails is a valid cookie to login the given User.
|
||||
*
|
||||
* This method should also:
|
||||
* - Create a new remember-me cookie to be sent with the response (using {@see createCookie()});
|
||||
* - If you store the token somewhere else (e.g. in a database), invalidate the stored token.
|
||||
*
|
||||
* @throws AuthenticationException throw this exception if the remember me details are not accepted
|
||||
*/
|
||||
abstract protected function processRememberMe(RememberMeDetails $rememberMeDetails, UserInterface $user): void;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): UserInterface
|
||||
{
|
||||
try {
|
||||
// @deprecated since 5.3, change to $this->userProvider->loadUserByIdentifier() in 6.0
|
||||
$method = 'loadUserByIdentifier';
|
||||
if (!method_exists($this->userProvider, 'loadUserByIdentifier')) {
|
||||
trigger_deprecation('symfony/security-core', '5.3', 'Not implementing method "loadUserByIdentifier()" in user provider "%s" is deprecated. This method will replace "loadUserByUsername()" in Symfony 6.0.', get_debug_type($this->userProvider));
|
||||
|
||||
$method = 'loadUserByUsername';
|
||||
}
|
||||
|
||||
$user = $this->userProvider->$method($rememberMeDetails->getUserIdentifier());
|
||||
} catch (AuthenticationException $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
if (!$user instanceof UserInterface) {
|
||||
throw new \LogicException(sprintf('The UserProviderInterface implementation must return an instance of UserInterface, but returned "%s".', get_debug_type($user)));
|
||||
}
|
||||
|
||||
$this->processRememberMe($rememberMeDetails, $user);
|
||||
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->info('Remember-me cookie accepted.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function clearRememberMeCookie(): void
|
||||
{
|
||||
if (null !== $this->logger) {
|
||||
$this->logger->debug('Clearing remember-me cookie.', ['name' => $this->options['name']]);
|
||||
}
|
||||
|
||||
$this->createCookie(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the remember-me cookie using the correct configuration.
|
||||
*
|
||||
* @param RememberMeDetails|null $rememberMeDetails The details for the cookie, or null to clear the remember-me cookie
|
||||
*/
|
||||
protected function createCookie(?RememberMeDetails $rememberMeDetails)
|
||||
{
|
||||
$request = $this->requestStack->getMainRequest();
|
||||
if (!$request) {
|
||||
throw new \LogicException('Cannot create the remember-me cookie; no master request available.');
|
||||
}
|
||||
|
||||
// the ResponseListener configures the cookie saved in this attribute on the final response object
|
||||
$request->attributes->set(ResponseListener::COOKIE_ATTR_NAME, new Cookie(
|
||||
$this->options['name'],
|
||||
$rememberMeDetails ? $rememberMeDetails->toString() : null,
|
||||
$rememberMeDetails ? $rememberMeDetails->getExpires() : 1,
|
||||
$this->options['path'],
|
||||
$this->options['domain'],
|
||||
$this->options['secure'] ?? $request->isSecure(),
|
||||
$this->options['httponly'],
|
||||
false,
|
||||
$this->options['samesite']
|
||||
));
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
<?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\RememberMe;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\CookieTheftException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* Implements remember-me tokens using a {@see TokenProviderInterface}.
|
||||
*
|
||||
* This requires storing remember-me tokens in a database. This allows
|
||||
* more control over the invalidation of remember-me tokens. See
|
||||
* {@see SignatureRememberMeHandler} if you don't want to use a database.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
final class PersistentRememberMeHandler extends AbstractRememberMeHandler
|
||||
{
|
||||
private $tokenProvider;
|
||||
private $secret;
|
||||
|
||||
public function __construct(TokenProviderInterface $tokenProvider, string $secret, UserProviderInterface $userProvider, RequestStack $requestStack, array $options, ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($userProvider, $requestStack, $options, $logger);
|
||||
|
||||
$this->tokenProvider = $tokenProvider;
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createRememberMeCookie(UserInterface $user): void
|
||||
{
|
||||
$series = base64_encode(random_bytes(64));
|
||||
$tokenValue = $this->generateHash(base64_encode(random_bytes(64)));
|
||||
$token = new PersistentToken(\get_class($user), method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), $series, $tokenValue, new \DateTime());
|
||||
|
||||
$this->tokenProvider->createNewToken($token);
|
||||
$this->createCookie(RememberMeDetails::fromPersistentToken($token, time() + $this->options['lifetime']));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInterface $user): void
|
||||
{
|
||||
if (!str_contains($rememberMeDetails->getValue(), ':')) {
|
||||
throw new AuthenticationException('The cookie is incorrectly formatted.');
|
||||
}
|
||||
|
||||
[$series, $tokenValue] = explode(':', $rememberMeDetails->getValue());
|
||||
$persistentToken = $this->tokenProvider->loadTokenBySeries($series);
|
||||
if (!hash_equals($persistentToken->getTokenValue(), $tokenValue)) {
|
||||
throw new CookieTheftException('This token was already used. The account is possibly compromised.');
|
||||
}
|
||||
|
||||
if ($persistentToken->getLastUsed()->getTimestamp() + $this->options['lifetime'] < time()) {
|
||||
throw new AuthenticationException('The cookie has expired.');
|
||||
}
|
||||
|
||||
$tokenValue = base64_encode(random_bytes(64));
|
||||
$this->tokenProvider->updateToken($series, $this->generateHash($tokenValue), new \DateTime());
|
||||
|
||||
$this->createCookie($rememberMeDetails->withValue($tokenValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function clearRememberMeCookie(): void
|
||||
{
|
||||
parent::clearRememberMeCookie();
|
||||
|
||||
$cookie = $this->requestStack->getMainRequest()->cookies->get($this->options['name']);
|
||||
if (null === $cookie) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rememberMeDetails = RememberMeDetails::fromRawCookie($cookie);
|
||||
[$series, ] = explode(':', $rememberMeDetails->getValue());
|
||||
$this->tokenProvider->deleteTokenBySeries($series);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getTokenProvider(): TokenProviderInterface
|
||||
{
|
||||
return $this->tokenProvider;
|
||||
}
|
||||
|
||||
private function generateHash(string $tokenValue): string
|
||||
{
|
||||
return hash_hmac('sha256', $tokenValue, $this->secret);
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
<?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\RememberMe;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
|
||||
/**
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
class RememberMeDetails
|
||||
{
|
||||
public const COOKIE_DELIMITER = ':';
|
||||
|
||||
private $userFqcn;
|
||||
private $userIdentifier;
|
||||
private $expires;
|
||||
private $value;
|
||||
|
||||
public function __construct(string $userFqcn, string $userIdentifier, int $expires, string $value)
|
||||
{
|
||||
$this->userFqcn = $userFqcn;
|
||||
$this->userIdentifier = $userIdentifier;
|
||||
$this->expires = $expires;
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
public static function fromRawCookie(string $rawCookie): self
|
||||
{
|
||||
$cookieParts = explode(self::COOKIE_DELIMITER, base64_decode($rawCookie), 4);
|
||||
if (false === $cookieParts[1] = base64_decode($cookieParts[1], true)) {
|
||||
throw new AuthenticationException('The user identifier contains a character from outside the base64 alphabet.');
|
||||
}
|
||||
|
||||
return new static(...$cookieParts);
|
||||
}
|
||||
|
||||
public static function fromPersistentToken(PersistentToken $persistentToken, int $expires): self
|
||||
{
|
||||
return new static($persistentToken->getClass(), $persistentToken->getUserIdentifier(), $expires, $persistentToken->getSeries().':'.$persistentToken->getTokenValue());
|
||||
}
|
||||
|
||||
public function withValue(string $value): self
|
||||
{
|
||||
$details = clone $this;
|
||||
$details->value = $value;
|
||||
|
||||
return $details;
|
||||
}
|
||||
|
||||
public function getUserFqcn(): string
|
||||
{
|
||||
return $this->userFqcn;
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return $this->userIdentifier;
|
||||
}
|
||||
|
||||
public function getExpires(): int
|
||||
{
|
||||
return $this->expires;
|
||||
}
|
||||
|
||||
public function getValue(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
// $userIdentifier is encoded because it might contain COOKIE_DELIMITER, we assume other values don't
|
||||
return base64_encode(implode(self::COOKIE_DELIMITER, [$this->userFqcn, base64_encode($this->userIdentifier), $this->expires, $this->value]));
|
||||
}
|
||||
}
|
@ -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\Component\Security\Http\RememberMe;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Handles creating and validating remember-me cookies.
|
||||
*
|
||||
* If you want to add a custom implementation, you want to extend from
|
||||
* {@see AbstractRememberMeHandler} instead.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
interface RememberMeHandlerInterface
|
||||
{
|
||||
/**
|
||||
* Creates a remember-me cookie.
|
||||
*
|
||||
* The actual cookie should be set as an attribute on the main request,
|
||||
* which is transformed into a response cookie by {@see ResponseListener}.
|
||||
*/
|
||||
public function createRememberMeCookie(UserInterface $user): void;
|
||||
|
||||
/**
|
||||
* Validates the remember-me cookie and returns the associated User.
|
||||
*
|
||||
* Every cookie should only be used once. This means that this method should also:
|
||||
* - Create a new remember-me cookie to be sent with the response (using the
|
||||
* {@see ResponseListener::COOKIE_ATTR_NAME} request attribute);
|
||||
* - If you store the token somewhere else (e.g. in a database), invalidate the
|
||||
* stored token.
|
||||
*
|
||||
* @throws AuthenticationException
|
||||
*/
|
||||
public function consumeRememberMeCookie(RememberMeDetails $rememberMeDetails): UserInterface;
|
||||
|
||||
/**
|
||||
* Clears the remember-me cookie.
|
||||
*
|
||||
* This should set a cookie with a `null` value on the request attribute.
|
||||
*/
|
||||
public function clearRememberMeCookie(): void;
|
||||
}
|
@ -24,6 +24,12 @@ use Symfony\Component\HttpKernel\KernelEvents;
|
||||
*/
|
||||
class ResponseListener implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* This attribute name can be used by the implementation if it needs to set
|
||||
* a cookie on the Request when there is no actual Response, yet.
|
||||
*/
|
||||
public const COOKIE_ATTR_NAME = '_security_remember_me_cookie';
|
||||
|
||||
public function onKernelResponse(ResponseEvent $event)
|
||||
{
|
||||
if (!$event->isMainRequest()) {
|
||||
@ -33,8 +39,8 @@ class ResponseListener implements EventSubscriberInterface
|
||||
$request = $event->getRequest();
|
||||
$response = $event->getResponse();
|
||||
|
||||
if ($request->attributes->has(RememberMeServicesInterface::COOKIE_ATTR_NAME)) {
|
||||
$response->headers->setCookie($request->attributes->get(RememberMeServicesInterface::COOKIE_ATTR_NAME));
|
||||
if ($request->attributes->has(self::COOKIE_ATTR_NAME)) {
|
||||
$response->headers->setCookie($request->attributes->get(self::COOKIE_ATTR_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,73 @@
|
||||
<?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\RememberMe;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException;
|
||||
use Symfony\Component\Security\Core\Signature\Exception\InvalidSignatureException;
|
||||
use Symfony\Component\Security\Core\Signature\SignatureHasher;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* Implements safe remember-me cookies using the {@see SignatureHasher}.
|
||||
*
|
||||
* This handler doesn't require a database for the remember-me tokens.
|
||||
* However, it cannot invalidate a specific user session, all sessions for
|
||||
* that user will be invalidated instead. Use {@see PersistentRememberMeHandler}
|
||||
* if you need this.
|
||||
*
|
||||
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||
*
|
||||
* @experimental in 5.3
|
||||
*/
|
||||
final class SignatureRememberMeHandler extends AbstractRememberMeHandler
|
||||
{
|
||||
private $signatureHasher;
|
||||
|
||||
public function __construct(SignatureHasher $signatureHasher, UserProviderInterface $userProvider, RequestStack $requestStack, array $options, ?LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($userProvider, $requestStack, $options, $logger);
|
||||
|
||||
$this->signatureHasher = $signatureHasher;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createRememberMeCookie(UserInterface $user): void
|
||||
{
|
||||
$expires = time() + $this->options['lifetime'];
|
||||
$value = $this->signatureHasher->computeSignatureHash($user, $expires);
|
||||
|
||||
$details = new RememberMeDetails(\get_class($user), method_exists($user, 'getUserIdentifier') ? $user->getUserIdentifier() : $user->getUsername(), $expires, $value);
|
||||
$this->createCookie($details);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInterface $user): void
|
||||
{
|
||||
try {
|
||||
$this->signatureHasher->verifySignatureHash($user, $rememberMeDetails->getExpires(), $rememberMeDetails->getValue());
|
||||
} catch (InvalidSignatureException $e) {
|
||||
throw new AuthenticationException('The cookie\'s hash is invalid.', 0, $e);
|
||||
} catch (ExpiredSignatureException $e) {
|
||||
throw new AuthenticationException('The cookie has expired.', 0, $e);
|
||||
}
|
||||
|
||||
$this->createRememberMeCookie($user);
|
||||
}
|
||||
}
|
@ -12,74 +12,72 @@
|
||||
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\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\User\InMemoryUser;
|
||||
use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeDetails;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
use Symfony\Component\Security\Http\RememberMe\ResponseListener;
|
||||
|
||||
class RememberMeAuthenticatorTest extends TestCase
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $rememberMeHandler;
|
||||
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->rememberMeHandler = $this->createMock(RememberMeHandlerInterface::class);
|
||||
$this->tokenStorage = new TokenStorage();
|
||||
$this->authenticator = new RememberMeAuthenticator($this->rememberMeHandler, 's3cr3t', $this->tokenStorage, '_remember_me_cookie');
|
||||
}
|
||||
|
||||
public function testSupportsTokenStorageWithToken()
|
||||
{
|
||||
$this->tokenStorage->expects($this->any())->method('getToken')->willReturn(TokenInterface::class);
|
||||
$this->tokenStorage->setToken(new UsernamePasswordToken('username', 'credentials', 'main'));
|
||||
|
||||
$this->assertFalse($this->authenticator->supports($this->request));
|
||||
$this->assertFalse($this->authenticator->supports(Request::create('/')));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSupportsData
|
||||
*/
|
||||
public function testSupports($autoLoginResult, $support)
|
||||
public function testSupports($request, $support)
|
||||
{
|
||||
$this->rememberMeServices->expects($this->once())->method('autoLogin')->with($this->request)->willReturn($autoLoginResult);
|
||||
|
||||
$this->assertSame($support, $this->authenticator->supports($this->request));
|
||||
$this->assertSame($support, $this->authenticator->supports($request));
|
||||
}
|
||||
|
||||
public function provideSupportsData()
|
||||
{
|
||||
yield [null, false];
|
||||
yield [$this->createMock(TokenInterface::class), null];
|
||||
}
|
||||
yield [Request::create('/'), false];
|
||||
|
||||
public function testConsecutiveSupportsCalls()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->once())->method('autoLogin')->with($this->request)->willReturn($this->createMock(TokenInterface::class));
|
||||
$request = Request::create('/', 'GET', [], ['_remember_me_cookie' => 'rememberme']);
|
||||
yield [$request, null];
|
||||
|
||||
$this->assertNull($this->authenticator->supports($this->request));
|
||||
$this->assertNull($this->authenticator->supports($this->request));
|
||||
$request = Request::create('/', 'GET', [], ['_remember_me_cookie' => 'rememberme']);
|
||||
$request->attributes->set(ResponseListener::COOKIE_ATTR_NAME, new Cookie('_remember_me_cookie', null));
|
||||
yield [$request, false];
|
||||
}
|
||||
|
||||
public function testAuthenticate()
|
||||
{
|
||||
$this->request->attributes->set('_remember_me_token', new RememberMeToken($user = new InMemoryUser('wouter', 'test'), 'main', 'secret'));
|
||||
$passport = $this->authenticator->authenticate($this->request);
|
||||
$rememberMeDetails = new RememberMeDetails(InMemoryUser::class, 'wouter', 1, 'secret');
|
||||
$request = Request::create('/', 'GET', [], ['_remember_me_cookie' => $rememberMeDetails->toString()]);
|
||||
$passport = $this->authenticator->authenticate($request);
|
||||
|
||||
$this->assertSame($user, $passport->getUser());
|
||||
$this->rememberMeHandler->expects($this->once())->method('consumeRememberMeCookie')->with($this->callback(function ($arg) use ($rememberMeDetails) {
|
||||
return $rememberMeDetails == $arg;
|
||||
}));
|
||||
$passport->getUser(); // trigger the user loader
|
||||
}
|
||||
|
||||
public function testAuthenticateWithoutToken()
|
||||
{
|
||||
$this->expectException(\LogicException::class);
|
||||
|
||||
$this->authenticator->authenticate($this->request);
|
||||
$this->authenticator->authenticate(Request::create('/'));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,101 @@
|
||||
<?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\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\Badge\UserBadge;
|
||||
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\CheckRememberMeConditionsListener;
|
||||
|
||||
class CheckRememberMeConditionsListenerTest extends TestCase
|
||||
{
|
||||
private $listener;
|
||||
private $request;
|
||||
private $response;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->listener = new CheckRememberMeConditionsListener();
|
||||
$this->request = Request::create('/login');
|
||||
$this->request->request->set('_remember_me', true);
|
||||
$this->response = new Response();
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWithoutSupportingAuthenticator()
|
||||
{
|
||||
$passport = $this->createPassport([]);
|
||||
|
||||
$this->listener->onSuccessfulLogin($this->createLoginSuccessfulEvent($passport));
|
||||
|
||||
$this->assertFalse($passport->hasBadge(RememberMeBadge::class));
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWithoutRequestParameter()
|
||||
{
|
||||
$this->request = Request::create('/login');
|
||||
$passport = $this->createPassport();
|
||||
|
||||
$this->listener->onSuccessfulLogin($this->createLoginSuccessfulEvent($passport));
|
||||
|
||||
$this->assertFalse($passport->getBadge(RememberMeBadge::class)->isEnabled());
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWhenRememberMeAlwaysIsTrue()
|
||||
{
|
||||
$passport = $this->createPassport();
|
||||
$listener = new CheckRememberMeConditionsListener(['always_remember_me' => true]);
|
||||
|
||||
$this->listener->onSuccessfulLogin($this->createLoginSuccessfulEvent($passport));
|
||||
|
||||
$this->assertTrue($passport->getBadge(RememberMeBadge::class)->isEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideRememberMeOptInValues
|
||||
*/
|
||||
public function testSuccessfulLoginWithOptInRequestParameter($optInValue)
|
||||
{
|
||||
$this->request->request->set('_remember_me', $optInValue);
|
||||
$passport = $this->createPassport();
|
||||
|
||||
$this->listener->onSuccessfulLogin($this->createLoginSuccessfulEvent($passport));
|
||||
|
||||
$this->assertTrue($passport->getBadge(RememberMeBadge::class)->isEnabled());
|
||||
}
|
||||
|
||||
public function provideRememberMeOptInValues()
|
||||
{
|
||||
yield ['true'];
|
||||
yield ['1'];
|
||||
yield ['on'];
|
||||
yield ['yes'];
|
||||
yield [true];
|
||||
}
|
||||
|
||||
private function createLoginSuccessfulEvent(PassportInterface $passport)
|
||||
{
|
||||
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), $this->request, $this->response, 'main_firewall');
|
||||
}
|
||||
|
||||
private function createPassport(array $badges = null)
|
||||
{
|
||||
return new SelfValidatingPassport(new UserBadge('test', function ($username) { return new User($username, null); }), $badges ?? [new RememberMeBadge()]);
|
||||
}
|
||||
}
|
@ -22,71 +22,66 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
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;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeHandlerInterface;
|
||||
|
||||
class RememberMeListenerTest extends TestCase
|
||||
{
|
||||
private $rememberMeServices;
|
||||
private $rememberMeHandler;
|
||||
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->createMock(Request::class);
|
||||
$this->response = $this->createMock(Response::class);
|
||||
$this->token = $this->createMock(TokenInterface::class);
|
||||
$this->rememberMeHandler = $this->createMock(RememberMeHandlerInterface::class);
|
||||
$this->listener = new RememberMeListener($this->rememberMeHandler);
|
||||
$this->request = Request::create('/login');
|
||||
$this->request->request->set('_remember_me', true);
|
||||
$this->response = new Response();
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWithoutSupportingAuthenticator()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->never())->method('loginSuccess');
|
||||
$this->rememberMeHandler->expects($this->never())->method('createRememberMeCookie');
|
||||
|
||||
$event = $this->createLoginSuccessfulEvent('main_firewall', $this->response, new SelfValidatingPassport(new UserBadge('wouter', function ($username) { return new InMemoryUser($username, null); })));
|
||||
$event = $this->createLoginSuccessfulEvent($this->createPassport([]));
|
||||
$this->listener->onSuccessfulLogin($event);
|
||||
}
|
||||
|
||||
public function testSuccessfulLoginWithoutSuccessResponse()
|
||||
public function testSuccessfulLoginWithRememberMeDisabled()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->never())->method('loginSuccess');
|
||||
$this->rememberMeHandler->expects($this->never())->method('createRememberMeCookie');
|
||||
|
||||
$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);
|
||||
$event = $this->createLoginSuccessfulEvent($this->createPassport([new RememberMeBadge()]));
|
||||
$this->listener->onSuccessfulLogin($event);
|
||||
}
|
||||
|
||||
public function testCredentialsInvalid()
|
||||
{
|
||||
$this->rememberMeServices->expects($this->once())->method('loginFail')->with($this->request, $this->isInstanceOf(AuthenticationException::class));
|
||||
$this->rememberMeHandler->expects($this->once())->method('clearRememberMeCookie');
|
||||
|
||||
$event = $this->createLoginFailureEvent('main_firewall');
|
||||
$this->listener->onFailedLogin($event);
|
||||
$this->listener->clearCookie();
|
||||
}
|
||||
|
||||
private function createLoginSuccessfulEvent($firewallName, $response, PassportInterface $passport = null)
|
||||
private function createLoginSuccessfulEvent(PassportInterface $passport = null)
|
||||
{
|
||||
if (null === $passport) {
|
||||
$passport = new SelfValidatingPassport(new UserBadge('test', function ($username) { return new InMemoryUser($username, null); }), [new RememberMeBadge()]);
|
||||
$passport = $this->createPassport();
|
||||
}
|
||||
|
||||
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->token, $this->request, $response, $firewallName);
|
||||
return new LoginSuccessEvent($this->createMock(AuthenticatorInterface::class), $passport, $this->createMock(TokenInterface::class), $this->request, $this->response, 'main_firewall');
|
||||
}
|
||||
|
||||
private function createLoginFailureEvent($firewallName)
|
||||
private function createPassport(array $badges = null)
|
||||
{
|
||||
return new LoginFailureEvent(new AuthenticationException(), $this->createMock(AuthenticatorInterface::class), $this->request, null, $firewallName, null);
|
||||
if (null === $badges) {
|
||||
$badge = new RememberMeBadge();
|
||||
$badge->enable();
|
||||
$badges = [$badge];
|
||||
}
|
||||
|
||||
return new SelfValidatingPassport(new UserBadge('test', function ($username) { return new InMemoryUser($username, null); }), $badges);
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +106,7 @@ class ContextListenerTest extends TestCase
|
||||
$tokenStorage = new TokenStorage();
|
||||
$tokenStorage->setToken(new UsernamePasswordToken('test1', 'pass1', 'phpunit'));
|
||||
$request = new Request();
|
||||
$request->attributes->set('_security_firewall_run', true);
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$request->setSession($session);
|
||||
|
||||
@ -148,22 +149,18 @@ class ContextListenerTest extends TestCase
|
||||
{
|
||||
$tokenStorage = $this->createMock(TokenStorageInterface::class);
|
||||
$event = $this->createMock(RequestEvent::class);
|
||||
$request = $this->createMock(Request::class);
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
|
||||
$event->expects($this->any())
|
||||
->method('getRequest')
|
||||
->willReturn($request);
|
||||
$request->expects($this->any())
|
||||
->method('hasPreviousSession')
|
||||
->willReturn(true);
|
||||
$request->expects($this->any())
|
||||
->method('getSession')
|
||||
->willReturn($session);
|
||||
$session->expects($this->any())->method('getName')->willReturn('SESSIONNAME');
|
||||
$session->expects($this->any())
|
||||
->method('get')
|
||||
->with('_security_key123')
|
||||
->willReturn($token);
|
||||
$request = new Request([], [], [], ['SESSIONNAME' => true]);
|
||||
$request->setSession($session);
|
||||
|
||||
$event->expects($this->any())
|
||||
->method('getRequest')
|
||||
->willReturn($request);
|
||||
$tokenStorage->expects($this->once())
|
||||
->method('setToken')
|
||||
->with(null);
|
||||
@ -196,7 +193,7 @@ class ContextListenerTest extends TestCase
|
||||
->willReturn(true);
|
||||
$event->expects($this->any())
|
||||
->method('getRequest')
|
||||
->willReturn($this->createMock(Request::class));
|
||||
->willReturn(new Request());
|
||||
|
||||
$dispatcher->expects($this->once())
|
||||
->method('addListener')
|
||||
@ -208,18 +205,15 @@ class ContextListenerTest extends TestCase
|
||||
public function testOnKernelResponseListenerRemovesItself()
|
||||
{
|
||||
$session = $this->createMock(SessionInterface::class);
|
||||
$session->expects($this->any())->method('getName')->willReturn('SESSIONNAME');
|
||||
$tokenStorage = $this->createMock(TokenStorageInterface::class);
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
|
||||
$listener = new ContextListener($tokenStorage, [], 'key123', null, $dispatcher);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->expects($this->any())
|
||||
->method('hasSession')
|
||||
->willReturn(true);
|
||||
$request->expects($this->any())
|
||||
->method('getSession')
|
||||
->willReturn($session);
|
||||
$request = new Request();
|
||||
$request->attributes->set('_security_firewall_run', true);
|
||||
$request->setSession($session);
|
||||
|
||||
$event = new ResponseEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST, new Response());
|
||||
|
||||
@ -232,8 +226,7 @@ class ContextListenerTest extends TestCase
|
||||
|
||||
public function testHandleRemovesTokenIfNoPreviousSessionWasFound()
|
||||
{
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->expects($this->any())->method('hasPreviousSession')->willReturn(false);
|
||||
$request = new Request();
|
||||
|
||||
$event = $this->createMock(RequestEvent::class);
|
||||
$event->expects($this->any())->method('getRequest')->willReturn($request);
|
||||
@ -377,6 +370,7 @@ class ContextListenerTest extends TestCase
|
||||
{
|
||||
$session = new Session(new MockArraySessionStorage());
|
||||
$request = new Request();
|
||||
$request->attributes->set('_security_firewall_run', true);
|
||||
$request->setSession($session);
|
||||
$requestStack = new RequestStack();
|
||||
$requestStack->push($request);
|
||||
|
@ -13,17 +13,20 @@ namespace Symfony\Component\Security\Http\Tests\LoginLink;
|
||||
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Routing\RequestContext;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\Signature\ExpiredSignatureStorage;
|
||||
use Symfony\Component\Security\Core\Signature\SignatureHasher;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\LoginLink\Exception\ExpiredLoginLinkException;
|
||||
use Symfony\Component\Security\Http\LoginLink\Exception\InvalidLoginLinkException;
|
||||
use Symfony\Component\Security\Http\LoginLink\ExpiredLoginLinkStorage;
|
||||
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandler;
|
||||
|
||||
class LoginLinkHandlerTest extends TestCase
|
||||
@ -34,15 +37,18 @@ class LoginLinkHandlerTest extends TestCase
|
||||
private $userProvider;
|
||||
/** @var PropertyAccessorInterface */
|
||||
private $propertyAccessor;
|
||||
/** @var MockObject|ExpiredLoginLinkStorage */
|
||||
/** @var MockObject|ExpiredSignatureStorage */
|
||||
private $expiredLinkStorage;
|
||||
/** @var CacheItemPoolInterface */
|
||||
private $expiredLinkCache;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->router = $this->createMock(UrlGeneratorInterface::class);
|
||||
$this->userProvider = new TestLoginLinkHandlerUserProvider();
|
||||
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
|
||||
$this->expiredLinkStorage = $this->createMock(ExpiredLoginLinkStorage::class);
|
||||
$this->expiredLinkCache = new ArrayAdapter();
|
||||
$this->expiredLinkStorage = new ExpiredSignatureStorage($this->expiredLinkCache, 360);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -118,13 +124,12 @@ class LoginLinkHandlerTest extends TestCase
|
||||
$user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash');
|
||||
$this->userProvider->createUser($user);
|
||||
|
||||
$this->expiredLinkStorage->expects($this->once())
|
||||
->method('incrementUsages')
|
||||
->with($signature);
|
||||
|
||||
$linker = $this->createLinker(['max_uses' => 3]);
|
||||
$actualUser = $linker->consumeLoginLink($request);
|
||||
$this->assertEquals($user, $actualUser);
|
||||
|
||||
$item = $this->expiredLinkCache->getItem(rawurlencode($signature));
|
||||
$this->assertSame(1, $item->get());
|
||||
}
|
||||
|
||||
public function testConsumeLoginLinkWithExpired()
|
||||
@ -172,10 +177,9 @@ class LoginLinkHandlerTest extends TestCase
|
||||
$user = new TestLoginLinkHandlerUser('weaverryan', 'ryan@symfonycasts.com', 'pwhash');
|
||||
$this->userProvider->createUser($user);
|
||||
|
||||
$this->expiredLinkStorage->expects($this->once())
|
||||
->method('countUsages')
|
||||
->with($signature)
|
||||
->willReturn(3);
|
||||
$item = $this->expiredLinkCache->getItem(rawurlencode($signature));
|
||||
$item->set(3);
|
||||
$this->expiredLinkCache->save($item);
|
||||
|
||||
$linker = $this->createLinker(['max_uses' => 3]);
|
||||
$linker->consumeLoginLink($request);
|
||||
@ -199,7 +203,7 @@ class LoginLinkHandlerTest extends TestCase
|
||||
'route_name' => 'app_check_login_link_route',
|
||||
], $options);
|
||||
|
||||
return new LoginLinkHandler($this->router, $this->userProvider, $this->propertyAccessor, $extraProperties, 's3cret', $options, $this->expiredLinkStorage);
|
||||
return new LoginLinkHandler($this->router, $this->userProvider, new SignatureHasher($this->propertyAccessor, $extraProperties, 's3cret', $this->expiredLinkStorage, $options['max_uses'] ?? null), $options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,7 +213,7 @@ class TestLoginLinkHandlerUserProvider implements UserProviderInterface
|
||||
|
||||
public function createUser(TestLoginLinkHandlerUser $user): void
|
||||
{
|
||||
$this->users[$user->getUsername()] = $user;
|
||||
$this->users[$user->getUserIdentifier()] = $user;
|
||||
}
|
||||
|
||||
public function loadUserByIdentifier(string $userIdentifier): TestLoginLinkHandlerUser
|
||||
|
@ -0,0 +1,127 @@
|
||||
<?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\RememberMe;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken;
|
||||
use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\CookieTheftException;
|
||||
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
|
||||
use Symfony\Component\Security\Core\User\InMemoryUser;
|
||||
use Symfony\Component\Security\Http\RememberMe\PersistentRememberMeHandler;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeDetails;
|
||||
use Symfony\Component\Security\Http\RememberMe\ResponseListener;
|
||||
|
||||
class PersistentRememberMeHandlerTest extends TestCase
|
||||
{
|
||||
private $tokenProvider;
|
||||
private $userProvider;
|
||||
private $requestStack;
|
||||
private $request;
|
||||
private $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tokenProvider = $this->createMock(TokenProviderInterface::class);
|
||||
$this->userProvider = new InMemoryUserProvider();
|
||||
$this->userProvider->createUser(new InMemoryUser('wouter', null));
|
||||
$this->requestStack = new RequestStack();
|
||||
$this->request = Request::create('/login');
|
||||
$this->requestStack->push($this->request);
|
||||
$this->handler = new PersistentRememberMeHandler($this->tokenProvider, 'secret', $this->userProvider, $this->requestStack, []);
|
||||
}
|
||||
|
||||
public function testCreateRememberMeCookie()
|
||||
{
|
||||
$this->tokenProvider->expects($this->once())
|
||||
->method('createNewToken')
|
||||
->with($this->callback(function ($persistentToken) {
|
||||
return $persistentToken instanceof PersistentToken
|
||||
&& $persistentToken->getUserIdentifier() === 'wouter'
|
||||
&& $persistentToken->getClass() === InMemoryUser::class;
|
||||
}));
|
||||
|
||||
$this->handler->createRememberMeCookie(new InMemoryUser('wouter', null));
|
||||
}
|
||||
|
||||
public function testClearRememberMeCookie()
|
||||
{
|
||||
$this->tokenProvider->expects($this->once())
|
||||
->method('deleteTokenBySeries')
|
||||
->with('series1');
|
||||
|
||||
$this->request->cookies->set('REMEMBERME', (new RememberMeDetails(InMemoryUser::class, 'wouter', 0, 'series1:tokenvalue'))->toString());
|
||||
|
||||
$this->handler->clearRememberMeCookie();
|
||||
|
||||
$this->assertTrue($this->request->attributes->has(ResponseListener::COOKIE_ATTR_NAME));
|
||||
|
||||
/** @var Cookie $cookie */
|
||||
$cookie = $this->request->attributes->get(ResponseListener::COOKIE_ATTR_NAME);
|
||||
$this->assertEquals(null, $cookie->getValue());
|
||||
}
|
||||
|
||||
public function testConsumeRememberMeCookieValid()
|
||||
{
|
||||
$this->tokenProvider->expects($this->any())
|
||||
->method('loadTokenBySeries')
|
||||
->with('series1')
|
||||
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('-10 min')))
|
||||
;
|
||||
|
||||
$this->tokenProvider->expects($this->once())->method('updateToken')->with('series1');
|
||||
|
||||
$rememberMeDetails = new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'series1:tokenvalue');
|
||||
$this->handler->consumeRememberMeCookie($rememberMeDetails);
|
||||
|
||||
// assert that the cookie has been updated with a new base64 encoded token value
|
||||
$this->assertTrue($this->request->attributes->has(ResponseListener::COOKIE_ATTR_NAME));
|
||||
|
||||
/** @var Cookie $cookie */
|
||||
$cookie = $this->request->attributes->get(ResponseListener::COOKIE_ATTR_NAME);
|
||||
$this->assertNotEquals($rememberMeDetails->toString(), $cookie->getValue());
|
||||
$this->assertMatchesRegularExpression('{'.str_replace('\\', '\\\\', base64_decode($rememberMeDetails->withValue('[a-zA-Z0-9/+]+')->toString())).'}', base64_decode($cookie->getValue()));
|
||||
}
|
||||
|
||||
public function testConsumeRememberMeCookieInvalidToken()
|
||||
{
|
||||
$this->expectException(CookieTheftException::class);
|
||||
|
||||
$this->tokenProvider->expects($this->any())
|
||||
->method('loadTokenBySeries')
|
||||
->with('series1')
|
||||
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue1', new \DateTime('-10 min')));
|
||||
|
||||
$this->tokenProvider->expects($this->never())->method('updateToken')->with('series1');
|
||||
|
||||
$this->handler->consumeRememberMeCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'series1:tokenvalue'));
|
||||
}
|
||||
|
||||
public function testConsumeRememberMeCookieExpired()
|
||||
{
|
||||
$this->expectException(AuthenticationException::class);
|
||||
$this->expectExceptionMessage('The cookie has expired.');
|
||||
|
||||
$this->tokenProvider->expects($this->any())
|
||||
->method('loadTokenBySeries')
|
||||
->with('series1')
|
||||
->willReturn(new PersistentToken(InMemoryUser::class, 'wouter', 'series1', 'tokenvalue', new \DateTime('-'.(31536000 - 1).' years')));
|
||||
|
||||
$this->tokenProvider->expects($this->never())->method('updateToken')->with('series1');
|
||||
|
||||
$this->handler->consumeRememberMeCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'series1:tokenvalue'));
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
<?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\RememberMe;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bridge\PhpUnit\ClockMock;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Signature\Exception\ExpiredSignatureException;
|
||||
use Symfony\Component\Security\Core\Signature\Exception\InvalidSignatureException;
|
||||
use Symfony\Component\Security\Core\Signature\SignatureHasher;
|
||||
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
|
||||
use Symfony\Component\Security\Core\User\InMemoryUser;
|
||||
use Symfony\Component\Security\Http\RememberMe\RememberMeDetails;
|
||||
use Symfony\Component\Security\Http\RememberMe\ResponseListener;
|
||||
use Symfony\Component\Security\Http\RememberMe\SignatureRememberMeHandler;
|
||||
|
||||
class SignatureRememberMeHandlerTest extends TestCase
|
||||
{
|
||||
private $signatureHasher;
|
||||
private $userProvider;
|
||||
private $request;
|
||||
private $requestStack;
|
||||
private $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->signatureHasher = $this->createMock(SignatureHasher::class);
|
||||
$this->userProvider = new InMemoryUserProvider();
|
||||
$user = new InMemoryUser('wouter', null);
|
||||
$this->userProvider->createUser($user);
|
||||
$this->requestStack = new RequestStack();
|
||||
$this->request = Request::create('/login');
|
||||
$this->requestStack->push($this->request);
|
||||
$this->handler = new SignatureRememberMeHandler($this->signatureHasher, $this->userProvider, $this->requestStack, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group time-sensitive
|
||||
*/
|
||||
public function testCreateRememberMeCookie()
|
||||
{
|
||||
ClockMock::register(SignatureRememberMeHandler::class);
|
||||
|
||||
$user = new InMemoryUser('wouter', null);
|
||||
$this->signatureHasher->expects($this->once())->method('computeSignatureHash')->with($user, $expire = time() + 31536000)->willReturn('abc');
|
||||
|
||||
$this->handler->createRememberMeCookie($user);
|
||||
|
||||
$this->assertTrue($this->request->attributes->has(ResponseListener::COOKIE_ATTR_NAME));
|
||||
|
||||
/** @var Cookie $cookie */
|
||||
$cookie = $this->request->attributes->get(ResponseListener::COOKIE_ATTR_NAME);
|
||||
$this->assertEquals(base64_encode(InMemoryUser::class.':d291dGVy:'.$expire.':abc'), $cookie->getValue());
|
||||
}
|
||||
|
||||
public function testClearRememberMeCookie()
|
||||
{
|
||||
$this->handler->clearRememberMeCookie();
|
||||
|
||||
$this->assertTrue($this->request->attributes->has(ResponseListener::COOKIE_ATTR_NAME));
|
||||
|
||||
/** @var Cookie $cookie */
|
||||
$cookie = $this->request->attributes->get(ResponseListener::COOKIE_ATTR_NAME);
|
||||
$this->assertEquals(null, $cookie->getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group time-sensitive
|
||||
*/
|
||||
public function testConsumeRememberMeCookieValid()
|
||||
{
|
||||
$this->signatureHasher->expects($this->once())->method('verifySignatureHash')->with($user = new InMemoryUser('wouter', null), 360, 'signature');
|
||||
$this->signatureHasher->expects($this->any())
|
||||
->method('computeSignatureHash')
|
||||
->with($user, $expire = time() + 31536000)
|
||||
->willReturn('newsignature');
|
||||
|
||||
$rememberMeDetails = new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'signature');
|
||||
$this->handler->consumeRememberMeCookie($rememberMeDetails);
|
||||
|
||||
$this->assertTrue($this->request->attributes->has(ResponseListener::COOKIE_ATTR_NAME));
|
||||
|
||||
/** @var Cookie $cookie */
|
||||
$cookie = $this->request->attributes->get(ResponseListener::COOKIE_ATTR_NAME);
|
||||
$this->assertEquals((new RememberMeDetails(InMemoryUser::class, 'wouter', $expire, 'newsignature'))->toString(), $cookie->getValue());
|
||||
}
|
||||
|
||||
public function testConsumeRememberMeCookieInvalidHash()
|
||||
{
|
||||
$this->expectException(AuthenticationException::class);
|
||||
$this->expectExceptionMessage('The cookie\'s hash is invalid.');
|
||||
|
||||
$this->signatureHasher->expects($this->any())
|
||||
->method('verifySignatureHash')
|
||||
->with(new InMemoryUser('wouter', null), 360, 'badsignature')
|
||||
->will($this->throwException(new InvalidSignatureException()));
|
||||
|
||||
$this->handler->consumeRememberMeCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'badsignature'));
|
||||
}
|
||||
|
||||
public function testConsumeRememberMeCookieExpired()
|
||||
{
|
||||
$this->expectException(AuthenticationException::class);
|
||||
$this->expectExceptionMessage('The cookie has expired.');
|
||||
|
||||
$this->signatureHasher->expects($this->any())
|
||||
->method('verifySignatureHash')
|
||||
->with(new InMemoryUser('wouter', null), 360, 'signature')
|
||||
->will($this->throwException(new ExpiredSignatureException()));
|
||||
|
||||
$this->handler->consumeRememberMeCookie(new RememberMeDetails(InMemoryUser::class, 'wouter', 360, 'signature'));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user