22

After a lot of search in the web and find nothing, I wonder if there is an easy way to automatic logout the user logged through the Symfony Security after an inactive period. I want that the user be logged out after 30 minutes of inactivity, for example.

I use a custom User Provider like this.

But after the user login into the system, the session never expires. Even if he close the browser and open it again after some days the session is still valid.

There is anyway to logout this user by an automatic way or even a manual way? How can I do that?

Nic Wortel
  • 11,155
  • 6
  • 60
  • 79
Vlamir Carbonari
  • 336
  • 1
  • 2
  • 9

10 Answers10

59

You have to implement it with a kernel listener, this is the way I solve it:

Listener src/Comakai/MyBundle/Handler/SessionIdleHandler.php

namespace Comakai\MyBundle\Handler;

use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class SessionIdleHandler
{

    protected $session;
    protected $securityToken;
    protected $router;
    protected $maxIdleTime;

    public function __construct(SessionInterface $session, TokenStorageInterface $securityToken, RouterInterface $router, $maxIdleTime = 0)
    {
        $this->session = $session;
        $this->securityToken = $securityToken;
        $this->router = $router;
        $this->maxIdleTime = $maxIdleTime;
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {

            return;
        }

        if ($this->maxIdleTime > 0) {

            $this->session->start();
            $lapse = time() - $this->session->getMetadataBag()->getLastUsed();

            if ($lapse > $this->maxIdleTime) {

                $this->securityToken->setToken(null);
                $this->session->getFlashBag()->set('info', 'You have been logged out due to inactivity.');

                // Change the route if you are not using FOSUserBundle.
                $event->setResponse(new RedirectResponse($this->router->generate('fos_user_security_login')));
            }
        }
    }

}

Config src/Comakai/MyBundle/Resources/config/services.yml (Comakai/MyBundle/DependencyInjection/MyBundleExtension.php)

services:
    my.handler.session_idle:
        class: Comakai\MyBundle\Handler\SessionIdleHandler
        arguments: ["@session", "@security.context", "@router", %session_max_idle_time%]
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }

Now you can set the session_max_idle_time in parameters.yml to 30 * 60 = 1800 seconds (or just hardcode the value wherever you want):

Parameters app/config/parameters.yml

parameters:
    ...
    session_max_idle_time: 1800
Ryaner
  • 751
  • 6
  • 16
coma
  • 16,429
  • 4
  • 51
  • 76
  • Thanks coma for your post..Kinldy tell me how I can pass session_max_idle_time value from parameters.yml ? – Ali Hassan Mar 26 '14 at 06:31
  • How would you write an integration test for that? http://stackoverflow.com/questions/36178901/test-sessionlistener-within-symfony – user3746259 Mar 23 '16 at 18:00
  • @user3746259, I think that a unit test would be enough since the rest is already tested by the Symfony guys, but anyway, I guess that you can try configuring it with a really short time, making a login call and making another call after that one and assert that the last one gets redirected. – coma Mar 23 '16 at 18:07
  • the "biggest" test you could do would be an integration test, nothing more because the parameter for timeout can only be changed when instantiating an object directly, like in an integration test. A functional test would not make it possible to change the parameter dynamically. – user3746259 Mar 24 '16 at 08:29
  • 1
    Looks like someone made a bundle to do pretty much exactly this: https://github.com/LionwareSolutions/symfony-session-timeout – Brian May 19 '16 at 13:53
  • 3
    @Brian, the funny thing is that I keep getting upvotes from this while I haven't written a single line of PHP for about two years now. – coma May 19 '16 at 13:56
  • @coma OK, but this solution only logs out the user on the background. I want the app to automatically redirect to the login page after the session expires. Without having to reload the page or navigate to another. How do I automatically redirect without an actual request? – Nat Naydenova Mar 02 '17 at 09:34
  • @NatNaydenova, you could run a websockets server and send any action on demand or just share the max iddle time via cookies and let javascript that on the front. – coma Mar 02 '17 at 10:41
17

The following setting will log out users that are inactive for more than 30minutes. If a request is made every 29minutes, they will never be logged out. Please note that this is not easy to test in an local environment as the garbage collector is only called from your request thus the gc_maxlifetime is never reached!

