diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php new file mode 100644 index 0000000000000..0fb2c14a2ddba --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Security\SessionRegistry; + +use Doctrine\DBAL\Schema\Schema as BaseSchema; + +/** + * The schema used for the ACL system. + * + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ +final class Schema extends BaseSchema +{ + /** + * @param string $table The name of the table to create. + */ + public function __construct($table) + { + parent::__construct(); + + $this->addSessionInformationTable($table); + } + + /** + * Adds the session_information table to the schema + * + * @param string $table The name of the table to create. + */ + private function addSessionInformationTable($table) + { + $table = $this->createTable($table); + $table->addColumn('session_id', 'string'); + $table->addColumn('username', 'string'); + $table->addColumn('expired', 'datetime', array('unsigned' => true, 'notnull' => false)); + $table->addColumn('last_request', 'datetime', array('unsigned' => true, 'notnull' => false)); + $table->setPrimaryKey(array('session_id')); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php new file mode 100644 index 0000000000000..1d60a91b21fc0 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -0,0 +1,190 @@ + + * @author Antonio J. García Lagar + */ +class SessionRegistryStorage implements SessionRegistryStorageInterface +{ + private $connection; + private $table; + + /** + * @param Connection $connection The DB connection + * @param string $table The table name to store session information + */ + public function __construct(Connection $connection, $table) + { + $this->connection = $connection; + $this->table = $table; + } + + /** + * Gets the stored information for the given session. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation a SessionInformation object. + */ + public function getSessionInformation($sessionId) + { + $statement = $this->connection->executeQuery( + 'SELECT * FROM '.$this->table.' WHERE session_id = :session_id', + array('session_id' => $sessionId) + ); + + $data = $statement->fetch(\PDO::FETCH_ASSOC); + + return $data ? $this->instantiateSessionInformationFromResultSet($data) : null; + } + + /** + * Gets the stored sessions information for the given username. + * + * @param string $username The user identifier. + * @param bool $includeExpiredSessions If true, expired sessions information is included. + * @return SessionInformations[] An array of SessionInformation objects. + */ + public function getSessionInformations($username, $includeExpiredSessions = false) + { + $sessionInformations = array(); + + $statement = $this->connection->executeQuery( + 'SELECT * + FROM '.$this->table.' + WHERE username = :username'.($includeExpiredSessions ? '' : ' AND expired IS NULL ').' + ORDER BY last_request DESC', + array('username' => $username) + ); + + while ($data = $statement->fetch(\PDO::FETCH_ASSOC)) { + $sessionInformations[] = $this->instantiateSessionInformationFromResultSet($data); + } + + $statement->closeCursor(); + + return $sessionInformations; + } + + /** + * Adds information for one session. + * + * @param SessionInformation a SessionInformation object. + */ + public function setSessionInformation(SessionInformation $sessionInformation) + { + $mergeSql = $this->getMergeSql(); + + if (null !== $mergeSql) { + $this->connection->executeQuery( + $mergeSql, + array( + 'session_id' => $sessionInformation->getSessionId(), + 'username' => $sessionInformation->getUsername(), + 'last_request' => $sessionInformation->getLastRequest(), + 'expired' => $sessionInformation->getExpired(), + ), + array( + 'last_request' => 'datetime', + 'expired' => 'datetime', + ) + ); + + return true; + } + + $updateStmt = $this->connection->prepare( + "UPDATE $this->table SET username=:username, last_request=:last_request, expired=:expired WHERE session_id = :session_id" + ); + $updateStmt->bindValue('session_id', $sessionInformation->getSessionId()); + $updateStmt->bindValue('username', $sessionInformation->getUsername()); + $updateStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); + $updateStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); + $updateStmt->execute(); + + // When MERGE is not supported, like in Postgres, we have to use this approach that can result in + // duplicate key errors when the same sessioninfo is written simultaneously. We can just catch such an + // error and re-execute the update. This is similar to a serializable transaction with retry logic + // on serialization failures but without the overhead and without possible false positives due to + // longer gap locking. + if (!$updateStmt->rowCount()) { + try { + $this->connection->insert( + $this->table, + array( + 'session_id' => $sessionInformation->getSessionId(), + 'username' => $sessionInformation->getUsername(), + 'last_request' => $sessionInformation->getLastRequest(), + 'expired' => $sessionInformation->getExpired(), + ), + array( + 'last_request' => 'datetime', + 'expired' => 'datetime', + ) + ); + } catch (DBALException $e) { + // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys + if ($e->getPrevious() instanceof \PDOException && 0 === strpos($e->getPrevious()->getCode(), '23')) { + $updateStmt->execute(); + } else { + throw $e; + } + } + } + } + + /** + * Deletes stored information of one session. + * + * @param string $sessionId the session identifier key. + */ + public function removeSessionInformation($sessionId) + { + $this->connection->delete($this->table, array('session_id' => $sessionId)); + } + + private function instantiateSessionInformationFromResultSet($data) + { + return new SessionInformation( + $data['session_id'], + $data['username'], + null === $data['last_request'] ? null : new \DateTime($data['last_request']), + null === $data['expired'] ? null : new \DateTime($data['expired']) + ); + } + + /** + * Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database. + * + * @return string|null The SQL string or null when not supported + */ + private function getMergeSql() + { + switch ($this->connection->getDatabasePlatform()->getName()) { + case 'mysql': + return "INSERT INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) ". + "ON DUPLICATE KEY UPDATE username = VALUES(username), last_request = VALUES(last_request), expired = VALUES(expired)"; + case 'oracle': + // DUAL is Oracle specific dummy table + return "MERGE INTO $this->table USING DUAL ON (session_id= :session_id) ". + "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) ". + "WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired"; + case 'mssql': + if ($this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SQLServer2008Platform || $this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform) { + // MERGE is only available since SQL Server 2008 and must be terminated by semicolon + // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx + return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON (session_id = :session_id) ". + "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) ". + "WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired;"; + } + case 'sqlite': + return "INSERT OR REPLACE INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired)"; + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php new file mode 100644 index 0000000000000..fa6203290350c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Bridge\Doctrine\Security\SessionRegistry\Schema; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Installs the database schema required by the concurrent session Doctrine implementation + * + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ +class InitConcurrentSessionsCommand extends ContainerAwareCommand +{ + /** + * @see Command + */ + protected function configure() + { + $this + ->setName('init:concurrent-session') + ->setDescription('Executes the SQL needed to generate the database schema required by the concurrent sessions feature.') + ->setHelp(<<init:concurrent-session command executes the SQL needed to +generate the database schema required by the concurrent session Doctrine implementation: + +./app/console init:concurrent-session +EOT + ); + } + + /** + * @see Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $connection = $this->getContainer()->get('security.session_registry.dbal.connection'); + $sm = $connection->getSchemaManager(); + $tableNames = $sm->listTableNames(); + $table = $this->getContainer()->getParameter('security.session_registry.dbal.session_information_table_name'); + + if (in_array($table, $tableNames, true)) { + $output->writeln(sprintf('The table "%s" already exists. Aborting.', $table)); + + return; + } + + $schema = new Schema($table); + foreach ($schema->toSql($connection->getDatabasePlatform()) as $sql) { + $connection->exec($sql); + } + + $output->writeln('concurrent session table have been initialized successfully.'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 3f25c3da03e4a..be6bac44b5d7c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -70,8 +70,7 @@ public function getConfigTreeBuilder() ->booleanNode('allow_if_equal_granted_denied')->defaultTrue()->end() ->end() ->end() - ->end() - ; + ->end(); $this->addAclSection($rootNode); $this->addEncodersSection($rootNode); @@ -79,10 +78,25 @@ public function getConfigTreeBuilder() $this->addFirewallsSection($rootNode, $this->factories); $this->addAccessControlSection($rootNode); $this->addRoleHierarchySection($rootNode); + $this->addSessionRegistrySection($rootNode); return $tb; } + private function addSessionRegistrySection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('session_registry') + ->children() + ->scalarNode('connection')->end() + ->scalarNode('table')->defaultValue('cs_session_information')->end() + ->scalarNode('session_registry_storage')->end() + ->end() + ->end() + ->end(); + } + private function addAclSection(ArrayNodeDefinition $rootNode) { $rootNode @@ -119,8 +133,7 @@ private function addAclSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() - ->end() - ; + ->end(); } private function addRoleHierarchySection(ArrayNodeDefinition $rootNode) @@ -140,8 +153,7 @@ private function addRoleHierarchySection(ArrayNodeDefinition $rootNode) ->prototype('scalar')->end() ->end() ->end() - ->end() - ; + ->end(); } private function addAccessControlSection(ArrayNodeDefinition $rootNode) @@ -180,8 +192,7 @@ private function addAccessControlSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() - ->end() - ; + ->end(); } private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $factories) @@ -195,8 +206,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->disallowNewKeysInSubsequentConfigs() ->useAttributeAsKey('name') ->prototype('array') - ->children() - ; + ->children(); $firewallNodeBuilder ->scalarNode('pattern')->end() @@ -288,15 +298,21 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end() ->end() ->end() - ; + ->arrayNode('session_concurrency') + ->canBeUnset() + ->children() + ->integerNode('max_sessions')->defaultValue(0)->min(0)->end() + ->booleanNode('error_if_maximum_exceeded')->defaultTrue()->end() + ->scalarNode('expiration_url')->defaultValue('/')->end() + ->end() + ->end(); $abstractFactoryKeys = array(); foreach ($factories as $factoriesAtPosition) { foreach ($factoriesAtPosition as $factory) { $name = str_replace('-', '_', $factory->getKey()); $factoryNode = $firewallNodeBuilder->arrayNode($name) - ->canBeUnset() - ; + ->canBeUnset(); if ($factory instanceof AbstractFactory) { $abstractFactoryKeys[] = $name; @@ -326,8 +342,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto return $firewall; }) - ->end() - ; + ->end(); } private function addProvidersSection(ArrayNodeDefinition $rootNode) @@ -351,8 +366,7 @@ private function addProvidersSection(ArrayNodeDefinition $rootNode) ->isRequired() ->requiresAtLeastOneElement() ->useAttributeAsKey('name') - ->prototype('array') - ; + ->prototype('array'); $providerNodeBuilder ->children() @@ -369,8 +383,7 @@ private function addProvidersSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() - ->end() - ; + ->end(); foreach ($this->userProviderFactories as $factory) { $name = str_replace('-', '_', $factory->getKey()); @@ -387,8 +400,7 @@ private function addProvidersSection(ArrayNodeDefinition $rootNode) ->validate() ->ifTrue(function ($v) {return count($v) === 0;}) ->thenInvalid('You must set a provider definition for the provider.') - ->end() - ; + ->end(); } private function addEncodersSection(ArrayNodeDefinition $rootNode) @@ -427,7 +439,6 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() - ->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..5d73d47d12833 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -59,8 +59,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProviderI if ($this->isRememberMeAware($config)) { $container ->getDefinition($listenerId) - ->addTag('security.remember_me_aware', array('id' => $id, 'provider' => $userProviderId)) - ; + ->addTag('security.remember_me_aware', array('id' => $id, 'provider' => $userProviderId)); } // create entry point if applicable (optional) @@ -77,8 +76,7 @@ public function addConfiguration(NodeDefinition $node) ->scalarNode('provider')->end() ->booleanNode('remember_me')->defaultTrue()->end() ->scalarNode('success_handler')->end() - ->scalarNode('failure_handler')->end() - ; + ->scalarNode('failure_handler')->end(); foreach (array_merge($this->options, $this->defaultSuccessHandlerOptions, $this->defaultFailureHandlerOptions) as $name => $default) { if (is_bool($default)) { @@ -157,6 +155,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..1c0d5f4e3a0fb 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'])) { + $this->sessionRegistryLoad($config['session_registry'], $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']); @@ -79,8 +83,7 @@ public function load(array $configs, ContainerBuilder $container) ->getDefinition('security.access.decision_manager') ->addArgument($config['access_decision_manager']['strategy']) ->addArgument($config['access_decision_manager']['allow_if_all_abstain']) - ->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']) - ; + ->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']); $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); @@ -147,8 +150,7 @@ private function configureDbalAclProvider(array $config, ContainerBuilder $conta 'connection' => $config['connection'], 'event' => 'postGenerateSchema', 'lazy' => true, - )) - ; + )); $container->getDefinition('security.acl.cache.doctrine')->addArgument($config['cache']['prefix']); @@ -159,6 +161,30 @@ private function configureDbalAclProvider(array $config, ContainerBuilder $conta $container->setParameter('security.acl.dbal.sid_table_name', $config['tables']['security_identity']); } + private function sessionRegistryLoad($config, ContainerBuilder $container, $loader) + { + $loader->load('security_session_concurrency.xml'); + + if (isset($config['session_registry_storage'])) { + $container->setAlias('security.authentication.session_registry_storage', $config['session_registry_storage']); + + return; + } + + $this->configureDbalSessionRegistryStorage($config, $container, $loader); + } + + private function configureDbalSessionRegistryStorage($config, ContainerBuilder $container, $loader) + { + $loader->load('security_session_registry_dbal.xml'); + + if (isset($config['connection'])) { + $container->setAlias('security.session_registry.dbal.connection', sprintf('doctrine.dbal.%s_connection', $config['connection'])); + } + + $container->setParameter('security.session_registry.dbal.session_information_table_name', $config['table']); + } + /** * Loads the web configuration. * @@ -236,8 +262,7 @@ private function createFirewalls($config, ContainerBuilder $container) $context = $container->setDefinition($contextId, new DefinitionDecorator('security.firewall.context')); $context ->replaceArgument(0, $listeners) - ->replaceArgument(1, $exceptionListener) - ; + ->replaceArgument(1, $exceptionListener); $map[$contextId] = $matcher; } $mapDef->replaceArgument(1, $map); @@ -248,8 +273,7 @@ private function createFirewalls($config, ContainerBuilder $container) }, array_values(array_unique($authenticationProviders))); $container ->getDefinition('security.authentication.manager') - ->replaceArgument(0, $authenticationProviders) - ; + ->replaceArgument(0, $authenticationProviders); } private function createFirewall(ContainerBuilder $container, $id, $firewall, &$authenticationProviders, $providerIds) @@ -304,14 +328,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a )); $listeners[] = new Reference($listenerId); - // add logout success handler - if (isset($firewall['logout']['success_handler'])) { - $logoutSuccessHandlerId = $firewall['logout']['success_handler']; - } else { - $logoutSuccessHandlerId = 'security.logout.success_handler.'.$id; - $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new DefinitionDecorator('security.logout.success_handler')); - $logoutSuccessHandler->replaceArgument(1, $firewall['logout']['target']); - } + $logoutSuccessHandlerId = $this->createLogoutSuccessHandler($container, $id, $firewall['logout']); $listener->replaceArgument(2, new Reference($logoutSuccessHandlerId)); // add CSRF provider @@ -319,24 +336,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $listener->addArgument(new Reference($firewall['logout']['csrf_token_generator'])); } - // add session logout handler - if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) { - $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); - } - - // add cookie logout handler - if (count($firewall['logout']['delete_cookies']) > 0) { - $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id; - $cookieHandler = $container->setDefinition($cookieHandlerId, new DefinitionDecorator('security.logout.handler.cookie_clearing')); - $cookieHandler->addArgument($firewall['logout']['delete_cookies']); - - $listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId))); - } - - // add custom handlers - foreach ($firewall['logout']['handlers'] as $handlerId) { - $listener->addMethodCall('addHandler', array(new Reference($handlerId))); - } + $this->addLogoutHandlers($container, $listenerId, $id, $firewall); // register with LogoutUrlHelper $container @@ -347,8 +347,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $firewall['logout']['csrf_token_id'], $firewall['logout']['csrf_parameter'], isset($firewall['logout']['csrf_token_generator']) ? new Reference($firewall['logout']['csrf_token_generator']) : null, - )) - ; + )); } // Authentication listeners @@ -364,6 +363,11 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a // Access listener $listeners[] = new Reference('security.access_listener'); + // Expired session listener + if (isset($firewall['session_concurrency'])) { + $listeners[] = new Reference($this->createExpiredSessionListener($container, $id, $firewall)); + } + // Determine default entry point if (isset($firewall['entry_point'])) { $defaultEntryPoint = $firewall['entry_point']; @@ -394,6 +398,10 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut $hasListeners = false; $defaultEntryPoint = null; + if (isset($firewall['session_concurrency'])) { + $this->createConcurrentSessionAuthenticationStrategy($container, $id, $firewall['session_concurrency']); + } + foreach ($this->listenerPositions as $position) { foreach ($this->factories[$position] as $factory) { $key = str_replace('-', '_', $factory->getKey()); @@ -415,16 +423,14 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut $listenerId = 'security.authentication.listener.anonymous.'.$id; $container ->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.anonymous')) - ->replaceArgument(1, $firewall['anonymous']['key']) - ; + ->replaceArgument(1, $firewall['anonymous']['key']); $listeners[] = new Reference($listenerId); $providerId = 'security.authentication.provider.anonymous.'.$id; $container ->setDefinition($providerId, new DefinitionDecorator('security.authentication.provider.anonymous')) - ->replaceArgument(0, $firewall['anonymous']['key']) - ; + ->replaceArgument(0, $firewall['anonymous']['key']); $authenticationProviders[] = $providerId; $hasListeners = true; @@ -446,8 +452,7 @@ private function createEncoders($encoders, ContainerBuilder $container) $container ->getDefinition('security.encoder_factory.generic') - ->setArguments(array($encoderMap)) - ; + ->setArguments(array($encoderMap)); } private function createEncoder($config, ContainerBuilder $container) @@ -542,8 +547,7 @@ private function createUserDaoProvider($name, $provider, ContainerBuilder $conta $container ->setDefinition($name, new DefinitionDecorator('security.user.provider.chain')) - ->addArgument($providers) - ; + ->addArgument($providers); return $name; } @@ -553,8 +557,7 @@ private function createUserDaoProvider($name, $provider, ContainerBuilder $conta $container ->setDefinition($name, new DefinitionDecorator('security.user.provider.entity')) ->addArgument($provider['entity']['class']) - ->addArgument($provider['entity']['property']) - ; + ->addArgument($provider['entity']['property']); return $name; } @@ -566,8 +569,7 @@ private function createUserDaoProvider($name, $provider, ContainerBuilder $conta $container ->setDefinition($userId, new DefinitionDecorator('security.user.provider.in_memory.user')) - ->setArguments(array($username, (string) $user['password'], $user['roles'])) - ; + ->setArguments(array($username, (string) $user['password'], $user['roles'])); $definition->addMethodCall('createUser', array(new Reference($userId))); } @@ -627,6 +629,22 @@ private function createExpression($container, $expression) return $this->expressions[$id] = new Reference($id); } + private function createExpiredSessionListener($container, $id, $config) + { + $expiredSessionListenerId = 'security.authentication.expiredsession_listener.'.$id; + $listener = $container->setDefinition($expiredSessionListenerId, new DefinitionDecorator('security.authentication.expiredsession_listener')); + + $listener->replaceArgument(3, $config['session_concurrency']['expiration_url']); + + if (isset($config['logout'])) { + $logoutSuccessHandlerId = $this->createLogoutSuccessHandler($container, $id, $config); + $listener->replaceArgument(4, new Reference($logoutSuccessHandlerId)); + $this->addLogoutHandlers($container, $expiredSessionListenerId, $id, $config); + } + + return $expiredSessionListenerId; + } + private function createRequestMatcher($container, $path = null, $host = null, $methods = array(), $ip = null, array $attributes = array()) { $serialized = serialize(array($path, $host, $methods, $ip, $attributes)); @@ -649,8 +667,7 @@ private function createRequestMatcher($container, $path = null, $host = null, $m $container ->register($id, '%security.matcher.class%') ->setPublic(false) - ->setArguments($arguments) - ; + ->setArguments($arguments); return $this->requestMatchers[$id] = new Reference($id); } @@ -697,4 +714,100 @@ private function getExpressionLanguage() return $this->expressionLanguage; } + + private function createConcurrentSessionAuthenticationStrategy($container, $id, $config) + { + $sessionStrategyId = 'security.authentication.session_strategy.'.$id; + + if (isset($config['max_sessions']) && $config['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['max_sessions']) + ->replaceArgument(2, $config['error_if_maximum_exceeded']); + + $fixationSessionStrategyId = 'security.authentication.session_strategy.fixation.'.$id; + $container->setAlias( + $fixationSessionStrategyId, + 'security.authentication.session_strategy' + ); + + $registerSessionStrategyId = 'security.authentication.session_strategy.register.'.$id; + $container->setDefinition( + $registerSessionStrategyId, + new DefinitionDecorator( + 'security.authentication.session_strategy.register' + ) + ); + + $container->setDefinition( + $sessionStrategyId, + new DefinitionDecorator( + 'security.authentication.session_strategy.composite' + ) + )->replaceArgument( + 0, + array( + new Reference($concurrentSessionControlStrategyId), + new Reference($fixationSessionStrategyId), + new Reference($registerSessionStrategyId), + ) + ); + } else { + $container->setAlias( + $sessionStrategyId, + 'security.authentication.session_strategy' + ); + } + + return $sessionStrategyId; + } + + private function createLogoutSuccessHandler($container, $id, $config) + { + // add logout success handler + if (isset($config['success_handler'])) { + $logoutSuccessHandlerId = $config['success_handler']; + } else { + $logoutSuccessHandlerId = 'security.logout.success_handler.'.$id; + if (!$container->hasDefinition($logoutSuccessHandlerId)) { + $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new DefinitionDecorator('security.logout.success_handler')); + $logoutSuccessHandler->replaceArgument(1, $config['target']); + } + } + + return $logoutSuccessHandlerId; + } + + private function addLogoutHandlers($container, $listenerId, $id, $config) + { + $listener = $container->findDefinition($listenerId); + + // add session registry logout handler + if (isset($config['session_concurrency'])) { + $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session_registry'))); + } + + // add session logout handler + if (true === $config['logout']['invalidate_session'] && false === $config['stateless']) { + $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); + } + + // add cookie logout handler + if (count($config['logout']['delete_cookies']) > 0) { + $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id; + $cookieHandler = $container->setDefinition($cookieHandlerId, new DefinitionDecorator('security.logout.handler.cookie_clearing')); + $cookieHandler->addArgument($config['logout']['delete_cookies']); + + $listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId))); + } + + // add custom handlers + foreach ($config['logout']['handlers'] as $handlerId) { + $listener->addMethodCall('addHandler', array(new Reference($handlerId))); + } + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 7d3ba1a6f322c..7b28ffa6cff09 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\ExpiredSessionListener + 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..8855be55ac393 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml @@ -0,0 +1,41 @@ + + + + + + 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\Component\Security\Http\Session\SessionInformation + Symfony\Component\Security\Http\Logout\SessionRegistryLogoutHandler + + + + + + + + + + + + + + + + + + + + %security.authentication.session_information.class% + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml new file mode 100644 index 0000000000000..bcee305f19d61 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml @@ -0,0 +1,20 @@ + + + + + + Symfony\Bridge\Doctrine\Security\SessionRegistry\SessionRegistryStorage + + + + + + + + %security.session_registry.dbal.session_information_table_name% + + + + 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..37c59d3225b6f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionConcurrencyTest.php @@ -0,0 +1,87 @@ + + * + * 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' => 'StandardFormLogin', 'root_config' => 'session_concurrency.yml')); + $client->insulate(); + $form = $client->request('GET', '/login')->selectButton('login')->form(); + $form['_username'] = 'johannes'; + $form['_password'] = 'test'; + $client->submit($form); + + $this->assertRedirect($client->getResponse(), '/profile'); + } + + public function testLoginFailsWhenConcurrentSessionsGreaterOrEqualThanMaximun() + { + $client1 = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'session_concurrency.yml')); + $client1->insulate(); + $form1 = $client1->request('GET', '/login')->selectButton('login')->form(); + $form1['_username'] = 'johannes'; + $form1['_password'] = 'test'; + $client1->submit($form1); + + $client2 = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'session_concurrency.yml')); + $client2->insulate(); + $form2 = $client2->request('GET', '/login')->selectButton('login')->form(); + $form2['_username'] = 'johannes'; + $form2['_password'] = 'test'; + $client2->submit($form2); + + $this->assertRedirect($client2->getResponse(), '/login'); + } + + public function testOldSessionExpiresConcurrentSessionsGreaterOrEqualThanMaximun() + { + $client1 = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'session_concurrency_expiration.yml')); + $client1->insulate(); + $form1 = $client1->request('GET', '/login')->selectButton('login')->form(); + $form1['_username'] = 'johannes'; + $form1['_password'] = 'test'; + $client1->submit($form1); + $this->assertRedirect($client1->getResponse(), '/profile'); + + $client2 = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'session_concurrency_expiration.yml')); + $client2->insulate(); + $form2 = $client2->request('GET', '/login')->selectButton('login')->form(); + $form2['_username'] = 'johannes'; + $form2['_password'] = 'test'; + $client2->submit($form2); + + $this->assertRedirect($client2->getResponse(), '/profile'); + + $client1->request('GET', '/profile'); + $this->assertRedirect($client1->getResponse(), '/expired'); + } + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('StandardFormLogin'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('StandardFormLogin'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency.yml new file mode 100644 index 0000000000000..edef156195f20 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency.yml @@ -0,0 +1,21 @@ +imports: + - { resource: ./config.yml } + +services: + my_session_registry_storage: + class: Symfony\Component\Security\Http\Session\MockFileSessionRegistryStorage + arguments: + - %kernel.cache_dir%/session_registry + +security: + firewalls: + default: + form_login: + check_path: /login_check + default_target_path: /profile + anonymous: ~ + session_concurrency: + max_sessions: 1 + + session_registry: + session_registry_storage: my_session_registry_storage diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency_expiration.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency_expiration.yml new file mode 100644 index 0000000000000..f47e7ea1dd14b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency_expiration.yml @@ -0,0 +1,14 @@ +imports: + - { resource: ./session_concurrency.yml } + +security: + firewalls: + default: + form_login: + check_path: /login_check + default_target_path: /profile + anonymous: ~ + session_concurrency: + max_sessions: 1 + error_if_maximum_exceeded: false + expiration_url: /expired diff --git a/src/Symfony/Component/Security/Core/Exception/MaxSessionsExceededException.php b/src/Symfony/Component/Security/Core/Exception/MaxSessionsExceededException.php new file mode 100644 index 0000000000000..6d3759a7a0c06 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Exception/MaxSessionsExceededException.php @@ -0,0 +1,23 @@ + + * + * 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. + * (By default, the ConcurrentSessionControlStrategy will expire the user's oldest existing session) + * + * @author Stefan Paschke + */ +class MaxSessionsExceededException extends AuthenticationException +{ +} diff --git a/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php new file mode 100644 index 0000000000000..2be5d619bfe09 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php @@ -0,0 +1,104 @@ + + * + * 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\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Log\LoggerInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; +use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; +use Symfony\Component\Security\Http\Session\SessionRegistry; +use Symfony\Component\Security\Http\HttpUtils; + +/** + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ +class ExpiredSessionListener implements ListenerInterface +{ + private $tokenStorage; + private $httpUtils; + private $sessionRegistry; + private $targetUrl; + private $successHandler; + private $logger; + private $handlers; + + public function __construct(TokenStorageInterface $tokenStorage, HttpUtils $httpUtils, SessionRegistry $sessionRegistry, $targetUrl = '/', LogoutSuccessHandlerInterface $successHandler = null, LoggerInterface $logger = null) + { + $this->tokenStorage = $tokenStorage; + $this->httpUtils = $httpUtils; + $this->sessionRegistry = $sessionRegistry; + $this->targetUrl = $targetUrl; + $this->successHandler = $successHandler; + $this->logger = $logger; + $this->handlers = array(); + } + + /** + * Adds a logout handler + * + * @param LogoutHandlerInterface $handler + */ + public function addHandler(LogoutHandlerInterface $handler) + { + $this->handlers[] = $handler; + } + + /** + * Handles the number of simultaneous sessions for a single user. + * + * @param GetResponseEvent $event A GetResponseEvent instance + * @throws \RuntimeException if the successHandler exists and do not return a response + */ + public function handle(GetResponseEvent $event) + { + $request = $event->getRequest(); + + $session = $request->getSession(); + + if (null === $session || null === $token = $this->tokenStorage->getToken()) { + return; + } + + if ($sessionInformation = $this->sessionRegistry->getSessionInformation($session->getId())) { + if ($sessionInformation->isExpired()) { + if (null !== $this->logger) { + $this->logger->info(sprintf("Logging out expired session for username '%s'", $token->getUsername())); + } + + if (null !== $this->successHandler) { + $response = $this->successHandler->onLogoutSuccess($request); + if (!$response instanceof Response) { + throw new \RuntimeException('Logout Success Handler did not return a Response.'); + } + } else { + $response = $this->httpUtils->createRedirectResponse($request, $this->targetUrl); + } + + foreach ($this->handlers as $handler) { + $handler->logout($request, $response, $token); + } + + $this->tokenStorage->setToken(null); + + $event->setResponse($response); + } else { + $this->sessionRegistry->refreshLastRequest($session->getId()); + } + } else { + // sessionInformation was lost, try to recover by recreating it + $this->sessionRegistry->registerNewSession($session->getId(), $token->getUsername()); + } + } +} diff --git a/src/Symfony/Component/Security/Http/Logout/SessionRegistryLogoutHandler.php b/src/Symfony/Component/Security/Http/Logout/SessionRegistryLogoutHandler.php new file mode 100644 index 0000000000000..760e67ec42f63 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Logout/SessionRegistryLogoutHandler.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\Component\Security\Http\Logout; + +use Symfony\Component\Security\Http\Session\SessionRegistry; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; + +/** + * Handler for removing session information from the session registry. + * + * @author Antonio J. García Lagar + */ +class SessionRegistryLogoutHandler +{ + private $registry; + + /** + * Constructor + * + * @param SessionRegistry $registry + */ + public function __construct(SessionRegistry $registry) + { + $this->registry = $registry; + } + + /** + * Remove current session information from the session registry + * + * @param Request $request + * @param Response $response + * @param TokenInterface $token + */ + public function logout(Request $request, Response $response, TokenInterface $token) + { + if (null !== $session = $request->getSession()) { + $this->registry->removeSessionInformation($session->getId()); + } + } +} 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..cd3592c7c2ee0 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php @@ -0,0 +1,121 @@ + + * + * 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->getAllSessions($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, $this->registry); + } + + /** + * 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 + * @param SessionRegistry $registry + */ + protected function allowedSessionsExceeded($orderedSessions, $allowableSessions, SessionRegistry $registry) + { + 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++) { + $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/MockFileSessionRegistryStorage.php b/src/Symfony/Component/Security/Http/Session/MockFileSessionRegistryStorage.php new file mode 100644 index 0000000000000..6bc0e8faa50b1 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/MockFileSessionRegistryStorage.php @@ -0,0 +1,94 @@ + + * + * 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; + +/** + * MockFileSessionRegistryStorage mocks the session registry for functional tests. + * + * @author Antonio J. García Lagar + */ +class MockFileSessionRegistryStorage 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 getSessionInformations($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) + { + if (isset($this->sessionInformations[$sessionId])) { + unset($this->sessionInformations[$sessionId]); + } + } + + private function getFilePath($sessionId) + { + return $this->savePath.'/'.$sessionId.'.mocksessinfo'; + } + + 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..8d426f03820b9 --- /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($session->getId(), $token->getUsername()); + } + } +} 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..ebb22d285060e --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -0,0 +1,121 @@ + + * + * 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 + */ +class SessionInformation +{ + private $sessionId; + private $username; + private $expired; + private $lastRequest; + + public function __construct($sessionId, $username, \DateTime $lastRequest, \DateTime $expired = null) + { + $this->setSessionId($sessionId); + $this->setUsername($username); + $this->setLastRequest($lastRequest); + + if (null !== $expired) { + $this->setExpired($expired); + } + } + + /** + * Sets the session informations expiration date to the current date and time. + * + */ + public function expireNow() + { + $this->setExpired(new \DateTime()); + } + + /** + * Obtain the last request date. + * + * @return DateTime the last request date and time. + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + /** + * 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->getExpired() && $this->getExpired()->getTimestamp() < microtime(true); + } + + /** + * Set the last request date to the current date and time. + * + */ + public function refreshLastRequest() + { + $this->lastRequest = new \DateTime(); + } + + private function getExpired() + { + return $this->expired; + } + + private function setExpired(\DateTime $expired) + { + $this->expired = $expired; + } + + private function setLastRequest(\DateTime $lastRequest) + { + $this->lastRequest = $lastRequest; + } + + private function setSessionId($sessionId) + { + $this->sessionId = $sessionId; + } + + private function setUsername($username) + { + $this->username = $username; + } +} 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..5ff6a53348467 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -0,0 +1,114 @@ + + * + * 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; + +/** + * 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; + } + + /** + * Returns all the sessions stored for the given user ordered from newest to oldest. + * + * @param string $username the given user. + * @param bool $includeExpiredSessions + * @return SessionInformation[] An array of SessionInformation objects. + */ + public function getAllSessions($username, $includeExpiredSessions = false) + { + return $this->sessionRegistryStorage->getSessionInformations($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); + } + + /** + * Sets a SessionInformation object. + * + * @param SessionInformation $sessionInformation + */ + private function setSessionInformation(SessionInformation $sessionInformation) + { + $this->sessionRegistryStorage->setSessionInformation($sessionInformation); + } + + /** + * Updates the given sessionId so its last request time is equal to the present date and time. + * + * @param string $sessionId the session identifier key. + */ + public function refreshLastRequest($sessionId) + { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + $sessionInformation->refreshLastRequest(); + $this->setSessionInformation($sessionInformation); + } + } + + /** + * Expires the given sessionId. + * + * @param string $sessionId the session identifier key. + */ + public function expireNow($sessionId) + { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + $sessionInformation->expireNow(); + $this->setSessionInformation($sessionInformation); + } + } + + /** + * Registers a new session for the given user. + * + * @param string $sessionId the session identifier key. + * @param string $username the given user. + * @param \DateTime $lastRequest + */ + public function registerNewSession($sessionId, $username, \DateTime $lastRequest = null) + { + $lastRequest = $lastRequest ?: new \DateTime(); + $sessionInformation = new SessionInformation($sessionId, $username, $lastRequest); + + $this->setSessionInformation($sessionInformation); + } + + /** + * Deletes the stored information of one session. + * + * @param string $sessionId the session identifier key. + */ + public function removeSessionInformation($sessionId) + { + $this->sessionRegistryStorage->removeSessionInformation($sessionId); + } +} 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..b9a04995f57cd --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.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\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); + + /** + * Obtains the maintained information for one user ordered from newest to + * oldest + * + * @param string $username The user identifier. + * @param bool $includeExpiredSessions + * @return SessionInformation[] An array of SessionInformation objects. + */ + public function getSessionInformations($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); +} diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php new file mode 100644 index 0000000000000..55bc7b28b9a38 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php @@ -0,0 +1,288 @@ + + * + * 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\ExpiredSessionListener; + +/** + * @author Antonio J. García Lagar + */ +class ExpiredSessionListenerTest extends \PHPUnit_Framework_TestCase +{ + public function testHandleWhenNoSession() + { + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('hasSession') + ->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 ExpiredSessionListener( + $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'), + $this->getHttpUtils(), + $this->getSessionRegistry() + ); + + $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('hasSession') + ->will($this->returnValue(true)); + $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 ExpiredSessionListener( + $securityContext, + $this->getHttpUtils(), + $this->getSessionRegistry() + ); + + $this->assertNull($listener->handle($event)); + } + + public function testHandleWhenSessionInformationIsExpired() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session + ->expects($this->any()) + ->method('getId') + ->will($this->returnValue('foo')); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('hasSession') + ->will($this->returnValue(true)); + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)); + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token + ->expects($this->any()) + ->method('getUsername') + ->will($this->returnValue('foobar')); + + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $securityContext + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)); + + $sessionInformation = $this->getSessionInformation(); + $sessionInformation + ->expects($this->once()) + ->method('isExpired') + ->will($this->returnValue(true)); + + $sessionRegistry = $this->getSessionRegistry(); + $sessionRegistry + ->expects($this->once()) + ->method('getSessionInformation') + ->with($this->equalTo('foo')) + ->will($this->returnValue($sessionInformation)); + + $response = $this->getMock('Symfony\Component\HttpFoundation\Response'); + + $httpUtils = $this->getHttpUtils(); + $httpUtils + ->expects($this->once()) + ->method('createRedirectResponse') + ->with($this->identicalTo($request), $this->equalTo('/')) + ->will($this->returnValue($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)); + + $listener = new ExpiredSessionListener( + $securityContext, + $httpUtils, + $sessionRegistry + ); + + $this->assertNull($listener->handle($event)); + } + + public function testHandleWhenSessionInformationIsNotExpired() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session + ->expects($this->any()) + ->method('getId') + ->will($this->returnValue('foo')); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('hasSession') + ->will($this->returnValue(true)); + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)); + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token + ->expects($this->any()) + ->method('getUsername') + ->will($this->returnValue('foobar')); + + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $securityContext + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)); + + $sessionInformation = $this->getSessionInformation(); + $sessionInformation + ->expects($this->once()) + ->method('isExpired') + ->will($this->returnValue(false)); + + $sessionRegistry = $this->getSessionRegistry(); + $sessionRegistry + ->expects($this->once()) + ->method('getSessionInformation') + ->with($this->equalTo('foo')) + ->will($this->returnValue($sessionInformation)); + $sessionRegistry + ->expects($this->once()) + ->method('refreshLastRequest') + ->with($this->equalTo('foo')); + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)); + + $listener = new ExpiredSessionListener( + $securityContext, + $this->getHttpUtils(), + $sessionRegistry + ); + + $this->assertNull($listener->handle($event)); + } + + public function testHandleWhenNoSessionInformationIsRegistered() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session + ->expects($this->any()) + ->method('getId') + ->will($this->returnValue('foo')); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('hasSession') + ->will($this->returnValue(true)); + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)); + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token + ->expects($this->once()) + ->method('getUsername') + ->will($this->returnValue('foobar')); + + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); + $securityContext + ->expects($this->any()) + ->method('getToken') + ->will($this->returnValue($token)); + + $sessionRegistry = $this->getSessionRegistry(); + $sessionRegistry + ->expects($this->once()) + ->method('getSessionInformation') + ->with($this->equalTo('foo')) + ->will($this->returnValue(null)); + $sessionRegistry + ->expects($this->once()) + ->method('registerNewSession') + ->with($this->equalTo('foo'), $this->equalTo('foobar')); + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)); + + $listener = new ExpiredSessionListener( + $securityContext, + $this->getHttpUtils(), + $sessionRegistry + ); + + $this->assertNull($listener->handle($event)); + } + + private function getHttpUtils() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\HttpUtils') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getSessionRegistry() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionRegistry') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getSessionInformation() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionInformation') + ->disableOriginalConstructor() + ->getMock(); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Logout/SessionRegistryLogoutHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Logout/SessionRegistryLogoutHandlerTest.php new file mode 100644 index 0000000000000..32f9b6cc83563 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Logout/SessionRegistryLogoutHandlerTest.php @@ -0,0 +1,48 @@ + + * + * 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\Logout; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Http\Logout\SessionRegistryLogoutHandler; + +class SessionRegistryLogoutHandlerTest extends \PHPUnit_Framework_TestCase +{ + public function testLogout() + { + $registry = $this->getMock('Symfony\Component\Security\Http\Session\SessionRegistry', array(), array(), '', false); + $handler = new SessionRegistryLogoutHandler($registry); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $response = new Response(); + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\Session', array(), array(), '', false); + + $request + ->expects($this->once()) + ->method('getSession') + ->will($this->returnValue($session)) + ; + + $session + ->expects($this->once()) + ->method('getId') + ->will($this->returnValue('foobar')) + ; + + $registry + ->expects($this->once()) + ->method('removeSessionInformation') + ->with($this->equalTo('foobar')) + ; + + $handler->logout($request, $response, $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')); + } +} 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..d9126506778b4 --- /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('getAllSessions') + ->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('getAllSessions') + ->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('getAllSessions') + ->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('getAllSessions') + ->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..bbdce8f50ea5f --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/RegisterSessionAuthenticationStrategyTest.php @@ -0,0 +1,59 @@ + + * + * 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'); + $session->expects($this->any())->method('getId')->will($this->returnValue('bar')); + $request = $this->getRequest($session); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once())->method('registerNewSession')->with($this->equalTo('bar'), $this->equalTo('foo')); + + $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..c876c7adb58b3 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.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\SessionInformation; + +/** + * @author Antonio J. García Lagar + */ +class SessionInformationTest extends \PHPUnit_Framework_TestCase +{ + public function testExpiration() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertFalse($sessionInfo->isExpired()); + $sessionInfo->expireNow(); + + $this->assertTrue($sessionInfo->isExpired()); + } + + public function testRefreshLastRequest() + { + $sessionInfo = $this->getSessionInformation(); + $lastRequest = $sessionInfo->getLastRequest(); + $this->assertInstanceOf('DateTime', $lastRequest); + $sessionInfo->refreshLastRequest(); + $this->assertGreaterThanOrEqual($lastRequest, $sessionInfo->getLastRequest()); + } + + public function testGetSessionId() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertEquals('foo', $sessionInfo->getSessionId()); + } + + public function testGetUsername() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertEquals('bar', $sessionInfo->getUsername()); + } + + /** + * @return \Symfony\Component\Security\Http\Session\SessionInformation + */ + private function getSessionInformation() + { + return new SessionInformation('foo', 'bar', new \DateTime()); + } +} 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..aadcecda896fd --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php @@ -0,0 +1,91 @@ + + * + * 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 testGetAllSessions() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('getSessionInformations')->with('foo', true); + $registry = $this->getSessionRegistry($storage); + $registry->getAllSessions('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 testRefreshLastRequest() + { + $sessionInformation = $this->getSessionInformation(); + $sessionInformation->expects($this->once())->method('refreshLastRequest'); + $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->refreshLastRequest('foobar'); + } + + public function testExpireNow() + { + $sessionInformation = $this->getSessionInformation(); + $sessionInformation->expects($this->once())->method('expireNow'); + $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 testRegisterNewSession() + { + $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', 'bar', new \DateTime()); + } + + public function testRemoveSessionInformation() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('removeSessionInformation')->with('foobar'); + $registry = $this->getSessionRegistry($storage); + $registry->removeSessionInformation('foobar'); + } + + 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