feature #38616 [FrameworkBundle][HttpFoundation][Security] Deprecate service "session" (jderusse)

This PR was merged into the 5.3-dev branch.

Discussion
----------

[FrameworkBundle][HttpFoundation][Security] Deprecate service "session"

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | no
| Deprecations? | yes
| Tickets       | Fix #10557 and Fix #12839
| License       | MIT
| Doc PR        | TODO

This is a attempt to deprecate service `session` and `SessionInterface`.

This PR replaces the `session` service by a `.session.do-not-use` service (used internally by Symfony) and make `session` a deprecated alias.
In Symfony 6.0 we can remove the `session` service and replace the `SessionListener` by a Factory that build the session (instead of fetching it from container)

This PR also add a short cut `RequestStack::getSession(): ?SessionInterface`

For backward compatibility the `SessionListener` is replaced by `FactorySessionListener` **only when** the user don't override the service `session` (ping @wouterj )

TODO:
- [x] Test many configuration and dependencies (ie. session disabled + csrf)
- [x] ChangeLog and Upgrade
- [x] fix tests

Commits
-------

54acc00769 Deprecat service "session"
This commit is contained in:
Nicolas Grekas 2021-01-28 17:45:48 +01:00
commit f0c3bc9f32
37 changed files with 431 additions and 77 deletions

View File

@ -23,6 +23,11 @@ Form
* Deprecated passing an array as the second argument of the `RadioListMapper::mapDataToForms()` method, pass `\Traversable` instead * Deprecated passing an array as the second argument of the `RadioListMapper::mapDataToForms()` method, pass `\Traversable` instead
* Deprecated passing an array as the first argument of the `RadioListMapper::mapFormsToData()` method, pass `\Traversable` instead * Deprecated passing an array as the first argument of the `RadioListMapper::mapFormsToData()` method, pass `\Traversable` instead
FrameworkBundle
---------------
* Deprecate the `session` service and the `SessionInterface` alias, use the `\Symfony\Component\HttpFoundation\Request::getSession()` or the new `\Symfony\Component\HttpFoundation\RequestStack::getSession()` methods instead
HttpFoundation HttpFoundation
-------------- --------------

View File

@ -69,6 +69,7 @@ Form
FrameworkBundle FrameworkBundle
--------------- ---------------
* Remove the `session` service and the `SessionInterface` alias, use the `\Symfony\Component\HttpFoundation\Request::getSession()` or the new `\Symfony\Component\HttpFoundation\RequestStack::getSession()` methods instead
* `MicroKernelTrait::configureRoutes()` is now always called with a `RoutingConfigurator` * `MicroKernelTrait::configureRoutes()` is now always called with a `RoutingConfigurator`
* The "framework.router.utf8" configuration option defaults to `true` * The "framework.router.utf8" configuration option defaults to `true`
* Removed `session.attribute_bag` service and `session.flash_bag` service. * Removed `session.attribute_bag` service and `session.flash_bag` service.
@ -165,6 +166,9 @@ Routing
Security Security
-------- --------
* Drop support for `SessionInterface $session` as constructor argument of `SessionTokenStorage`, inject a `\Symfony\Component\HttpFoundation\RequestStack $requestStack` instead
* Drop support for `session` provided by the ServiceLocator injected in `UsageTrackingTokenStorage`, provide a `request_stack` service instead
* Make `SessionTokenStorage` throw a `SessionNotFoundException` when called outside a request context
* Removed `ROLE_PREVIOUS_ADMIN` role in favor of `IS_IMPERSONATOR` attribute * Removed `ROLE_PREVIOUS_ADMIN` role in favor of `IS_IMPERSONATOR` attribute
* Removed `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead. * Removed `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead.
* Removed `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`. * Removed `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`.

View File

@ -4,6 +4,7 @@ CHANGELOG
5.3 5.3
--- ---
* Deprecate the `session` service and the `SessionInterface` alias, use the `Request::getSession()` or the new `RequestStack::getSession()` methods instead
* Added `AbstractController::renderForm()` to render a form and set the appropriate HTTP status code * Added `AbstractController::renderForm()` to render a form and set the appropriate HTTP status code
* Added support for configuring PHP error level to log levels * Added support for configuring PHP error level to log levels
* Added the `dispatcher` option to `debug:event-dispatcher` * Added the `dispatcher` option to `debug:event-dispatcher`

View File