#app/config/config.yml
session:
    cookie_lifetime: 86400
    gc_maxlifetime: 1800

You can test this if you open more browsers/sessions and use the following config:

#app/config/config.yml
session:
    cookie_lifetime: 86400
    gc_maxlifetime: 1800
    gc_probability: 1
    gc_divisor: 1

Hope that helps!

Please note, adding:

 session:
    gc_probability: 1
    gc_divisor: 1

Is only meant for testing the garbage collector on a local environment where there are no other requests that cause the garbage collector to remove your session. Making the garbage collector run on every request is not meant (or necessary) on a productive environment!

HKandulla
  • 1,101
  • 12
  • 17
  • 1
    I am pretty sure this is the correct solution, you shouldn't have to define a service to do this, it's also in the docs: http://symfony.com/doc/current/components/http_foundation/session_configuration.html#session-idle-time-keep-alive – Steve May 09 '14 at 14:52
  • 2
    Note: this will only log out the user on the second request after the expiry time, since the gc seems to be called after a request. If you have a high traffic site this will be fine, but for low it's not really great. – peterjwest Aug 13 '14 at 11:37
  • gc is randomly called at each request, the probability it's called is session.gc_probability / session.gc_divisor then when you put gc_probability = 1 and gc_divisor=1 probability is 1 and this solution will work perfectly but it's really not an efficient solution because gc is called at each request... – Nico Dec 24 '14 at 10:49
  • 1
    Ouch, this solution is really not right: making the garbage colelctor run on each request will instantly kill an high trafic server (and even slow down drastically a low trafic server with some complex application), but most of all this solution is **not secure**: the cookie time can be user-manipulated. Consider using @coma's solution instead. – Ninj Mar 06 '15 at 11:21
  • 1
    As mentioned in the answer using gc_probability: 1 is just a way to test it in a **local/tesing environment** and **NOT** meant for a productive environment! If you are not worried about users manipulating the cookie time (as I am), I still think this is best/easiest solution. – HKandulla Mar 10 '15 at 14:27
  • @Ninj It is not a security issue because GC is server-side and not influenced by the cookie lifetime. GC clears all session data that's older than gc_maxliftime. – Philipp Rieber Feb 02 '16 at 15:08
8

In case anybody wants to implement this in Symfony 4, I've updated the answer @coma gave since security.context is depreciated, parameters.yml is now just part of app/config/service.yaml and you can just inject the other variables for the contructor. It's basically the same answer though, just tweaked to work for Symfony 4:

