Skip to content

Commit

Permalink
Added remember me functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
wouterj committed Apr 20, 2020
1 parent 1c810d5 commit ddf430f
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 105 deletions.
Expand Up @@ -42,7 +42,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider,
return [$providerId, $listenerId, $defaultEntryPoint];
}

public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string
public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string
{
if (null === $config['secret']) {
$config['secret'] = new Parameter('container.build_hash');
Expand Down
Expand Up @@ -25,5 +25,5 @@ interface AuthenticatorFactoryInterface
*
* @return string|string[] The authenticator service ID(s) to be used by the firewall
*/
public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId);
public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId);
}
Expand Up @@ -97,7 +97,7 @@ public function createEntryPoint(ContainerBuilder $container, string $id, array
return $entryPointId;
}

public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string
public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string
{
$authenticatorId = 'security.authenticator.form_login.'.$id;
$defaultOptions = array_merge($this->defaultSuccessHandlerOptions, $this->options);
Expand Down
Expand Up @@ -46,7 +46,7 @@ public function create(ContainerBuilder $container, string $id, array $config, s
return [$provider, $listenerId, $entryPointId];
}

public function createAuthenticator(ContainerBuilder $container, string $id, array $config, ?string $userProviderId): string
public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string
{
$authenticatorId = 'security.authenticator.http_basic.'.$id;
$container
Expand Down
Expand Up @@ -20,7 +20,7 @@
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\Security\Http\EventListener\RememberMeLogoutListener;

class RememberMeFactory implements SecurityFactoryInterface
class RememberMeFactory implements SecurityFactoryInterface, AuthenticatorFactoryInterface
{
protected $options = [
'name' => 'REMEMBERME',
Expand All @@ -46,29 +46,8 @@ public function create(ContainerBuilder $container, string $id, array $config, ?
;

// remember me services
if (isset($config['service'])) {
$templateId = $config['service'];
$rememberMeServicesId = $templateId.'.'.$id;
} elseif (isset($config['token_provider'])) {
$templateId = 'security.authentication.rememberme.services.persistent';
$rememberMeServicesId = $templateId.'.'.$id;
} else {
$templateId = 'security.authentication.rememberme.services.simplehash';
$rememberMeServicesId = $templateId.'.'.$id;
}

$rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId));
$rememberMeServices->replaceArgument(1, $config['secret']);
$rememberMeServices->replaceArgument(2, $id);

if (isset($config['token_provider'])) {
$rememberMeServices->addMethodCall('setTokenProvider', [
new Reference($config['token_provider']),
]);
}

// remember-me options
$rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options));
$templateId = $this->generateRememberMeServicesTemplateId($config, $id);
$rememberMeServicesId = $templateId.'.'.$id;

// attach to remember-me aware listeners
$userProviders = [];
Expand All @@ -93,17 +72,8 @@ public function create(ContainerBuilder $container, string $id, array $config, ?
;
}
}
if ($config['user_providers']) {
$userProviders = [];
foreach ($config['user_providers'] as $providerName) {
$userProviders[] = new Reference('security.user.provider.concrete.'.$providerName);
}
}
if (0 === \count($userProviders)) {
throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.');
}

$rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders)));
$this->createRememberMeServices($container, $id, $templateId, $userProviders, $config);

// remember-me listener
$listenerId = 'security.authentication.listener.rememberme.'.$id;
Expand All @@ -119,6 +89,42 @@ public function create(ContainerBuilder $container, string $id, array $config, ?
return [$authProviderId, $listenerId, $defaultEntryPoint];
}

