Reputation: 336
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?
Upvotes: 22
Views: 40724
Reputation: 101
Solution from @coma adapted for symfony 6.3:
services.yaml:
parameters:
session_max_idle_time: 3600
services:
app.handler.session_idle:
class: App\Handler\SessionIdleHandler
arguments: ["@router", "@security.helper", '%session_max_idle_time%']
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
SessionIdleHandler.php
<?php
namespace App\Handler;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class SessionIdleHandler
{
protected $session;
protected $router;
protected $maxIdleTime;
protected $security;
public function __construct(RouterInterface $router, Security $security, $maxIdleTime = 0)
{
$this->router = $router;
$this->security = $security;
$this->maxIdleTime = $maxIdleTime;
}
public function onKernelRequest(RequestEvent $event): ?Response {
if (HttpKernelInterface::MAIN_REQUEST !== $event->getRequestType()) {
return null;
}
if ($this->maxIdleTime > 0) {
$event->getRequest()->getSession()->start();
$lapse = time() - $event->getRequest()->getSession()->getMetadataBag()->getLastUsed();
if ($lapse > $this->maxIdleTime) {
$this->security->logout(false);
$event->getRequest()
->getSession()
->getFlashBag()
->set('warning', 'You were logged out due to inactivity');
$event->setResponse(new RedirectResponse($this->router->generate('home')));
}
}
return null;
}
}
Upvotes: 0
Reputation: 167
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 }
Upvotes: 1
Reputation: 11
#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" );
}
}
#templates/layout.html.twig
<body>
{{ logoutAfter(5) }} #it will logout after 5 seconds
...
</body>
Upvotes: -1
Reputation: 779
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 }
Upvotes: 2
Reputation: 1751
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 }]
Upvotes: 8
Reputation: 16649
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
Upvotes: 59
Reputation: 1847
What about:
#app/config/config.yml
framework:
session:
cookie_lifetime: 1800
Upvotes: 0
Reputation: 11
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
Upvotes: -1
Reputation: 1121
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!
Upvotes: 17
Reputation: 2495
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: ~
Upvotes: 1
Reputation: 320
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.
}
Upvotes: 2