Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to force password change using FOSUserBundle?

I'm attempting to implement a security feature in a Symfony 2.1 project where the admin can create a user with an initial password, and then when the user logs in the first time the change-password handler is fired automatically.

I'm running into problems overriding the FOSUserBundle classes and I'm thinking that surely this is already built in somehow, at least in part, although I can't see it in the docs anywhere.

I would like to use the credentials_expired flag in the entity. When the admin creates the user, this would be set to 1. When the user first logs in, credentials_expired is checked and rather than throwing an exception, change-password is fired. I've made it this far.

ChangePasswordController would then make sure that the password was actually changed (this doesn't seem like the default behavior in FOS) and the credentials_expired is set to 0. This is where I'm stuck. There are so many layers of services I can't seem to get things customized properly.

like image 912
David Avatar asked Mar 06 '13 19:03

David


2 Answers

Here is the detailed answer. Thanks Manu for the springboard!

First, make sure to get the correct FOSUserBundle in the composer.json file ("dev-master", NOT "*"):

"friendsofsymfony/user-bundle":"dev-master"

The following is all contained in my own user bundle, which extends the FOSUserBundle as instructed in the installation doc.

PortalFlare/Bundle/UserBundle/Resources/config/services.xml:

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services  http://symfony.com/schema/dic/services/services-1.0.xsd">

<parameters>
    <parameter key="portal_flare_user.forcepasswordchange.class">PortalFlare\Bundle\UserBundle\EventListener\ForcePasswordChange</parameter>
    <parameter key="portal_flare_user.passwordchangelistener.class">PortalFlare\Bundle\UserBundle\EventListener\PasswordChangeListener</parameter>
</parameters>

<services>
    <service id="portal_flare_user.forcepasswordchange" class="%portal_flare_user.forcepasswordchange.class%">
        <argument type="service" id="router" />
        <argument type="service" id="security.context" />
        <argument type="service" id="session" />
        <tag name="kernel.event_listener" event="kernel.request" method="onCheckStatus" priority="1" />
    </service>
    <service id="portal_flare_user.passwordchange" class="%portal_flare_user.passwordchangelistener.class%">
        <argument type="service" id="router" />
        <argument type="service" id="security.context" />
        <argument type="service" id="fos_user.user_manager" />
        <tag name="kernel.event_subscriber" />
    </service>
</services>

</container>

PortalFlare/Bundle/UserBundle/EventListener/ForcePasswordChange.php:

    <?php

namespace PortalFlare\Bundle\UserBundle\EventListener;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;

use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpFoundation\Session\Session;

/**
 * @Service("request.set_messages_count_listener")
 *
 */
class ForcePasswordChange {

  private $security_context;
  private $router;
  private $session;

  public function __construct(Router $router, SecurityContext $security_context, Session $session) {
    $this->security_context = $security_context;
    $this->router           = $router;
    $this->session          = $session;

  }

  public function onCheckStatus(GetResponseEvent $event) {

    if (($this->security_context->getToken()) && ($this->security_context->isGranted('IS_AUTHENTICATED_FULLY'))) {

      $route_name = $event->getRequest()->get('_route');

      if ($route_name != 'fos_user_change_password') {

        if ($this->security_context->getToken()->getUser()->hasRole('ROLE_FORCEPASSWORDCHANGE')) {

          $response = new RedirectResponse($this->router->generate('fos_user_change_password'));
          $this->session->setFlash('notice', "Your password has expired. Please change it.");
          $event->setResponse($response);

        }

      }

    }

  }

} 

PortalFlare/Bundle/UserBundle/EventListener/PasswordChangeListener.php:

<?php
namespace PortalFlare\Bundle\UserBundle\EventListener;

use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\Doctrine\UserManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\SecurityContext;

/**
 * Listener responsible to change the redirection at the end of the password change
 */
class PasswordChangeListener implements EventSubscriberInterface {
  private $security_context;
  private $router;
  private $usermanager;

  public function __construct(UrlGeneratorInterface $router, SecurityContext $security_context, UserManager $usermanager) {
    $this->security_context = $security_context;
    $this->router           = $router;
    $this->usermanager      = $usermanager;
  }

  /**
   * {@inheritDoc}
   */
  public static function getSubscribedEvents() {
    return array(
      FOSUserEvents::CHANGE_PASSWORD_SUCCESS => 'onChangePasswordSuccess',
    );
  }

  public function onChangePasswordSuccess(FormEvent $event) {

    $user = $this->security_context->getToken()->getUser();
    $user->removeRole('ROLE_FORCEPASSWORDCHANGE');
    $this->usermanager->updateUser($user);

    $url = $this->router->generate('_welcome');
    $event->setResponse(new RedirectResponse($url));
  }
}

The issue with FOSUserBundle not actually making sure the user changes the password is an issue for another day.

I hope this helps someone.

like image 195
David Avatar answered Oct 30 '22 11:10

David


A good approach could be start defining a ROLE_USER that will be the role that have access to the whole app. When user register, automatically adds him the ROLE_CREDENTIAL_EXPIRED or something like that. Using JMSSecurityExtraBundle, you can use annotations in your controller, deciding if users with a given role can access. Check also the docs of how Symfony handle the HTTP Authentication.

like image 26
Manu Avatar answered Oct 30 '22 11:10

Manu