diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 3f25c3da03e4a..dd29dc57b1429 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -79,6 +79,7 @@ public function getConfigTreeBuilder() $this->addFirewallsSection($rootNode, $this->factories); $this->addAccessControlSection($rootNode); $this->addRoleHierarchySection($rootNode); + $this->addSessionRegistrySection($rootNode); return $tb; } @@ -288,6 +289,21 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end() ->end() ->end() + ->arrayNode('session_expiration') + ->canBeUnset() + ->children() + ->integerNode('max_idle_time')->defaultValue(ini_get('session.gc_maxlifetime'))->min(1)->end() + ->scalarNode('expiration_url')->defaultNull()->end() + ->end() + ->end() + ->arrayNode('session_concurrency') + ->canBeUnset() + ->children() + ->integerNode('max_sessions')->min(0)->end() + ->booleanNode('error_if_maximum_exceeded')->defaultTrue()->end() + ->booleanNode('register_new_sessions')->defaultNull()->end() + ->end() + ->end() ; $abstractFactoryKeys = array(); @@ -430,4 +446,13 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode) ->end() ; } + + private function addSessionRegistrySection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->scalarNode('session_registry_storage')->defaultValue('security.session_registry.storage.file')->end() + ->end() + ; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index 00f0c3a0e1dcc..a324836a4ea25 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -146,7 +146,7 @@ protected function createEntryPoint($container, $id, $config, $defaultEntryPoint * * @param array $config * - * @return bool Whether a possibly configured RememberMeServices should be set for this listener + * @return bool Whether a possibly configured RememberMeServices should be set for this listener */ protected function isRememberMeAware($config) { @@ -157,6 +157,13 @@ protected function createListener($container, $id, $config, $userProvider) { $listenerId = $this->getListenerId(); $listener = new DefinitionDecorator($listenerId); + + //Check for custom session authentication strategy + $sessionAuthenticationStrategyId = 'security.authentication.session_strategy.'.$id; + if ($container->hasDefinition($sessionAuthenticationStrategyId) || $container->hasAlias($sessionAuthenticationStrategyId)) { + $listener->replaceArgument(2, new Reference($sessionAuthenticationStrategyId)); + } + $listener->replaceArgument(4, $id); $listener->replaceArgument(5, new Reference($this->createAuthenticationSuccessHandler($container, $id, $config))); $listener->replaceArgument(6, new Reference($this->createAuthenticationFailureHandler($container, $id, $config))); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 1f20fc7596414..52b382133db1f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -71,6 +71,10 @@ public function load(array $configs, ContainerBuilder $container) $container->removeDefinition('security.access.expression_voter'); } + if (isset($config['session_registry_storage'])) { + $this->loadSessionRegistry($config, $container, $loader); + } + // set some global scalars $container->setParameter('security.access.denied_url', $config['access_denied_url']); $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']); @@ -361,6 +365,11 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider)); } + // Session expiration listener + if (isset($firewall['session_expiration'])) { + $listeners[] = new Reference($this->createSessionExpirationListener($container, $id, $firewall)); + } + // Access listener $listeners[] = new Reference('security.access_listener'); @@ -394,6 +403,10 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut $hasListeners = false; $defaultEntryPoint = null; + if (isset($firewall['session_concurrency'])) { + $this->createConcurrentSessionAuthenticationStrategy($container, $id, $firewall); + } + foreach ($this->listenerPositions as $position) { foreach ($this->factories[$position] as $factory) { $key = str_replace('-', '_', $factory->getKey()); @@ -611,6 +624,17 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv return $switchUserListenerId; } + private function createSessionExpirationListener($container, $id, $config) + { + $expiredSessionListenerId = 'security.authentication.sessionexpiration_listener.'.$id; + $listener = $container->setDefinition($expiredSessionListenerId, new DefinitionDecorator('security.authentication.sessionexpiration_listener')); + + $listener->replaceArgument(2, $config['session_expiration']['max_idle_time']); + $listener->replaceArgument(3, $config['session_expiration']['expiration_url']); + + return $expiredSessionListenerId; + } + private function createExpression($container, $expression) { if (isset($this->expressions[$id = 'security.expression.'.sha1($expression)])) { @@ -697,4 +721,60 @@ private function getExpressionLanguage() return $this->expressionLanguage; } + + private function loadSessionRegistry($config, ContainerBuilder $container, $loader) + { + $container->setAlias('security.session_registry.storage', $config['session_registry_storage']); + $loader->load('security_session_concurrency.xml'); + } + + private function createConcurrentSessionAuthenticationStrategy($container, $id, $config) + { + $authenticationStrategies = array(); + $sessionStrategyId = 'security.authentication.session_strategy.'.$id; + + if (isset($config['session_concurrency']['max_sessions']) && $config['session_concurrency']['max_sessions'] > 0) { + $concurrentSessionControlStrategyId = 'security.authentication.session_strategy.concurrent_control.'.$id; + $container + ->setDefinition( + $concurrentSessionControlStrategyId, new DefinitionDecorator( + 'security.authentication.session_strategy.concurrent_control' + ) + ) + ->replaceArgument(1, $config['session_concurrency']['max_sessions']) + ->replaceArgument(2, $config['session_concurrency']['error_if_maximum_exceeded']) + ; + + $authenticationStrategies[] = new Reference($concurrentSessionControlStrategyId); + } + + $fixationSessionStrategyId = 'security.authentication.session_strategy.fixation.'.$id; + $container->setAlias( + $fixationSessionStrategyId, 'security.authentication.session_strategy' + ); + $authenticationStrategies[] = new Reference($fixationSessionStrategyId); + + if ( + (!isset($config['register_new_sessions']) && $config['stateless'] == false) || (isset($config['register_new_sessions']) && $config['register_new_sessions'] == true) + ) { + $registerSessionStrategyId = 'security.authentication.session_strategy.register.'.$id; + $container->setDefinition( + $registerSessionStrategyId, new DefinitionDecorator( + 'security.authentication.session_strategy.register' + ) + ); + $authenticationStrategies[] = new Reference($registerSessionStrategyId); + } + + $container + ->setDefinition( + $sessionStrategyId, new DefinitionDecorator( + 'security.authentication.session_strategy.composite' + ) + ) + ->replaceArgument(0, $authenticationStrategies) + ; + + return $sessionStrategyId; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/SessionRegistryGarbageCollectorListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/SessionRegistryGarbageCollectorListener.php new file mode 100644 index 0000000000000..e8e3750dc0b8e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/SessionRegistryGarbageCollectorListener.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpKernel\Event\PostResponseEvent; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\Security\Http\Session\SessionRegistry; + +/** + * Clear session information from registry for idle sessions + * + * @author Antonio J. García Lagar + */ +class SessionRegistryGarbageCollectorListener implements EventSubscriberInterface +{ + /** + * @var SessionRegistry + */ + private $sessionRegistry; + private $maxLifetime; + private $probability; + private $divisor; + + public function __construct(SessionRegistry $sessionRegistry, $maxLifetime = 1, $probability = null, $divisor = null) + { + $this->sessionRegistry = $sessionRegistry; + $this->maxLifetime = $maxLifetime ?: ini_get('session.gc_maxlifetime'); + $this->probability = $probability ?: ini_get('session.gc_probability'); + $this->divisor = $divisor ?: ini_get('session.gc_divisor'); + } + + public function onKernelTerminate(PostResponseEvent $event) + { + if ($this->probability / $this->divisor > lcg_value() || true) { + $this->sessionRegistry->collectGarbage($this->maxLifetime); + } + } + + public static function getSubscribedEvents() + { + return array( + KernelEvents::TERMINATE => array(array('onKernelTerminate')), + ); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 7d3ba1a6f322c..fa12c1903d434 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -28,6 +28,8 @@ Symfony\Component\Security\Http\Firewall\SwitchUserListener + Symfony\Component\Security\Http\Firewall\SessionExpirationListener + Symfony\Component\Security\Http\Firewall\LogoutListener Symfony\Component\Security\Http\Logout\SessionLogoutHandler Symfony\Component\Security\Http\Logout\CookieClearingLogoutHandler @@ -257,6 +259,16 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml new file mode 100644 index 0000000000000..7b410605e9318 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml @@ -0,0 +1,46 @@ + + + + + + Symfony\Component\Security\Http\Session\ConcurrentSessionControlAuthenticationStrategy + Symfony\Component\Security\Http\Session\RegisterSessionAuthenticationStrategy + Symfony\Component\Security\Http\Session\CompositeSessionAuthenticationStrategy + Symfony\Component\Security\Http\Session\SessionRegistry + Symfony\Bundle\SecurityBundle\EventListener\SessionRegistryGarbageCollectorListener + Symfony\Component\Security\Http\Session\FileSessionRegistryStorage + + + + + + + + + + + + + + + + + + + + + + + + %kernel.cache_dir%/session_registry + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionConcurrencyTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionConcurrencyTest.php new file mode 100644 index 0000000000000..1ea1ea4f7747c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionConcurrencyTest.php @@ -0,0 +1,81 @@ + + * + * 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; + +/** + * @author Antonio J. García Lagar + * @group functional + */ +class SessionConcurrencyTest extends WebTestCase +{ + public function testLoginWorksWhenConcurrentSessionsLesserThanMaximun() + { + $client = $this->createClient(array('test_case' => 'SessionExpiration', 'root_config' => 'session_concurrency.yml')); + $form = $client->request('GET', '/login')->selectButton('login')->form(); + $form['_username'] = 'antonio'; + $form['_password'] = 'secret'; + $client->submit($form); + + $this->assertRedirect($client->getResponse(), '/profile'); + } + + public function testLoginFailsWhenConcurrentSessionsGreaterOrEqualThanMaximun() + { + $client1 = $this->createClient(array('test_case' => 'SessionExpiration', 'root_config' => 'session_concurrency.yml')); + $client1->insulate(); + $form1 = $client1->request('GET', '/login')->selectButton('login')->form(); + $form1['_username'] = 'antonio'; + $form1['_password'] = 'secret'; + $client1->submit($form1); + + $client2 = $this->createClient(array('test_case' => 'SessionExpiration', 'root_config' => 'session_concurrency.yml')); + $client2->insulate(); + $form2 = $client2->request('GET', '/login')->selectButton('login')->form(); + $form2['_username'] = 'antonio'; + $form2['_password'] = 'secret'; + $client2->submit($form2); + + $this->assertRedirect($client2->getResponse(), '/login'); + } + + public function testOldSessionExpiresConcurrentSessionsGreaterOrEqualThanMaximun() + { + $client1 = $this->createClient(array('test_case' => 'SessionExpiration', 'root_config' => 'session_concurrency_expiration.yml')); + $form1 = $client1->request('GET', '/login')->selectButton('login')->form(); + $form1['_username'] = 'antonio'; + $form1['_password'] = 'secret'; + $client1->submit($form1); + $this->assertRedirect($client1->getResponse(), '/profile'); + + $client2 = $this->createClient(array('test_case' => 'SessionExpiration', 'root_config' => 'session_concurrency_expiration.yml')); + $client2->insulate(); + $form2 = $client2->request('GET', '/login')->selectButton('login')->form(); + $form2['_username'] = 'antonio'; + $form2['_password'] = 'secret'; + $client2->submit($form2); + $this->assertRedirect($client2->getResponse(), '/profile'); + + $client1->request('GET', '/profile'); + $this->assertEquals(200, $client1->getResponse()->getStatusCode()); + $sessionRegistry = $client1->getContainer()->get('security.session_registry'); + $session1Infomation = $sessionRegistry->getSessionInformation($client1->getRequest()->getSession()->getId()); + sleep(1); //Waiting for the session to expire + $this->assertTrue($session1Infomation->isExpired()); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('SessionExpiration'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionExpirationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionExpirationTest.php new file mode 100644 index 0000000000000..4558ee0e72de9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionExpirationTest.php @@ -0,0 +1,43 @@ + + * + * 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; + +/** + * @author Antonio J. García Lagar + * @group functional + */ +class SessionExpirationTest extends WebTestCase +{ + public function testExpiredExceptionRedirectsToTargetUrl() + { + $client = $this->createClient(array('test_case' => 'SessionExpiration', 'root_config' => 'config.yml')); + $form = $client->request('GET', '/login')->selectButton('login')->form(); + $form['_username'] = 'antonio'; + $form['_password'] = 'secret'; + $client->submit($form); + $this->assertRedirect($client->getResponse(), '/profile'); + + $client->request('GET', '/protected_resource'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + sleep(3); //Wait for session to expire + $client->request('GET', '/protected_resource'); + $this->assertRedirect($client->getResponse(), '/expired'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('SessionExpiration'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SessionExpiration/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SessionExpiration/bundles.php new file mode 100644 index 0000000000000..e4bbc08f73ff4 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SessionExpiration/bundles.php @@ -0,0 +1,13 @@ + +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ +namespace Symfony\Component\Security\Core\Exception; + +/** +* This exception is thrown when the user has exceeded the allowed number of +* sessions, and the ConcurrentSessionControlStrategy is set to limit the number +* by disallowing opening new sessions. +* +* @author Stefan Paschke +*/ +class MaxSessionsExceededException extends AuthenticationException +{ +} diff --git a/src/Symfony/Component/Security/Core/Exception/SessionExpiredException.php b/src/Symfony/Component/Security/Core/Exception/SessionExpiredException.php new file mode 100644 index 0000000000000..1fd2c5f1bd8e1 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Exception/SessionExpiredException.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * SessionExpiredException is thrown when session has been idle for a long time. + * + * @author Antonio J. García Lagar + */ +class SessionExpiredException extends AuthenticationException +{ + /** + * {@inheritdoc} + */ + public function getMessageKey() + { + return 'Session has expired.'; + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/SessionExpirationListener.php b/src/Symfony/Component/Security/Http/Firewall/SessionExpirationListener.php new file mode 100644 index 0000000000000..c99f196de2317 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/SessionExpirationListener.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Firewall; + +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Log\LoggerInterface; +use Symfony\Component\Security\Core\Exception\SessionExpiredException; +use Symfony\Component\Security\Http\HttpUtils; +use Symfony\Component\HttpFoundation\Session\SessionInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Http\Session\SessionRegistry; +use Symfony\Component\Security\Http\Session\SessionInformation; + +/** + * SessionExpirationListener controls idle sessions + * + * @author Antonio J. García Lagar + */ +class SessionExpirationListener implements ListenerInterface +{ + private $tokenStorage; + private $httpUtils; + private $maxIdleTime; + private $targetUrl; + private $sessionRegistry; + private $logger; + + public function __construct(TokenStorageInterface $tokenStorage, HttpUtils $httpUtils, $maxIdleTime, $targetUrl = null, SessionRegistry $sessionRegistry = null, LoggerInterface $logger = null) + { + $this->tokenStorage = $tokenStorage; + $this->httpUtils = $httpUtils; + $this->maxIdleTime = $maxIdleTime; + $this->targetUrl = $targetUrl; + $this->sessionRegistry = $sessionRegistry; + $this->logger = $logger; + } + + /** + * Handles expired sessions. + * + * @param GetResponseEvent $event A GetResponseEvent instance + * @throws SessionExpiredException If the session has expired + */ + public function handle(GetResponseEvent $event) + { + $request = $event->getRequest(); + $session = $request->getSession(); + + if (null === $session || null === $token = $this->tokenStorage->getToken()) { + return; + } + + $sessionInformation = null !== $this->sessionRegistry ? $this->sessionRegistry->getSessionInformation($session->getId()) : null; + + if (!$this->hasSessionExpired($session, $sessionInformation)) { + return; + } + + if (null !== $this->logger) { + $this->logger->info(sprintf("Expired session detected for user named '%s'", $token->getUsername())); + } + + $this->tokenStorage->setToken(null); + $session->invalidate(); + + if (null !== $this->sessionRegistry && null !== $sessionInformation) { + $this->sessionRegistry->removeSessionInformation($sessionInformation->getSessionId()); + } + + if (null === $this->targetUrl) { + throw new SessionExpiredException(); + } + + $response = $this->httpUtils->createRedirectResponse($request, $this->targetUrl); + $event->setResponse($response); + } + + /** + * Checks if the given session has expired. + * + * @param SessionInterface $session + * @param SessionInformation $sessionInformation + * @return bool + */ + private function hasSessionExpired(SessionInterface $session, SessionInformation $sessionInformation = null) + { + if (time() - $session->getMetadataBag()->getLastUsed() > $this->maxIdleTime) { + return true; + } + + if (null !== $sessionInformation) { + return $sessionInformation->isExpired(); + } + + return false; + } +} diff --git a/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php new file mode 100644 index 0000000000000..fc3ac355cedeb --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * A session authentication strategy that accepts multiple + * SessionAuthenticationStrategyInterface implementations to delegate to. + * + * Each SessionAuthenticationStrategyInterface is invoked in turn. The + * invocations are short circuited if any exception is thrown. + * + * @author Antonio J. García Lagar + */ +class CompositeSessionAuthenticationStrategy implements SessionAuthenticationStrategyInterface +{ + /** + * @var SessionAuthenticationStrategyInterface[] + */ + private $delegateStrategies = array(); + + public function __construct(array $delegateStrategies) + { + foreach ($delegateStrategies as $strategy) { + $this->addDelegateStrategy($strategy); + } + } + + /** + * {@inheritdoc} + */ + public function onAuthentication(Request $request, TokenInterface $token) + { + foreach ($this->delegateStrategies as $strategy) { + $strategy->onAuthentication($request, $token); + } + } + + private function addDelegateStrategy(SessionAuthenticationStrategyInterface $strategy) + { + $this->delegateStrategies[] = $strategy; + } +} diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php new file mode 100644 index 0000000000000..927a623c14732 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\MaxSessionsExceededException; + +/** + * Strategy which handles concurrent session-control. + * + * When invoked following an authentication, it will check whether the user in + * question should be allowed to proceed, by comparing the number of sessions + * they already have active with the configured maximumSessions value. + * The SessionRegistry is used as the source of data on authenticated users and + * session data. + * + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ +class ConcurrentSessionControlAuthenticationStrategy implements SessionAuthenticationStrategyInterface +{ + protected $registry; + protected $errorIfMaximumExceeded; + protected $maximumSessions; + + public function __construct(SessionRegistry $registry, $maximumSessions, $errorIfMaximumExceeded = true) + { + $this->registry = $registry; + $this->setMaximumSessions($maximumSessions); + $this->setErrorIfMaximumExceeded($errorIfMaximumExceeded); + } + + /** + * {@inheritdoc} + */ + public function onAuthentication(Request $request, TokenInterface $token) + { + $username = $token->getUsername(); + + $sessions = $this->registry->getAllSessionsInformation($username); + $sessionCount = count($sessions); + $maxSessions = $this->getMaximumSessionsForThisUser($username); + + if ($sessionCount < $maxSessions) { + return; + } + + if ($sessionCount == $maxSessions) { + foreach ($sessions as $sessionInfo) { + /* @var $sessionInfo SessionInformation */ + if ($sessionInfo->getSessionId() == $request->getSession()->getId()) { + return; + } + } + } + + $this->allowedSessionsExceeded($sessions, $maxSessions); + } + + /** + * Sets a boolean flag that causes a RuntimeException to be thrown if the number of sessions is exceeded. + * + * @param bool $errorIfMaximumExceeded + */ + public function setErrorIfMaximumExceeded($errorIfMaximumExceeded) + { + $this->errorIfMaximumExceeded = (bool) $errorIfMaximumExceeded; + } + + /** + * Sets the maxSessions property. + * + * @param $maximumSessions + */ + public function setMaximumSessions($maximumSessions) + { + $this->maximumSessions = (int) $maximumSessions; + } + + /** + * Allows subclasses to customize behavior when too many sessions are detected. + * + * @param array $orderedSessions Array of SessionInformation ordered from + * newest to oldest + * @param int $allowableSessions + */ + protected function allowedSessionsExceeded($orderedSessions, $allowableSessions) + { + if ($this->errorIfMaximumExceeded) { + throw new MaxSessionsExceededException(sprintf('Maximum number of sessions (%s) exceeded', $allowableSessions)); + } + + // Expire oldest session + $orderedSessionsVector = array_values($orderedSessions); + for ($i = $allowableSessions - 1, $countSessions = count($orderedSessionsVector); $i < $countSessions; $i++) { + $this->registry->expireNow($orderedSessionsVector[$i]->getSessionId()); + } + } + + /** + * Method intended for use by subclasses to override the maximum number of sessions that are permitted for a particular authentication. + * + * @param string $username + * @return int + */ + protected function getMaximumSessionsForThisUser($username) + { + return $this->maximumSessions; + } +} diff --git a/src/Symfony/Component/Security/Http/Session/FileSessionRegistryStorage.php b/src/Symfony/Component/Security/Http/Session/FileSessionRegistryStorage.php new file mode 100644 index 0000000000000..ea7b13ff9c626 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/FileSessionRegistryStorage.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +/** + * FileSessionRegistryStorage stores the session information in the filesystem. + * + * Useful for functional tests. + * + * @author Antonio J. García Lagar + */ +class FileSessionRegistryStorage implements SessionRegistryStorageInterface +{ + private $savePath; + + /** + * @param string $savePath + */ + public function __construct($savePath = null) + { + if (null === $savePath) { + $savePath = sys_get_temp_dir(); + } + + if (!is_dir($savePath)) { + mkdir($savePath, 0777, true); + } + + $this->savePath = $savePath; + } + + /** + * {@inheritdoc} + */ + public function getSessionInformation($sessionId) + { + $filename = $this->getFilePath($sessionId); + if (file_exists($filename)) { + return $this->fileToSessionInfo($filename); + } + } + + /** + * {@inheritdoc} + */ + public function getAllSessionsInformation($username, $includeExpiredSessions = false) + { + $result = array(); + + foreach (glob($this->getFilePath('*')) as $filename) { + $sessionInfo = $this->fileToSessionInfo($filename); + if ($sessionInfo->getUsername() == $username && ($includeExpiredSessions || !$sessionInfo->isExpired())) { + $result[] = $sessionInfo; + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function setSessionInformation(SessionInformation $sessionInformation) + { + file_put_contents($this->getFilePath($sessionInformation->getSessionId()), serialize($sessionInformation)); + } + + /** + * {@inheritdoc} + */ + public function removeSessionInformation($sessionId) + { + $filename = $this->getFilePath($sessionId); + if (file_exists($filename)) { + unlink($filename); + } + } + + /** + * {@inheritdoc} + */ + public function collectGarbage($maxLifetime) + { + $now = time(); + foreach (glob($this->getFilePath('*')) as $filename) { + $sessionInfo = $this->fileToSessionInfo($filename); + if ($now - $sessionInfo->getLastUsed() > $maxLifetime) { + $this->removeSessionInformation($sessionInfo->getSessionId()); + } + } + } + + /** + * @param string $sessionId + * @return string + */ + private function getFilePath($sessionId) + { + return $this->savePath.'/'.$sessionId.'.mocksessinfo'; + } + + /** + * @param string $filename + * @return SessionInformation + */ + private function fileToSessionInfo($filename) + { + return unserialize(file_get_contents($filename)); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php new file mode 100644 index 0000000000000..560d47bcf5020 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Strategy used to register a user with the SessionRegistry after + * successful authentication. + * + * @author Antonio J. García Lagar + */ +class RegisterSessionAuthenticationStrategy implements SessionAuthenticationStrategyInterface +{ + /** + * @var SessionRegistry + */ + private $registry; + + public function __construct(SessionRegistry $registry) + { + $this->registry = $registry; + } + + /** + * {@inheritdoc} + */ + public function onAuthentication(Request $request, TokenInterface $token) + { + if ($session = $request->getSession()) { + $this->registry->registerNewSession($token->getUsername(), $session); + } + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php new file mode 100644 index 0000000000000..aba379564da92 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +/** + * SessionInformation. + * + * Represents a record of a session. This is primarily used for concurrent session support. + * + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ +class SessionInformation +{ + private $username; + private $sessionId; + private $expired; + private $lastUsed; + + public function __construct($username, $sessionId, $lastUsed, $expired = null) + { + $this->sessionId = (string) $sessionId; + $this->username = (string) $username; + $this->lastUsed = (int) $lastUsed; + + if (null !== $expired) { + $this->expired = (int) $expired; + } + } + + /** + * Sets the session expiration timestamp. + */ + public function expireAt($expired) + { + $this->expired = (int) $expired; + } + + /** + * Set the last used timestamp. + */ + public function updateLastUsed($lastUsed) + { + $this->lastUsed = (int) $lastUsed; + } + + /** + * Obtain the last used timestamp. + * + * @return int the last request timestamp. + */ + public function getLastUsed() + { + return $this->lastUsed; + } + + /** + * Gets the username. + * + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * Gets the session identifier key. + * + * @return string $sessionId the session identifier key. + */ + public function getSessionId() + { + return $this->sessionId; + } + + /** + * Return whether this session is expired. + * + * @return bool + */ + public function isExpired() + { + return null !== $this->expired && $this->expired < time(); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php new file mode 100644 index 0000000000000..a851d4f4b491d --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +/** + * SessionRegistry. + * + * Stores a registry of SessionInformation instances. + * + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ +class SessionRegistry +{ + private $sessionRegistryStorage; + + public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage) + { + $this->sessionRegistryStorage = $sessionRegistryStorage; + } + + /** + * Registers a new session for the given user. + * + * @param string $username the given user. + * @param SessionInterface $session the session. + */ + public function registerNewSession($username, SessionInterface $session) + { + $sessionInformation = new SessionInformation($username, $session->getId(), $session->getMetadataBag()->getLastUsed()); + $this->setSessionInformation($sessionInformation); + } + + /** + * Registers session information for the given user. + * + * @param string $username the given user. + * @param string $sessionId the session identifier key. + * @param int $lastUsed + */ + public function registerNewSessionInformation($username, $sessionId, $lastUsed = null) + { + $lastUsed = $lastUsed ?: time(); + $sessionInformation = new SessionInformation($username, $sessionId, $lastUsed); + $this->setSessionInformation($sessionInformation); + } + + /** + * Returns all the sessions stored for the given user ordered from + * MRU (most recently used) to LRU (least recently used). + * + * @param string $username the given user. + * @param bool $includeExpiredSessions + * @return SessionInformation[] An array of SessionInformation objects. + */ + public function getAllSessionsInformation($username, $includeExpiredSessions = false) + { + return $this->sessionRegistryStorage->getAllSessionsInformation($username, $includeExpiredSessions); + } + + /** + * Obtains the session information for the given sessionId. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation|null $sessionInformation + */ + public function getSessionInformation($sessionId) + { + return $this->sessionRegistryStorage->getSessionInformation($sessionId); + } + + /** + * Updates the last used timestamp for the given session Id. + * + * @param string $sessionId the session identifier key. + * @parma int $lastUsed if null, current timestamp will be set. + */ + public function updateLastUsed($sessionId, $lastUsed = null) + { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + $lastUsed = $lastUsed ?: time(); + //prevents too many trips to session storage + if ($lastUsed != $sessionInformation->getLastUsed()) { + $sessionInformation->updateLastUsed($lastUsed); + $this->setSessionInformation($sessionInformation); + } + } + } + + /** + * Expires the given sessionId at the give timestamp. + * + * @param string $sessionId the session identifier key. + * @parma int $expired the expiration timestamp. + */ + public function expireAt($sessionId, $expired) + { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + $sessionInformation->expireAt($expired); + $this->setSessionInformation($sessionInformation); + } + } + + /** + * Expires the given sessionId. + * + * @param string $sessionId the session identifier key. + */ + public function expireNow($sessionId) + { + $this->expireAt($sessionId, time()); + } + + /** + * Deletes the stored information of one session. + * + * @param string $sessionId the session identifier key. + */ + public function removeSessionInformation($sessionId) + { + $this->sessionRegistryStorage->removeSessionInformation($sessionId); + } + + /** + * Removes sessions information which last used timestamp is older + * than the given lifetime + * + * @param int $maxLifetime + */ + public function collectGarbage($maxLifetime = null) + { + $maxLifetime = $maxLifetime ?: ini_get('session.gc_maxlifetime'); + $this->sessionRegistryStorage->collectGarbage($maxLifetime); + } + + /** + * Sets a SessionInformation object. + * + * @param SessionInformation $sessionInformation + */ + private function setSessionInformation(SessionInformation $sessionInformation) + { + $this->sessionRegistryStorage->setSessionInformation($sessionInformation); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php new file mode 100644 index 0000000000000..68897df583452 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +/** + * SessionRegistryStorageInterface. + * + * Stores the SessionInformation instances maintained in the SessionRegistry. + * + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ +interface SessionRegistryStorageInterface +{ + /** + * Obtains the session information for the specified sessionId. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation|null $sessionInformation + */ + public function getSessionInformation($sessionId); + + /** + * Returns all the sessions stored for the given user ordered from + * MRU (most recently used) to LRU (least recently used). + * + * @param string $username The user identifier. + * @param bool $includeExpiredSessions + * @return SessionInformation[] An array of SessionInformation objects. + */ + public function getAllSessionsInformation($username, $includeExpiredSessions = false); + + /** + * Sets a SessionInformation object. + * + * @param SessionInformation $sessionInformation + */ + public function setSessionInformation(SessionInformation $sessionInformation); + + /** + * Deletes the maintained information of one session. + * + * @param string $sessionId the session identifier key. + */ + public function removeSessionInformation($sessionId); + + /** + * Removes sessions information which last used timestamp is older + * than the given lifetime + * + * @param int $maxLifetime + */ + public function collectGarbage($maxLifetime); +} diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SessionExpirationListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SessionExpirationListenerTest.php new file mode 100644 index 0000000000000..527e559cb78ef --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SessionExpirationListenerTest.php @@ -0,0 +1,227 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Firewall; + +use Symfony\Component\Security\Http\Firewall\SessionExpirationListener; + +/** + * @author Antonio J. García Lagar + */ +class SessionExpirationListenerTest extends \PHPUnit_Framework_TestCase +{ + public function testHandleWhenNoSession() + { + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->once()) + ->method('getSession') + ->will($this->returnValue(false)); + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)); + + $listener = new SessionExpirationListener( + $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'), + $this->getHttpUtils(), + 1440 + ); + + $this->assertNull($listener->handle($event)); + } + + public function testHandleWhenNoToken() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)); + + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $securityContext + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue(null)); + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)); + + $listener = new SessionExpirationListener( + $securityContext, + $this->getHttpUtils(), + 1440 + ); + + $this->assertNull($listener->handle($event)); + } + + public function testHandleWhenSessionHasNotExpired() + { + $metadataBag = $this->getMock('\Symfony\Component\HttpFoundation\Session\Storage\MetadataBag'); + $metadataBag + ->expects($this->once()) + ->method('getLastUsed') + ->will($this->returnValue(time())); + + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session + ->expects($this->any()) + ->method('getMetadataBag') + ->will($this->returnValue($metadataBag)); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)); + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $securityContext + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)); + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)); + + $listener = new SessionExpirationListener( + $securityContext, + $this->getHttpUtils(), + 1440 + ); + + $this->assertNull($listener->handle($event)); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\SessionExpiredException + */ + public function testHandleWhenSessionHasExpiredAndNoTargetUrl() + { + $metadataBag = $this->getMock('\Symfony\Component\HttpFoundation\Session\Storage\MetadataBag'); + $metadataBag + ->expects($this->once()) + ->method('getLastUsed') + ->will($this->returnValue(time()-2)); + + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session + ->expects($this->any()) + ->method('getMetadataBag') + ->will($this->returnValue($metadataBag)); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)); + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $securityContext + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)); + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)); + + $listener = new SessionExpirationListener( + $securityContext, + $this->getHttpUtils(), + 1 + ); + + $listener->handle($event); + } + + public function testHandleWhenSessionHasExpiredAndTargetUrl() + { + $metadataBag = $this->getMock('\Symfony\Component\HttpFoundation\Session\Storage\MetadataBag'); + $metadataBag + ->expects($this->once()) + ->method('getLastUsed') + ->will($this->returnValue(time()-2)); + + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session + ->expects($this->any()) + ->method('getMetadataBag') + ->will($this->returnValue($metadataBag)); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)); + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $securityContext + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)); + + $response = $this->getMock('Symfony\Component\HttpFoundation\Response'); + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)); + $event + ->expects($this->once()) + ->method('setResponse') + ->with($this->identicalTo($response)); + + $httpUtils = $this->getHttpUtils(); + $httpUtils + ->expects($this->once()) + ->method('createRedirectResponse') + ->with($this->identicalTo($request), $this->equalTo('/expired')) + ->will($this->returnValue($response)); + + $listener = new SessionExpirationListener( + $securityContext, + $httpUtils, + 1, + '/expired' + ); + + $listener->handle($event); + } + + private function getHttpUtils() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\HttpUtils') + ->disableOriginalConstructor() + ->getMock(); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php new file mode 100644 index 0000000000000..15d7ac3ecf71b --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\CompositeSessionAuthenticationStrategy; + +/** + * @author Antonio J. García Lagar + */ +class CompositeSessionAuthenticationStrategyTest extends \PHPUnit_Framework_TestCase +{ + public function testAuthenticationDelegation() + { + $strategies = array( + $this->getDelegateAuthenticationStrategy(), + $this->getDelegateAuthenticationStrategy(), + $this->getDelegateAuthenticationStrategy(), + ); + + $request = $this->getRequest(); + + $strategy = new CompositeSessionAuthenticationStrategy($strategies); + $strategy->onAuthentication($request, $this->getToken()); + } + + private function getRequest() + { + return $this->getMock('Symfony\Component\HttpFoundation\Request'); + } + + private function getToken() + { + return $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + } + + private function getDelegateAuthenticationStrategy() + { + $strategy = $this->getMock('Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface'); + $strategy->expects($this->once())->method('onAuthentication'); + + return $strategy; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php new file mode 100644 index 0000000000000..fd1ad5f7f5c11 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\ConcurrentSessionControlAuthenticationStrategy; + +/** + * @author Antonio J. García Lagar + */ +class ConcurrentSessionControlAuthenticationStrategyTest extends \PHPUnit_Framework_TestCase +{ + public function testSessionsCountLesserThanAllowed() + { + $request = $this->getRequest($this->getSession()); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once()) + ->method('getAllSessionsInformation') + ->will($this->returnValue(array( + $this->getSessionInformation(), + $this->getSessionInformation(), + ))); + + $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 3); + $this->assertNull($strategy->onAuthentication($request, $this->getToken())); + } + + public function testSessionsCountEqualsThanAllowedWithRegisteredSession() + { + $request = $this->getRequest($this->getSession('bar')); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once()) + ->method('getAllSessionsInformation') + ->will($this->returnValue(array( + $this->getSessionInformation('bar'), + $this->getSessionInformation('foo'), + ))); + + $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 2); + $this->assertNull($strategy->onAuthentication($request, $this->getToken())); + } + + /** + * @expectedException Symfony\Component\Security\Core\Exception\MaxSessionsExceededException + * @expectedExceptionMessage Maximum number of sessions (2) exceeded + */ + public function testSessionsCountEqualsThanAllowedWithUnregisteredSession() + { + $request = $this->getRequest($this->getSession('foobar')); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once()) + ->method('getAllSessionsInformation') + ->will($this->returnValue(array( + $this->getSessionInformation('bar'), + $this->getSessionInformation('foo'), + ))); + + $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 2); + $this->assertNull($strategy->onAuthentication($request, $this->getToken())); + } + + public function testExpiresOldSessionsWhenNoExceptionIsThrownIfMaximunExceeded() + { + $request = $this->getRequest($this->getSession('foobar')); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once()) + ->method('getAllSessionsInformation') + ->will($this->returnValue(array( + $this->getSessionInformation('foo'), + $this->getSessionInformation('bar'), + $this->getSessionInformation('barfoo'), + ))); + + $registry->expects($this->at(1)) + ->method('expireNow') + ->with($this->equalTo('bar')); + $registry->expects($this->at(2)) + ->method('expireNow') + ->with($this->equalTo('barfoo')); + + $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 2, false); + $this->assertNull($strategy->onAuthentication($request, $this->getToken())); + } + + private function getSession($sessionId = null) + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + if (null !== $sessionId) { + $session->expects($this->any())->method('getId')->will($this->returnValue($sessionId)); + } + + return $session; + } + + private function getRequest($session = null) + { + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + + if (null !== $session) { + $request->expects($this->any())->method('getSession')->will($this->returnValue($session)); + } + + return $request; + } + + private function getToken() + { + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token->expects($this->any())->method('getUsername')->will($this->returnValue('foo')); + + return $token; + } + + private function getSessionRegistry() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionRegistry') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getSessionInformation($sessionId = null, $username = null) + { + $sessionInfo = $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionInformation') + ->disableOriginalConstructor() + ->getMock(); + + if (null !== $sessionId) { + $sessionInfo->expects($this->any())->method('getSessionId')->will($this->returnValue($sessionId)); + } + + if (null !== $username) { + $sessionInfo->expects($this->any())->method('getUsername')->will($this->returnValue($username)); + } + + return $sessionInfo; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/RegisterSessionAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/RegisterSessionAuthenticationStrategyTest.php new file mode 100644 index 0000000000000..4ff158ca007ad --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/RegisterSessionAuthenticationStrategyTest.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\RegisterSessionAuthenticationStrategy; + +/** + * @author Antonio J. García Lagar + */ +class RegisterSessionAuthenticationStrategyTest extends \PHPUnit_Framework_TestCase +{ + public function testRegisterSession() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $request = $this->getRequest($session); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once())->method('registerNewSession')->with($this->equalTo('foo'), $this->identicalTo($session)); + + $strategy = new RegisterSessionAuthenticationStrategy($registry); + $strategy->onAuthentication($request, $this->getToken()); + } + + private function getRequest($session = null) + { + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + + if (null !== $session) { + $request->expects($this->any())->method('getSession')->will($this->returnValue($session)); + } + + return $request; + } + + private function getToken() + { + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token->expects($this->any())->method('getUsername')->will($this->returnValue('foo')); + + return $token; + } + + private function getSessionRegistry() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionRegistry') + ->disableOriginalConstructor() + ->getMock(); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php new file mode 100644 index 0000000000000..1bf9c7dd2aa5a --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\SessionInformation; + +/** + * @author Antonio J. García Lagar + */ +class SessionInformationTest extends \PHPUnit_Framework_TestCase +{ + public function testExpiration() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertFalse($sessionInfo->isExpired()); + $sessionInfo->expireAt(time() - 1); + + $this->assertTrue($sessionInfo->isExpired()); + } + + public function testUpdateLastUsed() + { + $now = time() - 5; + $sessionInfo = $this->getSessionInformation(); + $this->assertNotEquals($now, $sessionInfo->getLastUsed()); + $sessionInfo->updateLastUsed($now); + $this->assertEquals($now, $sessionInfo->getLastUsed()); + } + + public function testGetLastUsed() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertLessThan(microtime(true), $sessionInfo->getLastUsed()); + } + + public function testGetSessionId() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertEquals('bar', $sessionInfo->getSessionId()); + } + + public function testGetUsername() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertEquals('foo', $sessionInfo->getUsername()); + } + + /** + * @return \Symfony\Component\Security\Http\Session\SessionInformation + */ + private function getSessionInformation() + { + return new SessionInformation('foo', 'bar', time()); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php new file mode 100644 index 0000000000000..a05aa7e10a89f --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\SessionRegistry; + +/** + * @author Antonio J. García Lagar + */ +class SessionRegistryTest extends \PHPUnit_Framework_TestCase +{ + public function testGetAllSessionsInformation() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('getAllSessionsInformation')->with('foo', true); + $registry = $this->getSessionRegistry($storage); + $registry->getAllSessionsInformation('foo', true); + } + + public function testGetSessionInformation() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('getSessionInformation')->with('foobar'); + $registry = $this->getSessionRegistry($storage); + $registry->getSessionInformation('foobar'); + } + + public function testUpdateLastUsed() + { + $sessionInformation = $this->getSessionInformation(); + $sessionInformation->expects($this->once())->method('updateLastUsed'); + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->any())->method('getSessionInformation')->with('foobar')->will($this->returnValue($sessionInformation)); + $storage->expects($this->once())->method('setSessionInformation')->with($sessionInformation); + $registry = $this->getSessionRegistry($storage); + $registry->updateLastUsed('foobar', time() - 5); + } + + public function testExpireAt() + { + $sessionInformation = $this->getSessionInformation(); + $sessionInformation->expects($this->once())->method('expireAt'); + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->any())->method('getSessionInformation')->with('foobar')->will($this->returnValue($sessionInformation)); + $storage->expects($this->once())->method('setSessionInformation')->with($this->identicalTo($sessionInformation)); + $registry = $this->getSessionRegistry($storage); + $registry->expireAt('foobar', time() - 5); + } + + public function testExpireNow() + { + $sessionInformation = $this->getSessionInformation(); + $sessionInformation->expects($this->once())->method('expireAt'); + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->any())->method('getSessionInformation')->with('foobar')->will($this->returnValue($sessionInformation)); + $storage->expects($this->once())->method('setSessionInformation')->with($this->identicalTo($sessionInformation)); + $registry = $this->getSessionRegistry($storage); + $registry->expireNow('foobar'); + } + + public function testRegisterNewSessionInformation() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('setSessionInformation')->with($this->isInstanceOf('Symfony\Component\Security\Http\Session\SessionInformation')); + $registry = $this->getSessionRegistry($storage); + $registry->registerNewSessionInformation('foo', 'bar', time()); + } + + public function testRegisterNewSession() + { + $metadata = $this->getMock('Symfony\Component\HttpFoundation\Session\Storage\MetadataBag'); + $metadata->expects($this->once())->method('getLastUsed')->will($this->returnValue(time() - 5)); + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session->expects($this->any())->method('getId')->will($this->returnValue('foobar')); + $session->expects($this->any())->method('getMetadataBag')->will($this->returnValue($metadata)); + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('setSessionInformation')->with($this->isInstanceOf('Symfony\Component\Security\Http\Session\SessionInformation')); + $registry = $this->getSessionRegistry($storage); + $registry->registerNewSession('foo', $session); + } + + public function testRemoveSessionInformation() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('removeSessionInformation')->with('foobar'); + $registry = $this->getSessionRegistry($storage); + $registry->removeSessionInformation('foobar'); + } + + public function testCollectGarbage() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('collectGarbage')->with(ini_get('session.gc_maxlifetime')); + $registry = $this->getSessionRegistry($storage); + $registry->collectGarbage(); + } + + private function getSessionRegistryStorage() + { + return $this->getMock('Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface'); + } + + private function getSessionInformation() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionInformation') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getSessionRegistry($storage) + { + return new SessionRegistry($storage, 'Symfony\Component\Security\Http\Session\SessionInformation'); + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy