How to immediately disable access to a user that is soft-deleted or no longer enabled?

Viewed 757

In my application, users with the role ROLE_ADMIN are able to manually disable other user accounts, by setting enabled on the user account to false.

Using a user checker, the user will not be able to log in the next time they try:

public function checkPostAuth(UserInterface $user)
{
    if (!$user->isEnabled()) {
        throw new CustomUserMessageAuthenticationException(
            'Account is not enabled.'
        );
    }
}

My issue with this, it only works when a user tries to log in. If a user is currently logged in (could also be using the remember-me-functionality), nothing happens until they logout.

Is there any way to immediately disable the user from making requests to routes it no longer should have access to, even when this user is currently logged in?

What I could do is check if the account is enabled for every route that requires this user access, like so:

if ($this->getUser()->isEnabled() === false) {
    throw new \Exception("Account disabled");
}

But this seems like a terrible solution because I would need this in many places.

2 Answers

If you implement the Symfony\Component\Security\Core\User\EquatableInterface in your User class, the User will be logged out when isEqualTo() returns false.

class User implements Symfony\Component\Security\Core\User\EquatableInterface
{
    /* ... */

    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof User) {
            return false;
        }

        if ($this->enabled !== $user->enabled) {
            // Forces the user to log in again if enabled 
            // changes from true to false or vice-versa.
            // This could be replaced by a more sophisticated comparison.
            return false;
        }

        // do a bunch of other checks, such as comparing the 
        // password hash: this will cause the user to be logged 
        // out when their password is changed.

        return true;
    }

    /* ... */
}

Relevant documentation

I followed Cerad's advice, and added a listener as can be seen below. However, for a better (built-in) solution, see Pete's answer.

namespace App\EventListener;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Security;

class RequestListener
{

    private $security; // needed to get the user
    private $router; // needed to generate the logout-url

    public function __construct(Security $security, UrlGeneratorInterface $router) 
    {
        $this->security = $security;
        $this->router = $router;
    }

    public function __invoke(RequestEvent $event) : void
    {
        $user = $this->security->getUser();

        // Don't do anything if no user is logged in.
        if ($user === null) {
            return;
        }

        if ($user->isEnabled() === false) {
            // Generate the logout url based on the path name and redirect to it.
            $url = $this->router->generate('app_logout');
            $event->setResponse(new RedirectResponse($url));
        }
    }
}
Related