Merge branch '3.4' into 4.3

* 3.4:
  [Security] Fix clearing remember-me cookie after deauthentication
  more robust initialization from request
This commit is contained in:
Nicolas Grekas 2019-11-30 14:16:45 +01:00
commit cad14177dc
10 changed files with 180 additions and 9 deletions

View File

@ -83,7 +83,11 @@ class RememberMeFactory implements SecurityFactoryInterface
throw new \RuntimeException('Each "security.remember_me_aware" tag must have a provider attribute.');
}
$userProviders[] = new Reference($attribute['provider']);
// context listeners don't need a provider
if ('none' !== $attribute['provider']) {
$userProviders[] = new Reference($attribute['provider']);
}
$container
->getDefinition($serviceId)
->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)])

View File

@ -321,10 +321,11 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
$listeners[] = new Reference('security.channel_listener');
$contextKey = null;
$contextListenerId = null;
// Context serializer listener
if (false === $firewall['stateless']) {
$contextKey = $firewall['context'] ?? $id;
$listeners[] = new Reference($this->createContextListener($container, $contextKey));
$listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey));
$sessionStrategyId = 'security.authentication.session_strategy';
} else {
$this->statelessFirewallKeys[] = $id;
@ -397,7 +398,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
$configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null;
// Authentication listeners
list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint);
list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId);
$config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint);
@ -452,7 +453,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
return $this->contextListeners[$contextKey] = $listenerId;
}
private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider = null, array $providerIds, $defaultEntryPoint)
private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider = null, array $providerIds, $defaultEntryPoint, $contextListenerId = null)
{
$listeners = [];
$hasListeners = false;
@ -470,6 +471,10 @@ class SecurityExtension extends Extension implements PrependExtensionInterface
} elseif ('remember_me' === $key) {
// RememberMeFactory will use the firewall secret when created
$userProvider = null;
if ($contextListenerId) {
$container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']);
}
} elseif ($defaultProvider) {
$userProvider = $defaultProvider;
} elseif (empty($providerIds)) {

View File

@ -0,0 +1,77 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\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
{
public function testUserChangeClearsCookie()
{
$client = $this->createClient(['test_case' => 'ClearRememberMe', 'root_config' => 'config.yml']);
$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->assertSame(200, $client->getResponse()->getStatusCode());
$this->assertNull($cookieJar->get('REMEMBERME'));
}
}
class RememberMeFooController
{
public function __invoke(UserInterface $user)
{
return new Response($user->getUsername());
}
}
class RememberMeUserProvider implements UserProviderInterface
{
private $inner;
public function __construct(InMemoryUserProvider $inner)
{
$this->inner = $inner;
}
public function loadUserByUsername($username)
{
return $this->inner->loadUserByUsername($username);
}
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);
}
}

View File

@ -0,0 +1,18 @@
<?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.
*/
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\SecurityBundle\SecurityBundle;
return [
new FrameworkBundle(),
new SecurityBundle(),
];

View File

@ -0,0 +1,31 @@
imports:
- { resource: ./../config/framework.yml }
security:
encoders:
Symfony\Component\Security\Core\User\User: 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
anonymous: ~
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']

View File

@ -0,0 +1,7 @@
login:
path: /login
foo:
path: /foo
defaults:
_controller: Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeFooController

View File

@ -24,7 +24,7 @@
"symfony/security-core": "~4.3",
"symfony/security-csrf": "~4.2",
"symfony/security-guard": "~4.2",
"symfony/security-http": "^4.3.8"
"symfony/security-http": "~4.3.9|^4.4.1"
},
"require-dev": {
"symfony/asset": "~3.4|~4.0",

View File

@ -57,8 +57,8 @@ class RequestContext
$this->setMethod($request->getMethod());
$this->setHost($request->getHost());
$this->setScheme($request->getScheme());
$this->setHttpPort($request->isSecure() ? $this->httpPort : $request->getPort());
$this->setHttpsPort($request->isSecure() ? $request->getPort() : $this->httpsPort);
$this->setHttpPort($request->isSecure() || null === $request->getPort() ? $this->httpPort : $request->getPort());
$this->setHttpsPort($request->isSecure() && null !== $request->getPort() ? $request->getPort() : $this->httpsPort);
$this->setQueryString($request->server->get('QUERY_STRING', ''));
return $this;

View File

@ -30,6 +30,7 @@ use Symfony\Component\Security\Core\Role\SwitchUserRole;
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\RememberMe\RememberMeServicesInterface;
/**
* ContextListener manages the SecurityContext persistence through a session.
@ -50,6 +51,7 @@ class ContextListener implements ListenerInterface
private $dispatcher;
private $registered;
private $trustResolver;
private $rememberMeServices;
/**
* @param iterable|UserProviderInterface[] $userProviders
@ -110,6 +112,10 @@ class ContextListener implements ListenerInterface
if ($token instanceof TokenInterface) {
$token = $this->refreshUser($token);
if (!$token && $this->rememberMeServices) {
$this->rememberMeServices->loginFail($request);
}
} elseif (null !== $token) {
if (null !== $this->logger) {
$this->logger->warning('Expected a security token from the session, got something else.', ['key' => $this->sessionKey, 'received' => $token]);
@ -278,4 +284,9 @@ class ContextListener implements ListenerInterface
{
throw new \ErrorException('Class not found: '.$class, 0x37313bc);
}
public function setRememberMeServices(RememberMeServicesInterface $rememberMeServices)
{
$this->rememberMeServices = $rememberMeServices;
}
}

View File

@ -33,6 +33,7 @@ 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\Firewall\ContextListener;
use Symfony\Component\Security\Http\RememberMe\RememberMeServicesInterface;
class ContextListenerTest extends TestCase
{
@ -263,10 +264,23 @@ class ContextListenerTest extends TestCase
$tokenStorage = new TokenStorage();
$badRefreshedUser = new User('foobar', 'baz');
$goodRefreshedUser = new User('foobar', 'bar');
$this->handleEventWithPreviousSession($tokenStorage, [new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser, true);
$this->handleEventWithPreviousSession($tokenStorage, [new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser);
$this->assertSame($goodRefreshedUser, $tokenStorage->getToken()->getUser());
}
public function testRememberMeGetsCanceledIfTokenIsDeauthenticated()
{
$tokenStorage = new TokenStorage();
$refreshedUser = new User('foobar', 'baz');
$rememberMeServices = $this->createMock(RememberMeServicesInterface::class);
$rememberMeServices->expects($this->once())->method('loginFail');
$this->handleEventWithPreviousSession($tokenStorage, [new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)], null, $rememberMeServices);
$this->assertNull($tokenStorage->getToken());
}
public function testTryAllUserProvidersUntilASupportingUserProviderIsFound()
{
$tokenStorage = new TokenStorage();
@ -363,7 +377,7 @@ class ContextListenerTest extends TestCase
return $session;
}
private function handleEventWithPreviousSession(TokenStorageInterface $tokenStorage, $userProviders, UserInterface $user = null)
private function handleEventWithPreviousSession(TokenStorageInterface $tokenStorage, $userProviders, UserInterface $user = null, RememberMeServicesInterface $rememberMeServices = null)
{
$user = $user ?: new User('foo', 'bar');
$session = new Session(new MockArraySessionStorage());
@ -374,6 +388,10 @@ class ContextListenerTest extends TestCase
$request->cookies->set('MOCKSESSID', true);
$listener = new ContextListener($tokenStorage, $userProviders, 'context_key');
if ($rememberMeServices) {
$listener->setRememberMeServices($rememberMeServices);
}
$listener(new RequestEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST));
}
}