php - Prevent simultaneous user sessions in Symfony2 -
the goal
we providing client solution multiple-choice practice system students pay monthly membership in order test knowledge , prepare medical-related examinations. major issue providing solution in symfony2 students can buy 1 subscription, share credentials classmates , colleagues, , split cost of subscription on multiple concurrent logins.
in order minimize problem, we wish prevent more 1 simultaneous session being maintained in our symfony2 project.
research
massive amounts of google-fu led me sparse google group thread op briefly told use pdosessionhandler store sessions in database.
here's another question else worked around same thing, no explanation on how it.
progress far
i've implemented handler project , have security.interactive_login
listener stores resulting session id user in database. progress here
public function __construct(securitycontext $securitycontext, doctrine $doctrine, container $container) { $this->securitycontext = $securitycontext; $this->doc = $doctrine; $this->em = $doctrine->getmanager(); $this->container = $container; } /** * magic. * * @param interactiveloginevent $event */ public function onsecurityinteractivelogin(interactiveloginevent $event) { if ($this->securitycontext->isgranted('is_authenticated_fully')) { // user has logged in } if ($this->securitycontext->isgranted('is_authenticated_remembered')) { // user has logged in using remember_me cookie } // first user object can work $user = $event->getauthenticationtoken()->getuser(); // check see if they're subscriber if ($this->securitycontext->isgranted('role_subscribed')) { // check expiry date versus if ($user->getexpiry() < new \datetime('now')) { // if expiry date past now, need remove role $user->removerole('role_subscribed'); $this->em->persist($user); $this->em->flush(); // we've removed role, have make new token , load session $token = new \symfony\component\security\core\authentication\token\usernamepasswordtoken( $user, null, 'main', $user->getroles() ); $this->securitycontext->settoken($token); } } // current session , associate user $sessionid = $this->container->get('session')->getid(); $user->setsessionid($sessionid); $this->em->persist($user); $s = $this->doc->getrepository('imcqbundle:session')->find($sessionid); if ($s) { // $s = false, part doesn't execute $s->setuserid($user->getid()); $this->em->persist($s); } $this->em->flush(); // have log out other users sharing same username outside of current session token // ... code detach other `imcqbundle:session` entities userid = logged in user }
the problem
the session isn't stored database pdosessionhandler until after security.interactive_login
listener finished, therefore user id never ends getting stored session table. how can make work? can have user id store in session table?
alternatively, there better way of going this? turning out extremely frustrating symfony don't think ever designed have exclusive single user sessions each user.
i've solved own problem, leave question open dialogue (if any) before i'm able accept own answer.
i created kernel.request
listener check user's current session id latest session id associated user upon each login.
here's code:
<?php namespace acme\bundle\listener; use symfony\component\httpkernel\event\getresponseevent; use symfony\component\httpkernel\httpkernel; use symfony\component\httpfoundation\redirectresponse; use symfony\component\security\core\securitycontext; use symfony\component\dependencyinjection\container; use symfony\component\routing\router; /** * custom session listener. */ class sessionlistener { private $securitycontext; private $container; private $router; public function __construct(securitycontext $securitycontext, container $container, router $router) { $this->securitycontext = $securitycontext; $this->container = $container; $this->router = $router; } public function onkernelrequest(getresponseevent $event) { if (!$event->ismasterrequest()) { return; } if ($token = $this->securitycontext->gettoken()) { // check token - or else isgranted() fail on assets if ($this->securitycontext->isgranted('is_authenticated_fully') || $this->securitycontext->isgranted('is_authenticated_remembered')) { // check if there authenticated user // compare stored session id current session id user if ($token->getuser() && $token->getuser()->getsessionid() !== $this->container->get('session')->getid()) { // tell user else has logged on different device $this->container->get('session')->getflashbag()->set( 'error', 'another device has logged on username , password. log in again, please enter credentials below. please note other device logged out.' ); // kick user out, because new user has logged in $this->securitycontext->settoken(null); // redirect user login page, or else they'll still trying access dashboard (which no longer have access to) $response = new redirectresponse($this->router->generate('sonata_user_security_login')); $event->setresponse($response); return $event; } } } } }
and services.yml
entry:
services: acme.session.listener: class: acme\bundle\listener\sessionlistener arguments: ['@security.context', '@service_container', '@router'] tags: - { name: kernel.event_listener, event: kernel.request, method: onkernelrequest }
it's interesting note spent embarrassing amount of time wondering why listener making application break when realized had named imcq.session.listener
session_listener
. turns out symfony (or other bundle) using name, , therefore overriding behaviour.
be careful! break implicit login functionality on fosuserbundle 1.3.x. should either upgrade 2.0.x-dev , use implicit login event or replace loginlistener
own fos_user.security.login_manager
service. (i did latter because i'm using sonatauserbundle)
by request, here's full solution fosuserbundle 1.3.x:
for implicit logins, add services.yml
:
fos_user.security.login_manager: class: acme\bundle\security\loginmanager arguments: ['@security.context', '@security.user_checker', '@security.authentication.session_strategy', '@service_container', '@doctrine']
and make file under acme\bundle\security
named loginmanager.php
code:
<?php namespace acme\bundle\security; use fos\userbundle\security\loginmanagerinterface; use fos\userbundle\model\userinterface; use symfony\component\dependencyinjection\containerinterface; use symfony\component\httpfoundation\response; use symfony\component\security\core\authentication\token\usernamepasswordtoken; use symfony\component\security\core\user\usercheckerinterface; use symfony\component\security\core\securitycontextinterface; use symfony\component\security\http\rememberme\remembermeservicesinterface; use symfony\component\security\http\session\sessionauthenticationstrategyinterface; use doctrine\bundle\doctrinebundle\registry doctrine; // symfony 2.1.0+ class loginmanager implements loginmanagerinterface { private $securitycontext; private $userchecker; private $sessionstrategy; private $container; private $em; public function __construct(securitycontextinterface $context, usercheckerinterface $userchecker, sessionauthenticationstrategyinterface $sessionstrategy, containerinterface $container, doctrine $doctrine) { $this->securitycontext = $context; $this->userchecker = $userchecker; $this->sessionstrategy = $sessionstrategy; $this->container = $container; $this->em = $doctrine->getmanager(); } final public function loginuser($firewallname, userinterface $user, response $response = null) { $this->userchecker->checkpostauth($user); $token = $this->createtoken($firewallname, $user); if ($this->container->isscopeactive('request')) { $this->sessionstrategy->onauthentication($this->container->get('request'), $token); if (null !== $response) { $remembermeservices = null; if ($this->container->has('security.authentication.rememberme.services.persistent.'.$firewallname)) { $remembermeservices = $this->container->get('security.authentication.rememberme.services.persistent.'.$firewallname); } elseif ($this->container->has('security.authentication.rememberme.services.simplehash.'.$firewallname)) { $remembermeservices = $this->container->get('security.authentication.rememberme.services.simplehash.'.$firewallname); } if ($remembermeservices instanceof remembermeservicesinterface) { $remembermeservices->loginsuccess($this->container->get('request'), $response, $token); } } } $this->securitycontext->settoken($token); // here's custom part, need current session , associate user $sessionid = $this->container->get('session')->getid(); $user->setsessionid($sessionid); $this->em->persist($user); $this->em->flush(); } protected function createtoken($firewall, userinterface $user) { return new usernamepasswordtoken($user, null, $firewall, $user->getroles()); } }
for more important interactive logins, should add services.yml
:
login_listener: class: acme\bundle\listener\loginlistener arguments: ['@security.context', '@doctrine', '@service_container'] tags: - { name: kernel.event_listener, event: security.interactive_login, method: onsecurityinteractivelogin }
and subsequent loginlistener.php
interactive login events:
<?php namespace acme\bundle\listener; use symfony\component\security\http\event\interactiveloginevent; use symfony\component\security\core\securitycontext; use symfony\component\dependencyinjection\container; use doctrine\bundle\doctrinebundle\registry doctrine; // symfony 2.1.0+ /** * custom login listener. */ class loginlistener { /** @var \symfony\component\security\core\securitycontext */ private $securitycontext; /** @var \doctrine\orm\entitymanager */ private $em; private $container; private $doc; /** * constructor * * @param securitycontext $securitycontext * @param doctrine $doctrine */ public function __construct(securitycontext $securitycontext, doctrine $doctrine, container $container) { $this->securitycontext = $securitycontext; $this->doc = $doctrine; $this->em = $doctrine->getmanager(); $this->container = $container; } /** * magic. * * @param interactiveloginevent $event */ public function onsecurityinteractivelogin(interactiveloginevent $event) { if ($this->securitycontext->isgranted('is_authenticated_fully')) { // user has logged in } if ($this->securitycontext->isgranted('is_authenticated_remembered')) { // user has logged in using remember_me cookie } // first user object can work $user = $event->getauthenticationtoken()->getuser(); // current session , associate user //$user->setsessionid($this->securitycontext->gettoken()->getcredentials()); $sessionid = $this->container->get('session')->getid(); $user->setsessionid($sessionid); $this->em->persist($user); $this->em->flush(); // ... } }
Comments
Post a Comment