Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

FOSUserBundle - Force password change after first login

In a Symfony2 application using FOSUserBundle for user management, the user table has been filled through an import script from a csv file and the password generated from a combination of data.

I would like to force the user to change his password at the first login.

When the event FOSUserEvents::SECURITY_IMPLICIT_LOGIN occurs, redirect to the route fos_user_change_password if the field last_login is NULL.

My idea was rewriting the method onImplicitLogin(UserEvent $event) of the class AGI\UserBundle\EventListener\LastLoginListener like this but the class is not overwritten:

public function onImplicitLogin(UserEvent $event) {
    $user = $event->getUser ();

    if ($user->getLastLogin () === null) {
        $user->setLastLogin ( new \DateTime () );
        $this->userManager->updateUser ( $user );
        $response = new RedirectResponse ( $this->router->generate ( 'fos_user_change_password' ) );
        $this->session->getFlashBag ()->add ( 'notice', 'Please change your password' );
        $event->setResponse ( $response );
    }
}

I already have a bundle overwriting FOSUserBundle and it works for controllers, forms, etc but It looks like it is not the way to do it with eventListeners.

How can I force the user to change the password after the first login?

like image 590
dmrrlc Avatar asked Dec 03 '14 19:12

dmrrlc


2 Answers

With the help of the precious hint from @sjagr about fos_user.security.implicit_login that drove me to fos_user.security.implicit_login and an external topic about doing stuff right after login, I got a working solution.

AGI\UserBundle\Resources\config\services.yml

login_listener:
    class: 'AGI\UserBundle\EventListener\LoginListener'
    arguments: ['@security.context', '@router', '@event_dispatcher']
    tags:
        - { name: 'kernel.event_listener', event: 'security.interactive_login', method: onSecurityInteractiveLogin }

AGI\UserBundle\EventListener\LoginListener.php

<?php

namespace AGI\UserBundle\EventListener;

use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class LoginListener {

    private $securityContext;
    private $router;
    private $dispatcher;

    public function __construct(SecurityContext $securityContext, Router $router, EventDispatcherInterface $dispatcher) {
        $this->securityContext = $securityContext;
        $this->router = $router;
        $this->dispatcher = $dispatcher;
    }
    public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) {
        if ($this->securityContext->isGranted ( 'IS_AUTHENTICATED_FULLY' )) {
            $user = $event->getAuthenticationToken ()->getUser ();

            if ($user->getLastLogin () === null) {
                $this->dispatcher->addListener ( KernelEvents::RESPONSE, array (
                        $this,
                        'onKernelResponse' 
                ) );
            }
        }
    }
    public function onKernelResponse(FilterResponseEvent $event) {
        $response = new RedirectResponse ( $this->router->generate ( 'fos_user_change_password' ) );
        $event->setResponse ( $response );
    }
}

Thank you

like image 198
dmrrlc Avatar answered Nov 08 '22 15:11

dmrrlc


If you require user change password due to some business rules, you can use kernel request EventListener:

<?php

namespace App\EventListener;

use App\Model\UserInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class ChangePasswordListener
{
    private TokenStorageInterface $security;

    private RouterInterface $router;

    private array $excludedRoutes = [
        'admin_change_password',
        'admin_login',
        'admin_login_check',
        'admin_logout',
    ];

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

    public function onKernelRequest(RequestEvent $event): void
    {
        if (false === $event->isMasterRequest()) {
            return;
        }

        if ($event->getRequest()->isXmlHttpRequest()) {
            return;
        }

        $currentRoute = $event->getRequest()->get('_route');
        if (\in_array($currentRoute, $this->excludedRoutes, true)) {
            return;
        }

        $token = $this->security->getToken();

        if (null === $token) {
            return;
        }

        $user = $token->getUser();

        if ($user instanceof UserInterface && $user->shouldPasswordChange()) {
            $response = new RedirectResponse($this->router->generate('admin_security_profile_change_password'));
            $event->setResponse($response);
        }
    }
}

services.yaml:

services:
    App\EventListener\ChangePasswordListener:
        arguments:
            - '@security.token_storage'
            - '@router'
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: -100 }

You should provide also own UserInterface with method "shouldPasswordChange" and custom implementation of it.

It works great with Symfony 5.0 and PHP 7.4 but if you modify this code it should works also for lower PHP versions.

like image 44
tomcyr Avatar answered Nov 08 '22 15:11

tomcyr