@ -22,6 +22,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -200,11 +201,11 @@ abstract class AbstractController implements ServiceSubscriberInterface
*/ */
protected function addFlash(string $type, $message): void protected function addFlash(string $type, $message): void
{ {
if (!$this->container->has('session')) { try {
throw new \LogicException('You can not use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".'); $this->container->get('request_stack')->getSession()->getFlashBag()->add($type, $message);
} catch (SessionNotFoundException $e) {
throw new \LogicException('You can not use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".', 0, $e);
} }
$this->container->get('session')->getFlashBag()->add($type, $message);
} }
/** /**

View File

@ -22,21 +22,41 @@ class SessionPass implements CompilerPassInterface
{ {
public function process(ContainerBuilder $container) public function process(ContainerBuilder $container)
{ {
if (!$container->hasDefinition('session')) { if (!$container->has('session.storage')) {
return; return;
} }
// BC layer: Make "session" an alias of ".session.do-not-use" when not overriden by the user
if (!$container->has('session')) {
$alias = $container->setAlias('session', '.session.do-not-use');
$alias->setDeprecated('symfony/framework-bundle', '5.3', 'The "%alias_id%" service is deprecated, use "$requestStack->getSession()" instead.');
return;
}
if ($container->hasDefinition('session')) {
$definition = $container->getDefinition('session');
$definition->setDeprecated('symfony/framework-bundle', '5.3', 'The "%service_id%" service is deprecated, use "$requestStack->getSession()" instead.');
} else {
$alias = $container->getAlias('session');
$alias->setDeprecated('symfony/framework-bundle', '5.3', 'The "%alias_id%" alias is deprecated, use "$requestStack->getSession()" instead.');
$definition = $container->findDefinition('session');
}
// Convert internal service `.session.do-not-use` into alias of `session`.
$container->setAlias('.session.do-not-use', 'session');
$bags = [ $bags = [
'session.flash_bag' => $container->hasDefinition('session.flash_bag') ? $container->getDefinition('session.flash_bag') : null, 'session.flash_bag' => $container->hasDefinition('session.flash_bag') ? $container->getDefinition('session.flash_bag') : null,
'session.attribute_bag' => $container->hasDefinition('session.attribute_bag') ? $container->getDefinition('session.attribute_bag') : null, 'session.attribute_bag' => $container->hasDefinition('session.attribute_bag') ? $container->getDefinition('session.attribute_bag') : null,
]; ];
foreach ($container->getDefinition('session')->getArguments() as $v) { foreach ($definition->getArguments() as $v) {
if (!$v instanceof Reference || !isset($bags[$bag = (string) $v]) || !\is_array($factory = $bags[$bag]->getFactory())) { if (!$v instanceof Reference || !isset($bags[$bag = (string) $v]) || !\is_array($factory = $bags[$bag]->getFactory())) {
continue; continue;
} }
if ([0, 1] !== array_keys($factory) || !$factory[0] instanceof Reference || 'session' !== (string) $factory[0]) { if ([0, 1] !== array_keys($factory) || !$factory[0] instanceof Reference || !\in_array((string) $factory[0], ['session', '.session.do-not-use'], true)) {
continue; continue;
} }

View File

@ -18,6 +18,7 @@ use Symfony\Component\BrowserKit\History;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\HttpKernelBrowser; use Symfony\Component\HttpKernel\HttpKernelBrowser;
use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\HttpKernel\Profiler\Profile as HttpProfile; use Symfony\Component\HttpKernel\Profiler\Profile as HttpProfile;
@ -122,7 +123,7 @@ class KernelBrowser extends HttpKernelBrowser
$token = new TestBrowserToken($user->getRoles(), $user); $token = new TestBrowserToken($user->getRoles(), $user);
$token->setAuthenticated(true); $token->setAuthenticated(true);
$session = $this->getContainer()->get('session'); $session = new Session($this->getContainer()->get('test.service_container')->get('session.storage'));
$session->set('_security_'.$firewallContext, serialize($token)); $session->set('_security_'.$firewallContext, serialize($token));
$session->save(); $session->save();

View File

@ -27,7 +27,7 @@ return static function (ContainerConfigurator $container) {
->alias(TokenGeneratorInterface::class, 'security.csrf.token_generator') ->alias(TokenGeneratorInterface::class, 'security.csrf.token_generator')
->set('security.csrf.token_storage', SessionTokenStorage::class) ->set('security.csrf.token_storage', SessionTokenStorage::class)
->args([service('session')]) ->args([service('request_stack')])
->alias(TokenStorageInterface::class, 'security.csrf.token_storage') ->alias(TokenStorageInterface::class, 'security.csrf.token_storage')

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator; namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Bundle\FrameworkBundle\Session\DeprecatedSessionFactory;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
@ -33,15 +34,17 @@ return static function (ContainerConfigurator $container) {
$container->parameters()->set('session.metadata.storage_key', '_sf2_meta'); $container->parameters()->set('session.metadata.storage_key', '_sf2_meta');
$container->services() $container->services()
->set('session', Session::class) ->set('.session.do-not-use', Session::class) // to be removed in 6.0
->public()
->args([ ->args([
service('session.storage'), service('session.storage'),
null, // AttributeBagInterface null, // AttributeBagInterface
null, // FlashBagInterface null, // FlashBagInterface
[service('session_listener'), 'onSessionUsage'], [service('session_listener'), 'onSessionUsage'],
]) ])
->alias(SessionInterface::class, 'session') ->set('.session.deprecated', SessionInterface::class) // to be removed in 6.0
->factory([inline_service(DeprecatedSessionFactory::class)->args([service('request_stack')]), 'getSession'])
->alias(SessionInterface::class, '.session.do-not-use')
->deprecate('symfony/framework-bundle', '5.3', 'The "%alias_id%" alias is deprecated, use "$requestStack->getSession()" instead.')
->alias(SessionStorageInterface::class, 'session.storage') ->alias(SessionStorageInterface::class, 'session.storage')
->alias(\SessionHandlerInterface::class, 'session.handler') ->alias(\SessionHandlerInterface::class, 'session.handler')
@ -65,12 +68,12 @@ return static function (ContainerConfigurator $container) {
]) ])
->set('session.flash_bag', FlashBag::class) ->set('session.flash_bag', FlashBag::class)
->factory([service('session'), 'getFlashBag']) ->factory([service('.session.do-not-use'), 'getFlashBag'])
->deprecate('symfony/framework-bundle', '5.1', 'The "%service_id%" service is deprecated, use "$session->getFlashBag()" instead.') ->deprecate('symfony/framework-bundle', '5.1', 'The "%service_id%" service is deprecated, use "$session->getFlashBag()" instead.')
->alias(FlashBagInterface::class, 'session.flash_bag') ->alias(FlashBagInterface::class, 'session.flash_bag')
->set('session.attribute_bag', AttributeBag::class) ->set('session.attribute_bag', AttributeBag::class)
->factory([service('session'), 'getBag']) ->factory([service('.session.do-not-use'), 'getBag'])
->args(['attributes']) ->args(['attributes'])
->deprecate('symfony/framework-bundle', '5.1', 'The "%service_id%" service is deprecated, use "$session->getAttributeBag()" instead.') ->deprecate('symfony/framework-bundle', '5.1', 'The "%service_id%" service is deprecated, use "$session->getAttributeBag()" instead.')
@ -94,8 +97,8 @@ return static function (ContainerConfigurator $container) {
->set('session_listener', SessionListener::class) ->set('session_listener', SessionListener::class)
->args([ ->args([
service_locator([ service_locator([
'session' => service('session')->ignoreOnInvalid(), 'session' => service('.session.do-not-use')->ignoreOnInvalid(),
'initialized_session' => service('session')->ignoreOnUninitialized(), 'initialized_session' => service('.session.do-not-use')->ignoreOnUninitialized(),
'logger' => service('logger')->ignoreOnInvalid(), 'logger' => service('logger')->ignoreOnInvalid(),
'session_collector' => service('data_collector.request.session_collector')->ignoreOnInvalid(), 'session_collector' => service('data_collector.request.session_collector')->ignoreOnInvalid(),
]), ]),

View File

@ -38,7 +38,7 @@ return static function (ContainerConfigurator $container) {
->set('test.session.listener', TestSessionListener::class) ->set('test.session.listener', TestSessionListener::class)
->args([ ->args([
service_locator([ service_locator([
'session' => service('session')->ignoreOnInvalid(), 'session' => service('.session.do-not-use')->ignoreOnInvalid(),
]), ]),
]) ])
->tag('kernel.event_subscriber') ->tag('kernel.event_subscriber')

View File

@ -0,0 +1,46 @@
<?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\FrameworkBundle\Session;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Provides session and trigger deprecation.
*
* Used by service that should trigger deprecation when accessed by the user.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @internal to be removed in 6.0
*/
class DeprecatedSessionFactory
{
private $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function getSession(): ?SessionInterface
{
trigger_deprecation('symfony/framework-bundle', '5.3', 'The "session" service is deprecated, use "$requestStack->getSession()" instead.');
try {
return $this->requestStack->getSession();
} catch (SessionNotFoundException $e) {
return null;
}
}
}