Listener src/Security/SessionIdleHandler.php (or anywhere, it's mapped in the event listener below)

<?php

namespace App\Security;

use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class SessionIdleHandler
{

    protected $session;
    protected $securityToken;
    protected $router;
    protected $maxIdleTime;

    public function __construct($maxIdleTime, SessionInterface $session, TokenStorageInterface $securityToken, RouterInterface $router)
    {
        $this->session = $session;
        $this->securityToken = $securityToken;
        $this->router = $router;
        $this->maxIdleTime = $maxIdleTime;
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {

            return;
        }

        if ($this->maxIdleTime > 0) {

            $this->session->start();
            $lapse = time() - $this->session->getMetadataBag()->getLastUsed();

            if ($lapse > $this->maxIdleTime) {

                $this->securityToken->setToken(null);
                $this->session->getFlashBag()->set('info', 'You have been logged out due to inactivity.');

                // logout is defined in security.yaml.  See 'Logging Out' section here:
                // https://symfony.com/doc/4.1/security.html
                $event->setResponse(new RedirectResponse($this->router->generate(logout)));
            }
        }
    }
}

Parameters app/config/service.yaml

parameters:
    ...
    session_max_idle_time: 600 // set to whatever value you want in seconds

Kernel Event Listener app/config/service.yaml

services:
    ...
    App.Handler.SessionIdle:
        class: App\Security\SessionIdleHandler
        arguments: ['%session_max_idle_time%']
        tags: [{ name: kernel.event_listener, event: kernel.request }]
Element Zero
  • 1,651
  • 3
  • 13
  • 31
2

Works perfect with FOSUserbundle, thank you.

I added this to the inner condition to prevent the anonymous user to get logged out.

...

$isFullyAuthenticated = $this->securityContext->isGranted('IS_AUTHENTICATED_FULLY');

if ($lapse > $this->maxIdleTime && $isFullyAuthenticated == true) {

 ... do logout / redirect etc.

}
Phil
  • 200
  • 10
2

Here is my example with Symfony 4.

Session was used instead of SessionInterface because this interface does not contain access to the getFlashBag() method.

A redirection is performed on app_login and not on app_logout, otherwise the flashBag of the current session will be lost.

$this->tokenStorage->setToken(); could be replaced by $this->tokenStorage->reset(); via the concrete class but the interface does not allow it.

You could use this:

<?php

declare(strict_types=1);

namespace App\EventListener;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;

class SessionIdleListener
{
    /**
     * @var int
     */
    private $maxIdleTime;

    /**
     * @var Session
     */
    private $session;

    /**
     * @var TokenStorageInterface
     */
    private $tokenStorage;

    /**
     * @var RouterInterface
     */
    private $router;

    /**
     * @var AuthorizationCheckerInterface
     */
    private $checker;

    public function __construct(
        string $maxIdleTime,
        Session $session,
        TokenStorageInterface $tokenStorage,
        RouterInterface $router,
        AuthorizationCheckerInterface $checker
    ) {
        $this->maxIdleTime = (int) $maxIdleTime;
        $this->session = $session;
        $this->tokenStorage = $tokenStorage;
        $this->router = $router;
        $this->checker = $checker;
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        if (!$event->isMasterRequest()
            || $this->maxIdleTime <= 0
            || $this->isAuthenticatedAnonymously()) {
            return;
        }

        $session = $this->session;
        $session->start();

        if ((time() - $session->getMetadataBag()->getLastUsed()) <= $this->maxIdleTime) {
            return;
        }

        $this->tokenStorage->setToken();
        $session->getFlashBag()->set('info', 'You have been logged out due to inactivity.');

        $event->setResponse(new RedirectResponse($this->router->generate('app_login')));
    }

    private function isAuthenticatedAnonymously(): bool
    {
        return !$this->tokenStorage->getToken()
            || !$this->checker->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_FULLY);
    }
}
App\EventListener\SessionIdleListener:
    bind:
        $maxIdleTime: '%env(APP_SESSION_MAX_IDLE_TIME)%'
        $session: '@session'
    tags:
        - { name: kernel.event_listener, event: kernel.request }
Roukmoute
  • 681
  • 1
  • 11
  • 26
1

In Symfony 2.4, the following worked just fine for me for a 1 hour time out :

framework:
    #esi:             ~
    translator:      { fallback: %locale% }
    secret:          %secret%
    router:
        resource: "%kernel.root_dir%/config/routing.yml"
        strict_requirements: ~
        http_port: 80
        https_port: 443
    form:            ~
    csrf_protection: ~
    validation:      { enable_annotations: true }
    templating:
        engines: ['twig']
        #assets_version: SomeVersionScheme
    default_locale:  "%locale%"
    trusted_proxies: ~
    session:         
        cookie_lifetime:       3600
    fragments:       ~
    trusted_hosts:   ~
kratos
  • 2,465
  • 2
  • 27
  • 45
  • 1
    Cookie lifetime works fine for longer time periods, but for short time periods the cookie expiry date is never updated so you may be logged out while still using the site. – peterjwest Aug 13 '14 at 10:36
  • 1
    Cookie lifetime counts time from cookie saving (i.e session start). What was asked here was idle timeout, i.e timeout from last site usage. – JohnSmith Jun 21 '17 at 12:49
1

Modifications for Symfony 6.1, based on @element-zero's answer:

src/Security/SessionIdleHandler.php

<?php

namespace App\Security;