public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId): string
{
$templateId = $this->generateRememberMeServicesTemplateId($config, $id);
$rememberMeServicesId = $templateId.'.'.$id;

// create remember me services (which manage the remember me cookies)
$this->createRememberMeServices($container, $id, $templateId, [new Reference($userProviderId)], $config);

// create remember me listener (which executes the remember me services for other authenticators and logout)
$this->createRememberMeListener($container, $id, $rememberMeServicesId);

// create remember me authenticator (which re-authenticates the user based on the remember me cookie)
$authenticatorId = 'security.authenticator.remember_me.'.$id;
$container
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.remember_me'))
->replaceArgument(0, new Reference($rememberMeServicesId))
->replaceArgument(3, array_intersect_key($config, $this->options))
;

foreach ($container->findTaggedServiceIds('security.remember_me_aware') as $serviceId => $attributes) {
// register ContextListener
if ('security.context_listener' === substr($serviceId, 0, 25)) {
$container
->getDefinition($serviceId)
->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)])
;

continue;
}

throw new \LogicException(sprintf('Symfony Authenticator Security dropped support for the "security.remember_me_aware" tag, service "%s" will no longer work as expected.', $serviceId));
}

return $authenticatorId;
}

public function getPosition()
{
return 'remember_me';
Expand Down Expand Up @@ -163,4 +169,63 @@ public function addConfiguration(NodeDefinition $node)
}
}
}

private function generateRememberMeServicesTemplateId(array $config, string $id): string
{
if (isset($config['service'])) {
return $config['service'];
}

if (isset($config['token_provider'])) {
return 'security.authentication.rememberme.services.persistent';
}

return 'security.authentication.rememberme.services.simplehash';
}

private function createRememberMeServices(ContainerBuilder $container, string $id, string $templateId, array $userProviders, array $config): void
{
$rememberMeServicesId = $templateId.'.'.$id;

$rememberMeServices = $container->setDefinition($rememberMeServicesId, new ChildDefinition($templateId));
$rememberMeServices->replaceArgument(1, $config['secret']);
$rememberMeServices->replaceArgument(2, $id);

if (isset($config['token_provider'])) {
$rememberMeServices->addMethodCall('setTokenProvider', [
new Reference($config['token_provider']),
]);
}

// remember-me options
$rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options));

if ($config['user_providers']) {
$userProviders = [];
foreach ($config['user_providers'] as $providerName) {
$userProviders[] = new Reference('security.user.provider.concrete.'.$providerName);
}
}

if (0 === \count($userProviders)) {
throw new \RuntimeException('You must configure at least one remember-me aware listener (such as form-login) for each firewall that has remember-me enabled.');
}

$rememberMeServices->replaceArgument(0, new IteratorArgument(array_unique($userProviders)));
}

private function createRememberMeListener(ContainerBuilder $container, string $id, string $rememberMeServicesId): void
{
$container
->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me'))
->addTag('kernel.event_subscriber')
->replaceArgument(0, new Reference($rememberMeServicesId))
->replaceArgument(1, $id)
;

$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));
}
}
Expand Up @@ -26,6 +26,7 @@
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
Expand All @@ -34,6 +35,7 @@
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
use Symfony\Component\Security\Core\User\ChainUserProvider;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Controller\UserValueResolver;
use Twig\Extension\AbstractExtension;
Expand Down Expand Up @@ -230,9 +232,16 @@ private function createFirewalls(array $config, ContainerBuilder $container)
foreach ($providerIds as $userProviderId) {
$userProviders[] = new Reference($userProviderId);
}
$arguments[1] = new IteratorArgument($userProviders);
$arguments[1] = $userProviderIteratorsArgument = new IteratorArgument($userProviders);
$contextListenerDefinition->setArguments($arguments);

if (\count($userProviders) > 1) {
$container->setDefinition('security.user_providers', new Definition(ChainUserProvider::class, [$userProviderIteratorsArgument]))
->setPublic(false);
} else {
$container->setAlias('security.user_providers', new Alias(current($providerIds)))->setPublic(false);
}

if (1 === \count($providerIds)) {
$container->setAlias(UserProviderInterface::class, current($providerIds));
}
Expand Down Expand Up @@ -423,16 +432,6 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
// Determine default entry point
$configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null;