View File

@ -497,8 +497,14 @@ class AbstractControllerTest extends TestCase
$session = $this->createMock(Session::class); $session = $this->createMock(Session::class);
$session->expects($this->once())->method('getFlashBag')->willReturn($flashBag); $session->expects($this->once())->method('getFlashBag')->willReturn($flashBag);
$request = new Request();
$request->setSession($session);
$requestStack = new RequestStack();
$requestStack->push($request);
$container = new Container(); $container = new Container();
$container->set('session', $session); $container->set('session', $session);
$container->set('request_stack', $requestStack);
$controller = $this->createController(); $controller = $this->createController();
$controller->setContainer($container); $controller->setContainer($container);

View File

@ -19,26 +19,77 @@ use Symfony\Component\DependencyInjection\Reference;
class SessionPassTest extends TestCase class SessionPassTest extends TestCase
{ {
public function testProcess() public function testProcess()
{
$container = new ContainerBuilder();
$container
->register('session.storage'); // marker service
$container
->register('.session.do-not-use');
(new SessionPass())->process($container);
$this->assertTrue($container->hasAlias('session'));
$this->assertSame($container->findDefinition('session'), $container->getDefinition('.session.do-not-use'));
$this->assertTrue($container->getAlias('session')->isDeprecated());
}
public function testProcessUserDefinedSession()
{ {
$arguments = [ $arguments = [
new Reference('session.flash_bag'), new Reference('session.flash_bag'),
new Reference('session.attribute_bag'), new Reference('session.attribute_bag'),
]; ];
$container = new ContainerBuilder(); $container = new ContainerBuilder();
$container
->register('session.storage'); // marker service
$container $container
->register('session') ->register('session')
->setArguments($arguments); ->setArguments($arguments);
$container $container
->register('session.flash_bag') ->register('session.flash_bag')
->setFactory([new Reference('session'), 'getFlashBag']); ->setFactory([new Reference('.session.do-not-use'), 'getFlashBag']);
$container $container
->register('session.attribute_bag') ->register('session.attribute_bag')
->setFactory([new Reference('session'), 'getAttributeBag']); ->setFactory([new Reference('.session.do-not-use'), 'getAttributeBag']);
(new SessionPass())->process($container); (new SessionPass())->process($container);
$this->assertSame($arguments, $container->getDefinition('session')->getArguments()); $this->assertSame($arguments, $container->getDefinition('session')->getArguments());
$this->assertNull($container->getDefinition('session.flash_bag')->getFactory()); $this->assertNull($container->getDefinition('session.flash_bag')->getFactory());
$this->assertNull($container->getDefinition('session.attribute_bag')->getFactory()); $this->assertNull($container->getDefinition('session.attribute_bag')->getFactory());
$this->assertTrue($container->hasAlias('.session.do-not-use'));
$this->assertSame($container->getDefinition('session'), $container->findDefinition('.session.do-not-use'));
$this->assertTrue($container->getDefinition('session')->isDeprecated());
}
public function testProcessUserDefinedAlias()
{
$arguments = [
new Reference('session.flash_bag'),
new Reference('session.attribute_bag'),
];
$container = new ContainerBuilder();
$container
->register('session.storage'); // marker service
$container
->register('trueSession')
->setArguments($arguments);
$container
->setAlias('session', 'trueSession');
$container
->register('session.flash_bag')
->setFactory([new Reference('.session.do-not-use'), 'getFlashBag']);
$container
->register('session.attribute_bag')
->setFactory([new Reference('.session.do-not-use'), 'getAttributeBag']);
(new SessionPass())->process($container);
$this->assertSame($arguments, $container->findDefinition('session')->getArguments());
$this->assertNull($container->getDefinition('session.flash_bag')->getFactory());
$this->assertNull($container->getDefinition('session.attribute_bag')->getFactory());
$this->assertTrue($container->hasAlias('.session.do-not-use'));
$this->assertSame($container->findDefinition('session'), $container->findDefinition('.session.do-not-use'));
$this->assertTrue($container->getAlias('session')->isDeprecated());
} }
} }

View File

