w3hello.com logo
Home PHP C# C++ Android Java Javascript Python IOS SQL HTML videos Categories
Prevent simultaneous user sessions in Symfony2

I've solved my own problem, but will leave the question open for dialogue (if any) before I'm able to accept my own answer.

I created a kernel.request listener that would check the user's current session ID with the latest session ID associated with the user upon each login.

Here's the code:

<?php

namespace AcmeBundleListener;

use SymfonyComponentHttpKernelEventGetResponseEvent;
use SymfonyComponentHttpKernelHttpKernel;
use SymfonyComponentHttpFoundationRedirectResponse;
use SymfonyComponentSecurityCoreSecurityContext;
use SymfonyComponentDependencyInjectionContainer;
use SymfonyComponentRoutingRouter;

/**
 * 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
for a token - or else isGranted() will fail on the assets
            if
($this->securityContext->isGranted('IS_AUTHENTICATED_FULLY') ||
$this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
// Check if there is an authenticated user
                // Compare the stored session ID to the current session ID
with the user 
                if ($token->getUser() &&
$token->getUser()->getSessionId() !==
$this->container->get('session')->getId()) {
                    // Tell the user that someone else has logged on with a
different device
                   
$this->container->get('session')->getFlashBag()->set(
                        'error',
                        'Another device has logged on with your username
and password. To log back in again, please enter your credentials below.
Please note that the other device will be logged out.'
                    );
                    // Kick this user out, because a new user has logged in
                    $this->securityContext->setToken(null);
                    // Redirect the user back to the login page, or else
they'll still be trying to access the dashboard (which they no longer have
access to)
                    $response = new
RedirectResponse($this->router->generate('sonata_user_security_login'));
                    $event->setResponse($response);
                    return $event;
                }
            }
        }
    }
}

and the services.yml entry:

services:
    acme.session.listener:
        class: AcmeBundleListenerSessionListener
        arguments: ['@security.context', '@service_container', '@router']
        tags:
            - { name: kernel.event_listener, event: kernel.request, method:
onKernelRequest }

It's interesting to note that I spent an embarrassing amount of time wondering why my listener was making my application break when I realized that I had previously named imcq.session.listener as session_listener. Turns out Symfony (or some other bundle) was already using that name, and therefore I was overriding its behaviour.

Be careful! This will break implicit login functionality on FOSUserBundle 1.3.x. You should either upgrade to 2.0.x-dev and use its implicit login event or replace the LoginListener with your own fos_user.security.login_manager service. (I did the latter because I'm using SonataUserBundle)

By request, here's the full solution for FOSUserBundle 1.3.x:

For implicit logins, add this to your services.yml:

fos_user.security.login_manager:
    class: AcmeBundleSecurityLoginManager
    arguments: ['@security.context', '@security.user_checker',
'@security.authentication.session_strategy', '@service_container',
'@doctrine']

And make a file under AcmeBundleSecurity named LoginManager.php with the code:

<?php

namespace AcmeBundleSecurity;

use FOSUserBundleSecurityLoginManagerInterface;

use FOSUserBundleModelUserInterface;
use SymfonyComponentDependencyInjectionContainerInterface;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentSecurityCoreAuthenticationTokenUsernamePasswordToken;
use SymfonyComponentSecurityCoreUserUserCheckerInterface;
use SymfonyComponentSecurityCoreSecurityContextInterface;
use SymfonyComponentSecurityHttpRememberMeRememberMeServicesInterface;
use
SymfonyComponentSecurityHttpSessionSessionAuthenticationStrategyInterface;

use DoctrineBundleDoctrineBundleRegistry as Doctrine; // for 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 the custom part, we need to get the current session and
associate the user with it
        $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 the more important Interactive Logins, you should also add this to your services.yml:

login_listener:
    class: AcmeBundleListenerLoginListener
    arguments: ['@security.context', '@doctrine', '@service_container']
    tags:
        - { name: kernel.event_listener, event: security.interactive_login,
method: onSecurityInteractiveLogin }

and the subsequent LoginListener.php for Interactive Login events:

<?php

namespace AcmeBundleListener;

use SymfonyComponentSecurityHttpEventInteractiveLoginEvent;
use SymfonyComponentSecurityCoreSecurityContext;
use SymfonyComponentDependencyInjectionContainer;
use DoctrineBundleDoctrineBundleRegistry as Doctrine; // for Symfony 2.1.0+

/**
 * Custom login listener.
 */
class LoginListener
{
    /** @var SymfonyComponentSecurityCoreSecurityContext */
    private $securityContext;

    /** @var DoctrineORMEntityManager */
    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;
    }

    /**
     * Do the magic.
     * 
     * @param InteractiveLoginEvent $event
     */
    public function onSecurityInteractiveLogin(InteractiveLoginEvent
$event)
    {
        if
($this->securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
            // user has just logged in
        }

        if
($this->securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
            // user has logged in using remember_me cookie
        }

        // First get that user object so we can work with it
        $user = $event->getAuthenticationToken()->getUser();

        // Get the current session and associate the user with it
       
//$user->setSessionId($this->securityContext->getToken()->getCredentials());
        $sessionId = $this->container->get('session')->getId();
        $user->setSessionId($sessionId);
        $this->em->persist($user);
        $this->em->flush();

        // ...
    }
}




© Copyright 2018 w3hello.com Publishing Limited. All rights reserved.