Comportamiento inesperado al expirar un token junto con remember_me en Symfony

Hola, estoy desarrollando una aplicación que tiene los siguientes requerimientos:

  • Poder guardar datos en sesión por el máximo de tiempo permitido por la sesión (idioma preferido, referencia de entrada, etc).
  • El usuario debe ser deslogueado automáticamente después de un determinado tiempo de inactividad a menos que haya checkeado la opción de "Remember me".

Para resolverlo mantengo los datos de sesión después de que el usuario se desloguea y para desloguearlo automáticamente uso el siguiente subscriber:

<?php
 
namespace AppBundle\EventListener;
 
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
 
class SessionListener implements EventSubscriberInterface
{
    protected $maxIdleTime;
 
    public function __construct($maxIdleTime = 0)
    {
        $this->maxIdleTime = $maxIdleTime;
    }
 
    public function onKernelRequest(GetResponseEvent $event)
    {
        if (!$event->isMasterRequest()) {
            return;
        }
 
        $session = $event->getRequest()->getSession();
 
        if ($this->maxIdleTime > 0) {
 
            $lapse = time() - $session->getMetadataBag()->getLastUsed();
 
            if ($lapse > $this->maxIdleTime) {
 
                throw new AuthenticationExpiredException;
            }
        }
    }
 
    public static function getSubscribedEvents()
    {
        return array(
            KernelEvents::REQUEST => array(array('onKernelRequest', 7)),
        );
    }
 
}

Si me logueo sin checkear _remember_me todo funciona según lo esperado y transcurrido el tiempo $maxIdleTime me redirige al formulario de login. El problema es que al chequear _remember_me, después del tiempo de inactividad, si recargo una página protegida me redirige igualmente al login a pesar de que sigo autenticado (Con el token RememberMeToken).

¿Hay alguna manera de que me mantenga en la página sin redirigirme al login?

Respuestas

#1

Yo diría que el comportamiento que se produce es el esperado. En tu código, el método onKernelRequest() solo comprueba si ha pasado el tiempo de expiración, pero nunca comprueba si estás usando Remember Me o no.

Diría que la solución es sencilla, ya que el token que representa al usuario es diferente si usas usuario+contraseña (UsernamePasswordToken) o Remember Me (RememberMeToken). Así que en tu código solo tienes tienes que comprobar si el token es de tipo RememberMeToken.

#2

Hola, antes que nada gracias por tu tiempo dedicado a responder. Con inesperado me refería a inesperado por mí, ojalá los scripts hagan lo que esperamos y no lo que escribimos... je.

Lo de comprobar el RememberMeToken lo hice, pero el comportamiento es el mismo cuando aún no tengo el RememberMeToken. Por lo que me dice el log el ciclo de la petición es el siguiente (Una vez superado el tiempo de inactividad):

  1. Encuentra la ruta correspondiente.
  2. Lee el token de la sesión (En este momento es PostAuthenticationGuardToken pero supongo que con UsernamePasswordToken es igual).
  3. Verifica la validez del token.
  4. Ejecuta la excepción del listener. (En este momento elimina el token y me redirige al punto de entrada, o sea el login)
  5. Repite el ciclo en la ruta login con la diferencia de que me autentica con el RememberMeToken.
  6. Estoy en el login, autenticado con RememberMeToken.

Se me ocurren algunas soluciones, quizás haya alguna mejor o mas eficiente y no logro verla:

  1. Ejecutar el listener antes del firewall y en vez de lanzar una excepción eliminar de la sesión la clave _security_firewall_context. El problema con esta solución es que no encuentro manera de obtener firewall_context. En este punto el token todavía es null por lo que no puedo ni obtener el tipo ni usar el método getProviderKey. Si pongo en firewall_context el valor a mano funciona bien pero con un pequeño bug: Si me deslogueo después de vencido el tiempo máximo de inactividad me mantiene logueado, supongo que al haber eliminado el valor de la sesión elimino algo más que la info del token o que al ejecutarse el logout y no existir ningún valor en sesión se salta el proceso.
  2. Verificar que el usuario tenga la opción de remember me y reemplazar el token existente por uno RememberMeToken en vez de lanzar la excepción. Lo que por un lado no estoy del todo seguro como implementarlo y por otro sería repetir código.

¿Cuál de las soluciones es mas correcta? ¿Hay alguna solución mejor?

Otro problema que veo es que la excepción también se ejecuta en páginas que no están protegidas por el firewall, por lo que supongo que debo crear otro listener para capturarla ya sea esta misma o una personalizada.

Así está mi subscriber en este momento:

<?php
 
namespace AppBundle\EventListener;
 
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationExpiredException;
 
class SessionListener implements EventSubscriberInterface
{
    protected $tokenStorage;
    protected $maxIdleTime;
 
    public function __construct(TokenStorageInterface $tokenStorage, $maxIdleTime = 0)
    {
        $this->tokenStorage = $tokenStorage;
        $this->maxIdleTime = $maxIdleTime;
    }
 
    public function onKernelRequest(GetResponseEvent $event)
    {
        if (!$event->isMasterRequest()) {
            return;
        }
 
        if ($this->maxIdleTime > 0) {
 
            $token = $this->tokenStorage->getToken();
 
            if(null !== $token && !$token instanceof RememberMeToken){
 
                $session = $event->getRequest()->getSession();
 
                $lapse = time() - $session->getMetadataBag()->getLastUsed();
 
                if ($lapse > $this->maxIdleTime) {
 
                    throw new AuthenticationExpiredException;
                }
            }
        }
    }
 
    public static function getSubscribedEvents()
    {
        return array(
            KernelEvents::REQUEST => array(array('onKernelRequest', 7)),
        );
    }
 
}