@ -46,6 +46,7 @@ use Symfony\Component\Form\Form;
use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\RetryableHttpClient; use Symfony\Component\HttpClient\RetryableHttpClient;
use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpClient\ScopingHttpClient;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass;
use Symfony\Component\Messenger\Transport\TransportFactory; use Symfony\Component\Messenger\Transport\TransportFactory;
use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\PropertyAccess\PropertyAccessor;
@ -541,7 +542,7 @@ abstract class FrameworkExtensionTest extends TestCase
{ {
$container = $this->createContainerFromFile('full'); $container = $this->createContainerFromFile('full');
$this->assertTrue($container->hasDefinition('session'), '->registerSessionConfiguration() loads session.xml'); $this->assertTrue($container->hasAlias(SessionInterface::class), '->registerSessionConfiguration() loads session.xml');
$this->assertEquals('fr', $container->getParameter('kernel.default_locale')); $this->assertEquals('fr', $container->getParameter('kernel.default_locale'));
$this->assertEquals('session.storage.native', (string) $container->getAlias('session.storage')); $this->assertEquals('session.storage.native', (string) $container->getAlias('session.storage'));
$this->assertEquals('session.handler.native_file', (string) $container->getAlias('session.handler')); $this->assertEquals('session.handler.native_file', (string) $container->getAlias('session.handler'));
@ -567,7 +568,7 @@ abstract class FrameworkExtensionTest extends TestCase
{ {
$container = $this->createContainerFromFile('session'); $container = $this->createContainerFromFile('session');
$this->assertTrue($container->hasDefinition('session'), '->registerSessionConfiguration() loads session.xml'); $this->assertTrue($container->hasAlias(SessionInterface::class), '->registerSessionConfiguration() loads session.xml');
$this->assertNull($container->getDefinition('session.storage.native')->getArgument(1)); $this->assertNull($container->getDefinition('session.storage.native')->getArgument(1));
$this->assertNull($container->getDefinition('session.storage.php_bridge')->getArgument(0)); $this->assertNull($container->getDefinition('session.storage.php_bridge')->getArgument(0));
$this->assertSame('session.handler.native_file', (string) $container->getAlias('session.handler')); $this->assertSame('session.handler.native_file', (string) $container->getAlias('session.handler'));

View File

@ -0,0 +1,16 @@
<?php
namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
class DeprecatedSessionController extends AbstractController
{
public function triggerAction()
{
$this->get('session');
return new Response('done');
}
}

View File

@ -22,6 +22,10 @@ injected_flashbag_session_setflash:
path: injected_flashbag/session_setflash/{message} path: injected_flashbag/session_setflash/{message}
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\InjectedFlashbagSessionController::setFlashAction} defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\InjectedFlashbagSessionController::setFlashAction}
deprecated_session_setflash:
path: /deprecated_session/trigger
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\DeprecatedSessionController::triggerAction}
session_showflash: session_showflash:
path: /session_showflash path: /session_showflash
defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::showFlashAction } defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::showFlashAction }

View File

@ -99,6 +99,26 @@ class SessionTest extends AbstractWebTestCase
$this->assertStringContainsString('No flash was set.', $crawler->text()); $this->assertStringContainsString('No flash was set.', $crawler->text());
} }
/**
* @group legacy
* @dataProvider getConfigs
*/
public function testSessionServiceTriggerDeprecation($config, $insulate)
{
$this->expectDeprecation('Since symfony/framework-bundle 5.3: The "session" service is deprecated, use "$requestStack->getSession()" instead.');
$client = $this->createClient(['test_case' => 'Session', 'root_config' => $config]);
if ($insulate) {
$client->insulate();
}
// trigger deprecation
$crawler = $client->request('GET', '/deprecated_session/trigger');
// check response
$this->assertStringContainsString('done', $crawler->text());
}
/** /**
* See if two separate insulated clients can run without * See if two separate insulated clients can run without
* polluting each other's session data. * polluting each other's session data.

View File

@ -9,3 +9,7 @@ services:
Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\InjectedFlashbagSessionController: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\InjectedFlashbagSessionController:
autowire: true autowire: true
tags: ['controller.service_arguments'] tags: ['controller.service_arguments']
Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\DeprecatedSessionController:
autowire: true
autoconfigure: true

View File

@ -20,11 +20,11 @@
"ext-xml": "*", "ext-xml": "*",
"symfony/cache": "^5.2", "symfony/cache": "^5.2",
"symfony/config": "^5.0", "symfony/config": "^5.0",
"symfony/dependency-injection": "^5.2", "symfony/dependency-injection": "^5.3",
"symfony/deprecation-contracts": "^2.1", "symfony/deprecation-contracts": "^2.1",
"symfony/event-dispatcher": "^5.1", "symfony/event-dispatcher": "^5.1",
"symfony/error-handler": "^4.4.1|^5.0.1", "symfony/error-handler": "^4.4.1|^5.0.1",
"symfony/http-foundation": "^5.2.1", "symfony/http-foundation": "^5.3",
"symfony/http-kernel": "^5.2.1", "symfony/http-kernel": "^5.2.1",
"symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php80": "^1.15", "symfony/polyfill-php80": "^1.15",
@ -52,8 +52,6 @@
"symfony/mime": "^4.4|^5.0", "symfony/mime": "^4.4|^5.0",
"symfony/process": "^4.4|^5.0", "symfony/process": "^4.4|^5.0",
"symfony/security-bundle": "^5.1", "symfony/security-bundle": "^5.1",
"symfony/security-csrf": "^4.4|^5.0",
"symfony/security-http": "^4.4|^5.0",
"symfony/serializer": "^5.2", "symfony/serializer": "^5.2",
"symfony/stopwatch": "^4.4|^5.0", "symfony/stopwatch": "^4.4|^5.0",
"symfony/string": "^5.0", "symfony/string": "^5.0",
@ -87,6 +85,8 @@
"symfony/property-info": "<4.4", "symfony/property-info": "<4.4",
"symfony/property-access": "<5.2", "symfony/property-access": "<5.2",
"symfony/serializer": "<5.2", "symfony/serializer": "<5.2",
"symfony/security-csrf": "<5.3",
"symfony/security-core": "<5.3",
"symfony/stopwatch": "<4.4", "symfony/stopwatch": "<4.4",
"symfony/translation": "<5.0", "symfony/translation": "<5.0",
"symfony/twig-bridge": "<4.4", "symfony/twig-bridge": "<4.4",

View File

@ -41,7 +41,7 @@ class RegisterTokenUsageTrackingPass implements CompilerPassInterface
TokenStorageInterface::class => new BoundArgument(new Reference('security.untracked_token_storage'), false), TokenStorageInterface::class => new BoundArgument(new Reference('security.untracked_token_storage'), false),
]); ]);
if (!$container->has('session')) { if (!$container->has('session.storage')) {
$container->setAlias('security.token_storage', 'security.untracked_token_storage')->setPublic(true); $container->setAlias('security.token_storage', 'security.untracked_token_storage')->setPublic(true);
$container->getDefinition('security.untracked_token_storage')->addTag('kernel.reset', ['method' => 'reset']); $container->getDefinition('security.untracked_token_storage')->addTag('kernel.reset', ['method' => 'reset']);
} elseif ($container->hasDefinition('security.context_listener')) { } elseif ($container->hasDefinition('security.context_listener')) {

View File

@ -73,7 +73,7 @@ return static function (ContainerConfigurator $container) {
->args([ ->args([
service('security.untracked_token_storage'), service('security.untracked_token_storage'),
service_locator([ service_locator([
'session' => service('session'), 'request_stack' => service('request_stack'),
]), ]),
]) ])
->tag('kernel.reset', ['method' => 'disableUsageTracking']) ->tag('kernel.reset', ['method' => 'disableUsageTracking'])

View File

@ -18,6 +18,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage;
use Symfony\Component\Security\Http\Firewall\ContextListener; use Symfony\Component\Security\Http\Firewall\ContextListener;
@ -65,7 +66,7 @@ class RegisterTokenUsageTrackingPassTest extends TestCase
$container = new ContainerBuilder(); $container = new ContainerBuilder();
$container->setParameter('security.token_storage.class', UsageTrackingTokenStorage::class); $container->setParameter('security.token_storage.class', UsageTrackingTokenStorage::class);
$container->register('session', Session::class); $container->register('session.storage', NativeSessionStorage::class);
$container->register('security.context_listener', ContextListener::class) $container->register('security.context_listener', ContextListener::class)
->setArguments([ ->setArguments([
new Reference('security.untracked_token_storage'), new Reference('security.untracked_token_storage'),

View File

@ -11,7 +11,12 @@
namespace Symfony\Bundle\SecurityBundle\Tests\Functional; namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Component\BrowserKit\Cookie; use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LogoutTest extends AbstractWebTestCase class LogoutTest extends AbstractWebTestCase
{ {
@ -44,19 +49,26 @@ class LogoutTest extends AbstractWebTestCase
public function testCsrfTokensAreClearedOnLogout(array $options) public function testCsrfTokensAreClearedOnLogout(array $options)
{ {
$client = $this->createClient($options + ['test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml']); $client = $this->createClient($options + ['test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml']);
static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar'); $client->disableReboot();
$this->callInRequestContext($client, function () {
static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar');
});
$client->request('POST', '/login', [ $client->request('POST', '/login', [
'_username' => 'johannes', '_username' => 'johannes',
'_password' => 'test', '_password' => 'test',
]); ]);
$this->assertTrue(static::$container->get('security.csrf.token_storage')->hasToken('foo')); $this->callInRequestContext($client, function () {
$this->assertSame('bar', static::$container->get('security.csrf.token_storage')->getToken('foo')); $this->assertTrue(static::$container->get('security.csrf.token_storage')->hasToken('foo'));
$this->assertSame('bar', static::$container->get('security.csrf.token_storage')->getToken('foo'));
});
$client->request('GET', '/logout'); $client->request('GET', '/logout');
$this->assertFalse(static::$container->get('security.csrf.token_storage')->hasToken('foo')); $this->callInRequestContext($client, function () {
$this->assertFalse(static::$container->get('security.csrf.token_storage')->hasToken('foo'));
});
} }
/** /**
@ -85,4 +97,22 @@ class LogoutTest extends AbstractWebTestCase
$this->assertRedirect($client->getResponse(), '/'); $this->assertRedirect($client->getResponse(), '/');
$this->assertNull($cookieJar->get('flavor')); $this->assertNull($cookieJar->get('flavor'));
} }
private function callInRequestContext(KernelBrowser $client, callable $callable): void
{
/** @var EventDispatcherInterface $eventDispatcher */
$eventDispatcher = static::$container->get(EventDispatcherInterface::class);
$wrappedCallable = function (RequestEvent $event) use (&$callable) {
$callable();
$event->setResponse(new Response(''));
$event->stopPropagation();
};
$eventDispatcher->addListener(KernelEvents::REQUEST, $wrappedCallable);
try {
$client->request('GET', '/'.uniqid('', true));
} finally {
$eventDispatcher->removeListener(KernelEvents::REQUEST, $wrappedCallable);
}
}
} }