use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class SessionIdleHandler
{
    
    private TokenStorageInterface $securityToken;

    private RouterInterface $router;

    private int $maxIdleTime;


    public function __construct(int $maxIdleTime, TokenStorageInterface $securityToken, RouterInterface $router)
    {
        $this->securityToken = $securityToken;
        $this->router = $router;
        $this->maxIdleTime = $maxIdleTime;
    }

    public function onKernelRequest(RequestEvent $event)
    {
        if (HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
            return;
        }

        if ($this->maxIdleTime > 0) {
            $session = $event->getRequest()->getSession();

            $session->start();
            $lapse = time() - $session->getMetadataBag()->getLastUsed();

            if ($lapse > $this->maxIdleTime) {

                $this->securityToken->setToken(null);
                $event->setResponse(new RedirectResponse($this->router->generate('app_homepage'))); // or whatever route you need
            }
        }
    }

}

config/services.yaml

parameters:
    ...
    session_max_idle_time: 1800 # in seconds

services:
    ...
    my.handler.session_idle:
    class: App\Security\SessionIdleHandler
    arguments: ["%session_max_idle_time%"]
    tags:
        - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
chris_cm
  • 61
  • 10
  • I have two questions on this: 1. Why didn't you define the constructor's parameter for session_max_idle_time as an int? (and if so: how can we pass such an int from the parameter setting? "int:%session_max_idle_time%" doesn't work as it would with %env(int:session_max_idle_time)% 2. You take the session object from the RequestStack. What is the difference if this vs. taking it from the request, i.e. $session = $event->getRequest()->getSession()? – Luc Hamers Jul 25 '23 at 09:13
  • 1
    1. I edited the reply and added the data type into the definition and the parameter. 2. I don't know, maybe it doesn't make a difference after all how you get the session. – chris_cm Jul 25 '23 at 09:53
  • 1
    I also edited the way how the session is retrieved. It is true that you don't need the request stack here. Actually it was there because I anyway used it somewhere else in that class. – chris_cm Jul 25 '23 at 10:04
  • 1
    Thank you for the clarification. It is a bit strange: yesterday I received an error message passing the parameter like you do the the constructor, something like "you are passing a string, but I want an int for the constructor's parameter $maxIdleTime". That is why I had to pass it as an environment variable: arguments: $maxIdleTime: '%env(int:session_max_idle_time)%', so converting it to an int before it is passed to the constructor. Today this value is accepted as you wrote it. I don't know what I did differently yesterday. – Luc Hamers Jul 26 '23 at 09:27
0

What about:

#app/config/config.yml
framework:
    session:
        cookie_lifetime:       1800
Mario Radomanana
  • 1,698
  • 1
  • 21
  • 31
Paul Andrieux
  • 1,836
  • 11
  • 24
-1

cookie lifetime is not appropriate because that can be manipulated by the client, so we must do the expiry on the server side. The easiest way is to implement this via garbage collection which runs reasonably frequently. The cookie_lifetime would be set to a relatively high value, and the garbage collection gc_maxlifetime would be set to destroy sessions at whatever the desired idle period is.

framework:
    #esi:             ~
    #translator:      { fallback: "%locale%" }
    secret:          "%secret%"
    router:
        resource: "%kernel.root_dir%/config/routing.yml"
        strict_requirements: ~
    form:            ~
    csrf_protection: ~
    validation:      { enable_annotations: true }
    templating:
        engines: ['twig']
        #assets_version: SomeVersionScheme
    default_locale:  "%locale%"
    trusted_hosts:   ~
    trusted_proxies: ~
    session:
        # handler_id set to null will use default session handler from php.ini
        #handler_id:  ~
        cookie_lifetime: 9999
        gc_maxlifetime: 900
        gc_probability: 1
        gc_divisor: 2
    fragments:       ~
    http_method_override: true
-1

A simple redirection to logout after moment using twig in your layout

First create your twig extension

#App/Twig/LogoutAfterMomentExtension.php  

<?php


namespace App\Twig;

use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class LogoutAfterMomentExtension extends AbstractExtension
{
    public function getFunctions()
    {
        return [
            new TwigFunction('logoutAfter', [$this, 'logoutAfter']),
        ];
    }

    public function logoutAfter(int $seconds)
    {
        return header( "refresh:".$seconds.";url=/admin/logout" );
    }

}

call the function in the template

#templates/layout.html.twig

<body>

{{ logoutAfter(5) }} #it will logout after 5 seconds
...

</body>