diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php new file mode 100644 index 000000000000..63c54bddc710 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php @@ -0,0 +1,51 @@ + + * + * 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\Bridge\Monolog\Processor\ProcessorInterface; +use Symfony\Component\DependencyInjection\Argument\BoundArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + +/** + * Injects the session tracker enabler in "security.context_listener" + binds "security.untracked_token_storage" to ProcessorInterface instances. + * + * @author Nicolas Grekas + * + * @internal + */ +class RegisterTokenUsageTrackingPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->has('security.untracked_token_storage')) { + return; + } + + $processorAutoconfiguration = $container->registerForAutoconfiguration(ProcessorInterface::class); + $processorAutoconfiguration->setBindings($processorAutoconfiguration->getBindings() + [ + TokenStorageInterface::class => new BoundArgument(new Reference('security.untracked_token_storage'), false), + ]); + + if (!$container->has('session')) { + $container->setAlias('security.token_storage', 'security.untracked_token_storage')->setPublic(true); + } elseif ($container->hasDefinition('security.context_listener')) { + $container->getDefinition('security.context_listener') + ->setArgument(6, [new Reference('security.token_storage'), 'enableUsageTracking']); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml index 2effc4554bb2..811c6dfc5cfd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml @@ -9,7 +9,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index b1263b057b31..86bf11869b84 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -21,11 +21,18 @@ - + + + + + + + + @@ -162,7 +169,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 55044986e310..e1a9ce5c038e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -9,7 +9,7 @@ - + @@ -37,7 +37,7 @@ - + @@ -128,7 +128,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml index 956a75a5be2b..94aa3a3824ba 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml @@ -9,7 +9,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index 61e288061594..ee8da39eacd9 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -15,6 +15,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory; @@ -66,5 +67,6 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new AddSecurityVotersPass()); $container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass()); + $container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200); } } diff --git a/src/Symfony/Component/HttpFoundation/Session/Session.php b/src/Symfony/Component/HttpFoundation/Session/Session.php index 9738189b4000..2192c629e896 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Session.php +++ b/src/Symfony/Component/HttpFoundation/Session/Session.php @@ -136,10 +136,7 @@ public function count() return \count($this->getAttributeBag()->all()); } - /** - * @internal - */ - public function getUsageIndex(): int + public function &getUsageIndex(): int { return $this->usageIndex; } diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/Storage/UsageTrackingTokenStorage.php b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/UsageTrackingTokenStorage.php new file mode 100644 index 000000000000..3ce8913aa4fb --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/UsageTrackingTokenStorage.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Authentication\Token\Storage; + +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Contracts\Service\ServiceSubscriberInterface; + +/** + * A token storage that increments the session usage index when the token is accessed. + * + * @author Nicolas Grekas + */ +final class UsageTrackingTokenStorage implements TokenStorageInterface, ServiceSubscriberInterface +{ + private $storage; + private $sessionLocator; + private $enableUsageTracking = false; + + public function __construct(TokenStorageInterface $storage, ContainerInterface $sessionLocator) + { + $this->storage = $storage; + $this->sessionLocator = $sessionLocator; + } + + /** + * {@inheritdoc} + */ + public function getToken(): ?TokenInterface + { + if ($this->enableUsageTracking) { + // increments the internal session usage index + $this->sessionLocator->get('session')->getMetadataBag(); + } + + return $this->storage->getToken(); + } + + /** + * {@inheritdoc} + */ + public function setToken(TokenInterface $token = null): void + { + $this->storage->setToken($token); + } + + public function enableUsageTracking(): void + { + $this->enableUsageTracking = true; + } + + public function disableUsageTracking(): void + { + $this->enableUsageTracking = false; + } + + public static function getSubscribedServices(): array + { + return [ + 'session' => SessionInterface::class, + ]; + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php new file mode 100644 index 000000000000..3f353594f021 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Token/Storage/UsageTrackingTokenStorageTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Tests\Authentication\Token\Storage; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +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\TokenInterface; +use Symfony\Contracts\Service\ServiceLocatorTrait; + +class UsageTrackingTokenStorageTest extends TestCase +{ + public function testGetSetToken() + { + $sessionAccess = 0; + $sessionLocator = new class(['session' => function () use (&$sessionAccess) { + ++$sessionAccess; + + $session = $this->createMock(SessionInterface::class); + $session->expects($this->once()) + ->method('getMetadataBag'); + + return $session; + }]) implements ContainerInterface { + use ServiceLocatorTrait; + }; + $tokenStorage = new TokenStorage(); + $trackingStorage = new UsageTrackingTokenStorage($tokenStorage, $sessionLocator); + + $this->assertNull($trackingStorage->getToken()); + $token = $this->getMockBuilder(TokenInterface::class)->getMock(); + + $trackingStorage->setToken($token); + $this->assertSame($token, $trackingStorage->getToken()); + $this->assertSame($token, $tokenStorage->getToken()); + $this->assertSame(0, $sessionAccess); + + $trackingStorage->enableUsageTracking(); + $this->assertSame($token, $trackingStorage->getToken()); + $this->assertSame(1, $sessionAccess); + + $trackingStorage->disableUsageTracking(); + $this->assertSame($token, $trackingStorage->getToken()); + $this->assertSame(1, $sessionAccess); + } +} diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index 5b31a9a392fb..ae44882f36c1 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -18,7 +18,7 @@ "require": { "php": "^7.1.3", "symfony/event-dispatcher-contracts": "^1.1|^2", - "symfony/service-contracts": "^1.1|^2" + "symfony/service-contracts": "^1.1.6|^2" }, "require-dev": { "psr/container": "^1.0", diff --git a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php index e915f024b99a..a309ab14dd9e 100644 --- a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php @@ -51,18 +51,18 @@ public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionM */ public function __invoke(RequestEvent $event) { - if (null === $token = $this->tokenStorage->getToken()) { - throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.'); - } - $request = $event->getRequest(); list($attributes) = $this->map->getPatterns($request); - if (null === $attributes) { + if (!$attributes) { return; } + if (null === $token = $this->tokenStorage->getToken()) { + throw new AuthenticationCredentialsNotFoundException('A Token was not found in the TokenStorage.'); + } + if (!$token->isAuthenticated()) { $token = $this->authManager->authenticate($token); $this->tokenStorage->setToken($token); diff --git a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php index e1b300e64317..95b1c7d6efc1 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ContextListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ContextListener.php @@ -13,6 +13,8 @@ use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpKernel\Event\FilterResponseEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\KernelEvents; @@ -49,11 +51,12 @@ class ContextListener implements ListenerInterface private $dispatcher; private $registered; private $trustResolver; + private $sessionTrackerEnabler; /** * @param iterable|UserProviderInterface[] $userProviders */ - public function __construct(TokenStorageInterface $tokenStorage, iterable $userProviders, string $contextKey, LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, AuthenticationTrustResolverInterface $trustResolver = null) + public function __construct(TokenStorageInterface $tokenStorage, iterable $userProviders, string $contextKey, LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, AuthenticationTrustResolverInterface $trustResolver = null, callable $sessionTrackerEnabler = null) { if (empty($contextKey)) { throw new \InvalidArgumentException('$contextKey must not be empty.'); @@ -65,6 +68,7 @@ public function __construct(TokenStorageInterface $tokenStorage, iterable $userP $this->logger = $logger; $this->dispatcher = $dispatcher; $this->trustResolver = $trustResolver ?: new AuthenticationTrustResolver(AnonymousToken::class, RememberMeToken::class); + $this->sessionTrackerEnabler = $sessionTrackerEnabler; } /** @@ -92,7 +96,21 @@ public function __invoke(RequestEvent $event) $request = $event->getRequest(); $session = $request->hasPreviousSession() && $request->hasSession() ? $request->getSession() : null; - if (null === $session || null === $token = $session->get($this->sessionKey)) { + if (null !== $session) { + $usageIndexValue = method_exists(Request::class, 'getPreferredFormat') && $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : 0; + $sessionId = $session->getId(); + $token = $session->get($this->sessionKey); + + if ($this->sessionTrackerEnabler && $session->getId() === $sessionId) { + $usageIndexReference = $usageIndexValue; + } + } + + if (null === $session || null === $token) { + if ($this->sessionTrackerEnabler) { + ($this->sessionTrackerEnabler)(); + } + $this->tokenStorage->setToken(null); return; @@ -117,6 +135,10 @@ public function __invoke(RequestEvent $event) $token = null; } + if ($this->sessionTrackerEnabler) { + ($this->sessionTrackerEnabler)(); + } + $this->tokenStorage->setToken($token); } @@ -137,19 +159,26 @@ public function onKernelResponse(FilterResponseEvent $event) $this->dispatcher->removeListener(KernelEvents::RESPONSE, [$this, 'onKernelResponse']); $this->registered = false; + $session = $request->getSession(); + $sessionId = $session->getId(); + $usageIndexValue = method_exists(Request::class, 'getPreferredFormat') && $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : null; $token = $this->tokenStorage->getToken(); if (null === $token || $this->trustResolver->isAnonymous($token)) { - if ($request->hasPreviousSession() && $request->hasSession()) { - $request->getSession()->remove($this->sessionKey); + if ($request->hasPreviousSession()) { + $session->remove($this->sessionKey); } } else { - $request->getSession()->set($this->sessionKey, serialize($token)); + $session->set($this->sessionKey, serialize($token)); if (null !== $this->logger) { $this->logger->debug('Stored the security token in the session.', ['key' => $this->sessionKey]); } } + + if ($this->sessionTrackerEnabler && $session->getId() === $sessionId) { + $usageIndexReference = $usageIndexValue; + } } /** diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php index 60c5bcb1a284..08515164971c 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php @@ -12,7 +12,12 @@ namespace Symfony\Component\Security\Http\Tests\Firewall; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; +use Symfony\Component\Security\Http\AccessMapInterface; use Symfony\Component\Security\Http\Firewall\AccessListener; class AccessListenerTest extends TestCase @@ -182,6 +187,41 @@ public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest() $listener($event); } + public function testHandleWhenAccessMapReturnsEmptyAttributes() + { + $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->disableOriginalClone()->getMock(); + + $accessMap = $this->getMockBuilder(AccessMapInterface::class)->getMock(); + $accessMap + ->expects($this->any()) + ->method('getPatterns') + ->with($this->equalTo($request)) + ->willReturn([[], null]) + ; + + $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); + $tokenStorage + ->expects($this->never()) + ->method('getToken') + ; + + $listener = new AccessListener( + $tokenStorage, + $this->getMockBuilder(AccessDecisionManagerInterface::class)->getMock(), + $accessMap, + $this->getMockBuilder(AuthenticationManagerInterface::class)->getMock() + ); + + $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); + $event + ->expects($this->any()) + ->method('getRequest') + ->willReturn($request) + ; + + $listener($event); + } + public function testHandleWhenTheSecurityTokenStorageHasNoToken() { $this->expectException('Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException'); @@ -192,14 +232,29 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken() ->willReturn(null) ; + $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->disableOriginalClone()->getMock(); + + $accessMap = $this->getMockBuilder(AccessMapInterface::class)->getMock(); + $accessMap + ->expects($this->any()) + ->method('getPatterns') + ->with($this->equalTo($request)) + ->willReturn([['foo' => 'bar'], null]) + ; + $listener = new AccessListener( $tokenStorage, $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface')->getMock(), - $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock(), + $accessMap, $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock() ); $event = $this->getMockBuilder(RequestEvent::class)->disableOriginalConstructor()->getMock(); + $event + ->expects($this->any()) + ->method('getRequest') + ->willReturn($request) + ; $listener($event); } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php index 3f26f0db5b6f..4afdd1fe0897 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ContextListenerTest.php @@ -12,11 +12,13 @@ namespace Symfony\Component\Security\Http\Tests\Firewall; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent; @@ -25,6 +27,7 @@ use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; @@ -33,6 +36,7 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Http\Event\DeauthenticatedEvent; use Symfony\Component\Security\Http\Firewall\ContextListener; +use Symfony\Contracts\Service\ServiceLocatorTrait; class ContextListenerTest extends TestCase { @@ -51,7 +55,7 @@ public function testUserProvidersNeedToImplementAnInterface() { $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('User provider "stdClass" must implement "Symfony\Component\Security\Core\User\UserProviderInterface'); - $this->handleEventWithPreviousSession(new TokenStorage(), [new \stdClass()]); + $this->handleEventWithPreviousSession([new \stdClass()]); } public function testOnKernelResponseWillAddSession() @@ -205,6 +209,7 @@ public function testHandleAddsKernelResponseListener() public function testOnKernelResponseListenerRemovesItself() { + $session = $this->getMockBuilder(SessionInterface::class)->getMock(); $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); $dispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMock(); @@ -214,6 +219,9 @@ public function testOnKernelResponseListenerRemovesItself() $request->expects($this->any()) ->method('hasSession') ->willReturn(true); + $request->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)); $event = new ResponseEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST, new Response()); @@ -243,9 +251,8 @@ public function testHandleRemovesTokenIfNoPreviousSessionWasFound() public function testIfTokenIsDeauthenticated() { - $tokenStorage = new TokenStorage(); $refreshedUser = new User('foobar', 'baz'); - $this->handleEventWithPreviousSession($tokenStorage, [new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)]); + $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)]); $this->assertNull($tokenStorage->getToken()); } @@ -255,32 +262,29 @@ public function testIfTokenIsNotDeauthenticated() $tokenStorage = new TokenStorage(); $badRefreshedUser = new User('foobar', 'baz'); $goodRefreshedUser = new User('foobar', 'bar'); - $this->handleEventWithPreviousSession($tokenStorage, [new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser, true); + $tokenStorage = $this->handleEventWithPreviousSession([new SupportingUserProvider($badRefreshedUser), new SupportingUserProvider($goodRefreshedUser)], $goodRefreshedUser, true); $this->assertSame($goodRefreshedUser, $tokenStorage->getToken()->getUser()); } public function testTryAllUserProvidersUntilASupportingUserProviderIsFound() { - $tokenStorage = new TokenStorage(); $refreshedUser = new User('foobar', 'baz'); - $this->handleEventWithPreviousSession($tokenStorage, [new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)], $refreshedUser); + $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)], $refreshedUser); $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser()); } public function testNextSupportingUserProviderIsTriedIfPreviousSupportingUserProviderDidNotLoadTheUser() { - $tokenStorage = new TokenStorage(); $refreshedUser = new User('foobar', 'baz'); - $this->handleEventWithPreviousSession($tokenStorage, [new SupportingUserProvider(), new SupportingUserProvider($refreshedUser)], $refreshedUser); + $tokenStorage = $this->handleEventWithPreviousSession([new SupportingUserProvider(), new SupportingUserProvider($refreshedUser)], $refreshedUser); $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser()); } public function testTokenIsSetToNullIfNoUserWasLoadedByTheRegisteredUserProviders() { - $tokenStorage = new TokenStorage(); - $this->handleEventWithPreviousSession($tokenStorage, [new NotSupportingUserProvider(), new SupportingUserProvider()]); + $tokenStorage = $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new SupportingUserProvider()]); $this->assertNull($tokenStorage->getToken()); } @@ -288,14 +292,13 @@ public function testTokenIsSetToNullIfNoUserWasLoadedByTheRegisteredUserProvider public function testRuntimeExceptionIsThrownIfNoSupportingUserProviderWasRegistered() { $this->expectException('RuntimeException'); - $this->handleEventWithPreviousSession(new TokenStorage(), [new NotSupportingUserProvider(), new NotSupportingUserProvider()]); + $this->handleEventWithPreviousSession([new NotSupportingUserProvider(), new NotSupportingUserProvider()]); } public function testAcceptsProvidersAsTraversable() { - $tokenStorage = new TokenStorage(); $refreshedUser = new User('foobar', 'baz'); - $this->handleEventWithPreviousSession($tokenStorage, new \ArrayObject([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)]), $refreshedUser); + $tokenStorage = $this->handleEventWithPreviousSession(new \ArrayObject([new NotSupportingUserProvider(), new SupportingUserProvider($refreshedUser)]), $refreshedUser); $this->assertSame($refreshedUser, $tokenStorage->getToken()->getUser()); } @@ -335,13 +338,21 @@ protected function runSessionOnKernelResponse($newToken, $original = null) $session->set('_security_session', $original); } - $tokenStorage = new TokenStorage(); + $tokenStorage = new UsageTrackingTokenStorage(new TokenStorage(), new class([ + 'session' => function () use ($session) { return $session; } + ]) implements ContainerInterface { + use ServiceLocatorTrait; + }); + $tokenStorage->setToken($newToken); $request = new Request(); $request->setSession($session); $request->cookies->set('MOCKSESSID', true); + $sessionId = $session->getId(); + $usageIndex = \method_exists(Request::class, 'getPreferredFormat') ? $session->getUsageIndex() : null; + $event = new ResponseEvent( $this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, @@ -349,13 +360,21 @@ protected function runSessionOnKernelResponse($newToken, $original = null) new Response() ); - $listener = new ContextListener($tokenStorage, [], 'session', null, new EventDispatcher()); + $listener = new ContextListener($tokenStorage, [], 'session', null, new EventDispatcher(), null, [$tokenStorage, 'enableUsageTracking']); $listener->onKernelResponse($event); + if (null !== $usageIndex) { + if ($session->getId() === $sessionId) { + $this->assertSame($usageIndex, $session->getUsageIndex()); + } else { + $this->assertNotSame($usageIndex, $session->getUsageIndex()); + } + } + return $session; } - private function handleEventWithPreviousSession(TokenStorageInterface $tokenStorage, $userProviders, UserInterface $user = null) + private function handleEventWithPreviousSession($userProviders, UserInterface $user = null) { $user = $user ?: new User('foo', 'bar'); $session = new Session(new MockArraySessionStorage()); @@ -365,8 +384,30 @@ private function handleEventWithPreviousSession(TokenStorageInterface $tokenStor $request->setSession($session); $request->cookies->set('MOCKSESSID', true); - $listener = new ContextListener($tokenStorage, $userProviders, 'context_key'); + $tokenStorage = new TokenStorage(); + $usageIndex = null; + $sessionTrackerEnabler = null; + + if (\method_exists(Request::class, 'getPreferredFormat')) { + $usageIndex = $session->getUsageIndex(); + $tokenStorage = new UsageTrackingTokenStorage($tokenStorage, new class([ + 'session' => function () use ($session) { return $session; } + ]) implements ContainerInterface { + use ServiceLocatorTrait; + }); + $sessionTrackerEnabler = [$tokenStorage, 'enableUsageTracking']; + } + + $listener = new ContextListener($tokenStorage, $userProviders, 'context_key', null, null, null, $sessionTrackerEnabler); $listener(new RequestEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST)); + + if (null !== $usageIndex) { + $this->assertSame($usageIndex, $session->getUsageIndex()); + $tokenStorage->getToken(); + $this->assertSame(1 + $usageIndex, $session->getUsageIndex()); + } + + return $tokenStorage; } } diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json index 1da2cab878f8..d51c9d77e438 100644 --- a/src/Symfony/Component/Security/Http/composer.json +++ b/src/Symfony/Component/Security/Http/composer.json @@ -17,7 +17,7 @@ ], "require": { "php": "^7.1.3", - "symfony/security-core": "^4.3", + "symfony/security-core": "^4.4", "symfony/http-foundation": "^3.4|^4.0|^5.0", "symfony/http-kernel": "^4.3", "symfony/property-access": "^3.4|^4.0|^5.0"