View File

@ -24,7 +24,7 @@
"symfony/event-dispatcher": "^5.1", "symfony/event-dispatcher": "^5.1",
"symfony/http-kernel": "^5.0", "symfony/http-kernel": "^5.0",
"symfony/polyfill-php80": "^1.15", "symfony/polyfill-php80": "^1.15",
"symfony/security-core": "^5.2", "symfony/security-core": "^5.3",
"symfony/security-csrf": "^4.4|^5.0", "symfony/security-csrf": "^4.4|^5.0",
"symfony/security-guard": "^5.2", "symfony/security-guard": "^5.2",
"symfony/security-http": "^5.2" "symfony/security-http": "^5.2"

View File

@ -12,12 +12,14 @@
namespace Symfony\Component\DependencyInjection\Compiler; namespace Symfony\Component\DependencyInjection\Compiler;
use Psr\Container\ContainerInterface as PsrContainerInterface; use Psr\Container\ContainerInterface as PsrContainerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Argument\BoundArgument; use Symfony\Component\DependencyInjection\Argument\BoundArgument;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Contracts\Service\ServiceProviderInterface; use Symfony\Contracts\Service\ServiceProviderInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface;
@ -66,7 +68,7 @@ class RegisterServiceSubscribersPass extends AbstractRecursivePass
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $this->currentId, ServiceSubscriberInterface::class)); throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $this->currentId, ServiceSubscriberInterface::class));
} }
$class = $r->name; $class = $r->name;
$replaceDeprecatedSession = $this->container->has('.session.deprecated') && $r->isSubclassOf(AbstractController::class);
$subscriberMap = []; $subscriberMap = [];
foreach ($class::getSubscribedServices() as $key => $type) { foreach ($class::getSubscribedServices() as $key => $type) {
@ -85,6 +87,11 @@ class RegisterServiceSubscribersPass extends AbstractRecursivePass
if (!$autowire) { if (!$autowire) {
throw new InvalidArgumentException(sprintf('Service "%s" misses a "container.service_subscriber" tag with "key"/"id" attributes corresponding to entry "%s" as returned by "%s::getSubscribedServices()".', $this->currentId, $key, $class)); throw new InvalidArgumentException(sprintf('Service "%s" misses a "container.service_subscriber" tag with "key"/"id" attributes corresponding to entry "%s" as returned by "%s::getSubscribedServices()".', $this->currentId, $key, $class));
} }
if ($replaceDeprecatedSession && SessionInterface::class === $type) {
// This prevents triggering the deprecation when building the container
// Should be removed in Symfony 6.0
$type = '.session.deprecated';
}
$serviceMap[$key] = new Reference($type); $serviceMap[$key] = new Reference($type);
} }