if ($this->authenticatorManagerEnabled) {
// Remember me listener (must be before calling createAuthenticationListeners() to inject remember me services)
$container
->setDefinition('security.listener.remember_me.'.$id, new ChildDefinition('security.listener.remember_me'))
->replaceArgument(0, $id)
->addTag('kernel.event_subscriber')
->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none'])
;
}

// Authentication listeners
$firewallAuthenticationProviders = [];
list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $firewallAuthenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId);
Expand Down Expand Up @@ -554,7 +553,7 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
return [$listeners, $defaultEntryPoint];
}

private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): ?string
private function getUserProvider(ContainerBuilder $container, string $id, array $firewall, string $factoryKey, ?string $defaultProvider, array $providerIds, ?string $contextListenerId): string
{
if (isset($firewall[$factoryKey]['provider'])) {
if (!isset($providerIds[$normalizedName = str_replace('-', '_', $firewall[$factoryKey]['provider'])])) {
Expand All @@ -564,13 +563,8 @@ private function getUserProvider(ContainerBuilder $container, string $id, array
return $providerIds[$normalizedName];
}

if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) {
if ('remember_me' === $factoryKey && $contextListenerId) {
$container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']);
}

// RememberMeFactory will use the firewall secret when created
return null;
if ('remember_me' === $factoryKey && $contextListenerId) {
$container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']);
}

if ($defaultProvider) {
Expand All @@ -587,6 +581,10 @@ private function getUserProvider(ContainerBuilder $container, string $id, array
return $userProvider;
}

if ('remember_me' === $factoryKey || 'anonymous' === $factoryKey) {
return 'security.user_providers';
}

throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $factoryKey, $id));
}

Expand Down
Expand Up @@ -52,7 +52,8 @@
class="Symfony\Component\Security\Http\EventListener\RememberMeListener"
abstract="true">
<tag name="monolog.logger" channel="security" />
<argument/> <!-- provider key -->
<argument type="abstract">remember me services</argument>
<argument type="abstract">provider key</argument>
<argument type="service" id="logger" on-invalid="null" />
</service>

Expand Down Expand Up @@ -82,5 +83,15 @@
<argument type="abstract">secret</argument>
<argument type="service" id="security.untracked_token_storage" />
</service>

<service id="security.authenticator.remember_me"
class="Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator"
abstract="true">
<argument type="abstract">remember me services</argument>
<argument>%kernel.secret%</argument>
<argument type="service" id="security.token_storage" />
<argument type="abstract">options</argument>
<argument type="service" id="security.authentication.session_strategy" />
</service>
</services>
</container>
Expand Up @@ -25,7 +25,7 @@
*
* @experimental in 5.1
*/
abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
abstract class AbstractLoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface, RememberMeAuthenticatorInterface
{
/**
* Return the URL to the login page.
Expand All @@ -46,11 +46,6 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio
return new RedirectResponse($url);
}

public function supportsRememberMe(): bool
{
return true;
}

/**
* Override to control what happens when the user hits a secure page
* but isn't logged in yet.
Expand All @@ -61,4 +56,9 @@ public function start(Request $request, AuthenticationException $authException =

return new RedirectResponse($url);
}

public function supportsRememberMe(): bool
{
return true;
}
}
Expand Up @@ -75,9 +75,4 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token,
{
return null;
}

public function supportsRememberMe(): bool
{
return false;
}
}
Expand Up @@ -102,18 +102,4 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio
* will be authenticated. This makes sense, for example, with an API.
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response;

/**
* Does this method support remember me cookies?
*
* Remember me cookie will be set if *all* of the following are met:
* A) This method returns true
* 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 onAuthenticationSuccess method returns a Response object
*/
public function supportsRememberMe(): bool;
}
Expand Up @@ -94,9 +94,4 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio

return $this->start($request, $exception);
}

public function supportsRememberMe(): bool
{
return false;
}
}

0 comments on commit ddf430f

Please sign in to comment.