View File

@ -4,6 +4,7 @@ CHANGELOG
5.3 5.3
--- ---
* Add the `RequestStack::getSession` method
* Deprecate the `NamespacedAttributeBag` class * Deprecate the `NamespacedAttributeBag` class
* added `ResponseFormatSame` PHPUnit constraint * added `ResponseFormatSame` PHPUnit constraint

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\HttpFoundation\Exception;
/**
* Raised when a session does not exists. This happens in the following cases:
* - the session is not enabled
* - attempt to read a session outside a request context (ie. cli script).
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class SessionNotFoundException extends \LogicException implements RequestExceptionInterface
{
public function __construct($message = 'There is currently no session available.', $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -11,6 +11,9 @@
namespace Symfony\Component\HttpFoundation; namespace Symfony\Component\HttpFoundation;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/** /**
* Request stack that controls the lifecycle of requests. * Request stack that controls the lifecycle of requests.
* *
@ -100,4 +103,18 @@ class RequestStack
return $this->requests[$pos]; return $this->requests[$pos];
} }
/**
* Gets the current session.
*
* @throws SessionNotFoundException
*/
public function getSession(): SessionInterface
{
if ((null !== $request = end($this->requests) ?: null) && $request->hasSession()) {
return $request->getSession();
}
throw new SessionNotFoundException();
}
} }

View File

@ -23,6 +23,7 @@ use Symfony\Component\DependencyInjection\LazyProxy\ProxyHelper;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\DependencyInjection\TypedReference;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/** /**
* Creates the service-locators required by ServiceValueResolver. * Creates the service-locators required by ServiceValueResolver.
@ -165,7 +166,7 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
$invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE; $invalidBehavior = ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE;
} }
if (Request::class === $type) { if (Request::class === $type || SessionInterface::class === $type) {
continue; continue;
} }

View File

@ -4,6 +4,9 @@ CHANGELOG
5.3 5.3
--- ---
* Deprecate the `SessionInterface $session` constructor argument of `SessionTokenStorage`, inject a `\Symfony\Component\HttpFoundation\RequestStack $requestStack` instead
* Deprecate the `session` service provided by the ServiceLocator injected in `UsageTrackingTokenStorage`, provide a `request_stack` service instead
* Deprecate using `SessionTokenStorage` outside a request context, it will throw a `SessionNotFoundException` in Symfony 6.0
* Randomize CSRF tokens to harden BREACH attacks * Randomize CSRF tokens to harden BREACH attacks
* Deprecated voters that do not return a valid decision when calling the `vote` method. * Deprecated voters that do not return a valid decision when calling the `vote` method.

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Security\Core\Authentication\Token\Storage; namespace Symfony\Component\Security\Core\Authentication\Token\Storage;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Contracts\Service\ServiceSubscriberInterface; use Symfony\Contracts\Service\ServiceSubscriberInterface;
@ -24,13 +25,13 @@ use Symfony\Contracts\Service\ServiceSubscriberInterface;
final class UsageTrackingTokenStorage implements TokenStorageInterface, ServiceSubscriberInterface final class UsageTrackingTokenStorage implements TokenStorageInterface, ServiceSubscriberInterface
{ {
private $storage; private $storage;
private $sessionLocator; private $container;
private $enableUsageTracking = false; private $enableUsageTracking = false;
public function __construct(TokenStorageInterface $storage, ContainerInterface $sessionLocator) public function __construct(TokenStorageInterface $storage, ContainerInterface $container)
{ {
$this->storage = $storage; $this->storage = $storage;
$this->sessionLocator = $sessionLocator; $this->container = $container;
} }
/** /**
@ -40,7 +41,7 @@ final class UsageTrackingTokenStorage implements TokenStorageInterface, ServiceS
{ {
if ($this->enableUsageTracking) { if ($this->enableUsageTracking) {
// increments the internal session usage index // increments the internal session usage index
$this->sessionLocator->get('session')->getMetadataBag(); $this->getSession()->getMetadataBag();
} }
return $this->storage->getToken(); return $this->storage->getToken();
@ -55,7 +56,7 @@ final class UsageTrackingTokenStorage implements TokenStorageInterface, ServiceS
if ($token && $this->enableUsageTracking) { if ($token && $this->enableUsageTracking) {
// increments the internal session usage index // increments the internal session usage index
$this->sessionLocator->get('session')->getMetadataBag(); $this->getSession()->getMetadataBag();
} }
} }
@ -72,7 +73,19 @@ final class UsageTrackingTokenStorage implements TokenStorageInterface, ServiceS
public static function getSubscribedServices(): array public static function getSubscribedServices(): array
{ {
return [ return [
'session' => SessionInterface::class, 'request_stack' => RequestStack::class,
]; ];
} }
private function getSession(): SessionInterface
{
// BC for symfony/security-bundle < 5.3
if ($this->container->has('session')) {
trigger_deprecation('symfony/security-core', '5.3', 'Injecting the "session" in "%s" is deprecated, inject the "request_stack" instead.', __CLASS__);
return $this->container->get('session');
}
return $this->container->get('request_stack')->getSession();
}
} }

View File

@ -13,6 +13,8 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Token\Storage;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage;
@ -24,14 +26,19 @@ class UsageTrackingTokenStorageTest extends TestCase
public function testGetSetToken() public function testGetSetToken()
{ {
$sessionAccess = 0; $sessionAccess = 0;
$sessionLocator = new class(['session' => function () use (&$sessionAccess) { $sessionLocator = new class(['request_stack' => function () use (&$sessionAccess) {
++$sessionAccess; ++$sessionAccess;
$session = $this->createMock(SessionInterface::class); $session = $this->createMock(SessionInterface::class);
$session->expects($this->once()) $session->expects($this->once())
->method('getMetadataBag'); ->method('getMetadataBag');
return $session; $request = new Request();
$request->setSession($session);
$requestStack = new RequestStack();
$requestStack->push($request);
return $requestStack;
}]) implements ContainerInterface { }]) implements ContainerInterface {
use ServiceLocatorTrait; use ServiceLocatorTrait;
}; };

View File

@ -26,7 +26,7 @@
"psr/container": "^1.0", "psr/container": "^1.0",
"symfony/event-dispatcher": "^4.4|^5.0", "symfony/event-dispatcher": "^4.4|^5.0",
"symfony/expression-language": "^4.4|^5.0", "symfony/expression-language": "^4.4|^5.0",
"symfony/http-foundation": "^4.4|^5.0", "symfony/http-foundation": "^5.3",
"symfony/ldap": "^4.4|^5.0", "symfony/ldap": "^4.4|^5.0",
"symfony/translation": "^4.4|^5.0", "symfony/translation": "^4.4|^5.0",
"symfony/validator": "^5.2", "symfony/validator": "^5.2",
@ -34,6 +34,7 @@
}, },
"conflict": { "conflict": {
"symfony/event-dispatcher": "<4.4", "symfony/event-dispatcher": "<4.4",
"symfony/http-foundation": "<5.3",
"symfony/security-guard": "<4.4", "symfony/security-guard": "<4.4",
"symfony/ldap": "<4.4", "symfony/ldap": "<4.4",
"symfony/validator": "<5.2" "symfony/validator": "<5.2"

View File

@ -12,6 +12,8 @@
namespace Symfony\Component\Security\Csrf\Tests\TokenStorage; namespace Symfony\Component\Security\Csrf\Tests\TokenStorage;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
@ -37,7 +39,11 @@ class SessionTokenStorageTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$this->session = new Session(new MockArraySessionStorage()); $this->session = new Session(new MockArraySessionStorage());
$this->storage = new SessionTokenStorage($this->session, self::SESSION_NAMESPACE); $request = new Request();
$request->setSession($this->session);
$requestStack = new RequestStack();
$requestStack->push($request);
$this->storage = new SessionTokenStorage($requestStack, self::SESSION_NAMESPACE);
} }
public function testStoreTokenInNotStartedSessionStartsTheSession() public function testStoreTokenInNotStartedSessionStartsTheSession()

View File

@ -11,7 +11,12 @@
namespace Symfony\Component\Security\Csrf\TokenStorage; namespace Symfony\Component\Security\Csrf\TokenStorage;
use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException; use Symfony\Component\Security\Csrf\Exception\TokenNotFoundException;
/** /**
@ -26,17 +31,30 @@ class SessionTokenStorage implements ClearableTokenStorageInterface
*/ */
public const SESSION_NAMESPACE = '_csrf'; public const SESSION_NAMESPACE = '_csrf';
private $session; private $requestStack;
private $namespace; private $namespace;
/**
* Tp be remove in Symfony 6.0
*/
private $session;
/** /**
* Initializes the storage with a Session object and a session namespace. * Initializes the storage with a RequestStack object and a session namespace.
* *
* @param string $namespace The namespace under which the token is stored in the session * @param RequestStack $requestStack
* @param string $namespace The namespace under which the token is stored in the requestStack
*/ */
public function __construct(SessionInterface $session, string $namespace = self::SESSION_NAMESPACE) public function __construct(/* RequestStack*/ $requestStack, string $namespace = self::SESSION_NAMESPACE)
{ {
$this->session = $session; if ($requestStack instanceof SessionInterface) {
trigger_deprecation('symfony/security-csrf', '5.3', 'Passing a "%s" to "%s" is deprecated, use a "%s" instead.', SessionInterface::class, __CLASS__, RequestStack::class);
$request = new Request();
$request->setSession($requestStack);
$requestStack = new RequestStack();
$requestStack->push($request);
}
$this->requestStack = $requestStack;
$this->namespace = $namespace; $this->namespace = $namespace;
} }
@ -45,15 +63,16 @@ class SessionTokenStorage implements ClearableTokenStorageInterface
*/ */
public function getToken(string $tokenId) public function getToken(string $tokenId)
{ {
if (!$this->session->isStarted()) { $session = $this->getSession();
$this->session->start(); if (!$session->isStarted()) {
$session->start();
} }
if (!$this->session->has($this->namespace.'/'.$tokenId)) { if (!$session->has($this->namespace.'/'.$tokenId)) {
throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.'); throw new TokenNotFoundException('The CSRF token with ID '.$tokenId.' does not exist.');
} }
return (string) $this->session->get($this->namespace.'/'.$tokenId); return (string) $session->get($this->namespace.'/'.$tokenId);
} }
/** /**
@ -61,11 +80,12 @@ class SessionTokenStorage implements ClearableTokenStorageInterface
*/ */
public function setToken(string $tokenId, string $token) public function setToken(string $tokenId, string $token)
{ {
if (!$this->session->isStarted()) { $session = $this->getSession();
$this->session->start(); if (!$session->isStarted()) {
$session->start();
} }
$this->session->set($this->namespace.'/'.$tokenId, $token); $session->set($this->namespace.'/'.$tokenId, $token);
} }
/** /**
@ -73,11 +93,12 @@ class SessionTokenStorage implements ClearableTokenStorageInterface
*/ */
public function hasToken(string $tokenId) public function hasToken(string $tokenId)
{ {
if (!$this->session->isStarted()) { $session = $this->getSession();
$this->session->start(); if (!$session->isStarted()) {
$session->start();
} }
return $this->session->has($this->namespace.'/'.$tokenId); return $session->has($this->namespace.'/'.$tokenId);
} }
/** /**
@ -85,11 +106,12 @@ class SessionTokenStorage implements ClearableTokenStorageInterface
*/ */
public function removeToken(string $tokenId) public function removeToken(string $tokenId)
{ {
if (!$this->session->isStarted()) { $session = $this->getSession();
$this->session->start(); if (!$session->isStarted()) {
$session->start();
} }
return $this->session->remove($this->namespace.'/'.$tokenId); return $session->remove($this->namespace.'/'.$tokenId);
} }
/** /**
@ -97,10 +119,22 @@ class SessionTokenStorage implements ClearableTokenStorageInterface
*/ */
public function clear() public function clear()
{ {
foreach (array_keys($this->session->all()) as $key) { $session = $this->getSession();
foreach (array_keys($session->all()) as $key) {
if (0 === strpos($key, $this->namespace.'/')) { if (0 === strpos($key, $this->namespace.'/')) {
$this->session->remove($key); $session->remove($key);
} }
} }
} }
private function getSession(): SessionInterface
{
try {
return $this->requestStack->getSession();
} catch (SessionNotFoundException $e) {
trigger_deprecation('symfony/security-csrf', '5.3', 'Using the "%s" without a session has no effect and is deprecated. It will throw a "%s" in Symfony 6.0', __CLASS__, SessionNotFoundException::class);
return $this->session ?? $this->session = new Session(new MockArraySessionStorage());
}
}
} }

View File

@ -20,10 +20,10 @@
"symfony/security-core": "^4.4|^5.0" "symfony/security-core": "^4.4|^5.0"
}, },
"require-dev": { "require-dev": {
"symfony/http-foundation": "^4.4|^5.0" "symfony/http-foundation": "^5.3"
}, },
"conflict": { "conflict": {
"symfony/http-foundation": "<4.4" "symfony/http-foundation": "<5.3"
}, },
"suggest": { "suggest": {
"symfony/http-foundation": "For using the class SessionTokenStorage." "symfony/http-foundation": "For using the class SessionTokenStorage."

View File

@ -16,6 +16,7 @@ use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface;
@ -375,13 +376,17 @@ class ContextListenerTest extends TestCase
protected function runSessionOnKernelResponse($newToken, $original = null) protected function runSessionOnKernelResponse($newToken, $original = null)
{ {
$session = new Session(new MockArraySessionStorage()); $session = new Session(new MockArraySessionStorage());
$request = new Request();
$request->setSession($session);
$requestStack = new RequestStack();
$requestStack->push($request);
if (null !== $original) { if (null !== $original) {
$session->set('_security_session', $original); $session->set('_security_session', $original);
} }
$tokenStorage = new UsageTrackingTokenStorage(new TokenStorage(), new class(['session' => function () use ($session) { $tokenStorage = new UsageTrackingTokenStorage(new TokenStorage(), new class(['request_stack' => function () use ($requestStack) {
return $session; return $requestStack;
}, },
]) implements ContainerInterface { ]) implements ContainerInterface {
use ServiceLocatorTrait; use ServiceLocatorTrait;
@ -389,8 +394,6 @@ class ContextListenerTest extends TestCase
$tokenStorage->setToken($newToken); $tokenStorage->setToken($newToken);
$request = new Request();
$request->setSession($session);
$request->cookies->set('MOCKSESSID', true); $request->cookies->set('MOCKSESSID', true);
$sessionId = $session->getId(); $sessionId = $session->getId();
@ -424,13 +427,22 @@ class ContextListenerTest extends TestCase
$request = new Request(); $request = new Request();
$request->setSession($session); $request->setSession($session);
$request->cookies->set('MOCKSESSID', true); $request->cookies->set('MOCKSESSID', true);
$requestStack = new RequestStack();
$requestStack->push($request);
$tokenStorage = new TokenStorage(); $tokenStorage = new TokenStorage();
$usageIndex = $session->getUsageIndex(); $usageIndex = $session->getUsageIndex();
$tokenStorage = new UsageTrackingTokenStorage($tokenStorage, new class(['session' => function () use ($session) { $tokenStorage = new UsageTrackingTokenStorage($tokenStorage, new class(
return $session; (new \ReflectionClass(UsageTrackingTokenStorage::class))->hasMethod('getSession') ? [
}, 'request_stack' => function () use ($requestStack) {
]) implements ContainerInterface { return $requestStack;
}] : [
// BC for symfony/framework-bundle < 5.3
'session' => function () use ($session) {
return $session;
},
]
) implements ContainerInterface {
use ServiceLocatorTrait; use ServiceLocatorTrait;
}); });
$sessionTrackerEnabler = [$tokenStorage, 'enableUsageTracking']; $sessionTrackerEnabler = [$tokenStorage, 'enableUsageTracking'];

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Security\Http\Tests\Logout;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
@ -23,13 +24,23 @@ use Symfony\Component\Security\Http\Logout\CsrfTokenClearingLogoutHandler;
class CsrfTokenClearingLogoutHandlerTest extends TestCase class CsrfTokenClearingLogoutHandlerTest extends TestCase
{ {
private $session; private $session;
private $requestStack;
private $csrfTokenStorage; private $csrfTokenStorage;
private $csrfTokenClearingLogoutHandler; private $csrfTokenClearingLogoutHandler;
protected function setUp(): void protected function setUp(): void
{ {
$this->session = new Session(new MockArraySessionStorage()); $this->session = new Session(new MockArraySessionStorage());
$this->csrfTokenStorage = new SessionTokenStorage($this->session, 'foo');
// BC for symfony/security-core < 5.3
if (\method_exists(SessionTokenStorage::class, 'getSession')) {
$request = new Request();
$request->setSession($this->session);
$this->requestStack = new RequestStack();
$this->requestStack->push($request);
}
$this->csrfTokenStorage = new SessionTokenStorage($this->requestStack ?? $this->session, 'foo');
$this->csrfTokenStorage->setToken('foo', 'bar'); $this->csrfTokenStorage->setToken('foo', 'bar');
$this->csrfTokenStorage->setToken('foobar', 'baz'); $this->csrfTokenStorage->setToken('foobar', 'baz');
$this->csrfTokenClearingLogoutHandler = new CsrfTokenClearingLogoutHandler($this->csrfTokenStorage); $this->csrfTokenClearingLogoutHandler = new CsrfTokenClearingLogoutHandler($this->csrfTokenStorage);
@ -51,7 +62,7 @@ class CsrfTokenClearingLogoutHandlerTest extends TestCase
public function testCsrfTokenCookieWithDifferentNamespaceIsNotRemoved() public function testCsrfTokenCookieWithDifferentNamespaceIsNotRemoved()
{ {
$barNamespaceCsrfSessionStorage = new SessionTokenStorage($this->session, 'bar'); $barNamespaceCsrfSessionStorage = new SessionTokenStorage($this->requestStack ?? $this->session, 'bar');
$barNamespaceCsrfSessionStorage->setToken('foo', 'bar'); $barNamespaceCsrfSessionStorage->setToken('foo', 'bar');
$barNamespaceCsrfSessionStorage->setToken('foobar', 'baz'); $barNamespaceCsrfSessionStorage->setToken('foobar', 'baz');