2
votes

I am quite new in Symfony 2. I have to build an application in Symfony 2 (I'm using 2.8 and also latest version of FOSUser Bundle). Majority of work is done, although I have to do some changes in default security settings. I was looking for them for two days and I have only really foggy concept how this can be fixed. The supposed changes are following:

  • REGISTRATION - I have enabled confirmation by e-mail and I don't know how to make to the confirmation link expire after one hour and/or after one usage (one click). There is setting in config.yaml settting ttl, but only for password resseting.
  • REGISTRATION - Before confirming users are prevented from logging in and there is Symfony 2 Exception - Disabled Account working, rendering short message about it. Actually I have to set redirection to another page (I suppose template) to render custom message that 'this account is blocked...' and link to send another link with confirmation token (Am I right? The link in email is confirmation token?).
  • RESETTING - As mentioned before ttl for link (token?) ressetting password is set for one hour, but I don't know how to make it expire after one usage (one click).

I know how to override some template of FOSUser, but I have no clear idea which of files should I override to change these things.

I have noticed that my FOSUser uses Symfony 2 Exceptions files and if I have changed content of message in my Exception file, it has changed also on my page, but I don't know how to make it well and override it, adding all necessary features.

I was trying to override AuthenticationListener (from FOSUser) with use of Compiler Pass (http://symfony.com/doc/current/cookbook/service_container/compiler_passes.html), but I don't know if it worked, because any changes in overriden Listener were not visible. Actually I don't know if this is the file I should override.

I have check a few Stackoverflow questions, but I haven't found an answer.

FOSUser Bundle - Prevent Disabled users from logging in This doesn't work for me, because users are prevented and I need only override message of exception and create redirection with another link sending confirmation one.

FOS user bundle authentication I have tried to implement the solution pointed here, but it didn't work and I am not sure if I really need such a complicated solution.

Thanks for help in advance and I someone need to see my files, configuration just write and I will post here necessary ones.

1
And by the way, while I was looking for some answer I have met some types of files like Event, EventListener, Helper, Provider and I am not sure about their usage, if anyone can explain it in plain words I will be very gratefull. - Bartosz Kubicki
have you read my comments below? - sonja

1 Answers

2
votes

I have finally figured all these things out. If someone has a similar problem i advise to read these topics in Symfony documentation: Overriding FOSUser B controllers Hooking into a controller

It turns out that for described features I need to override some files form FOSUser Bundle, sometimes Controller was enough, sometimes I needed to modify EventListener (actually I have even created my own event). There is more than one way.

The hardest part was one-click link for ressetting password. I have used a flag, which is set to false while sending an e-mail and set true while clicking on link to prevent from using link once more. The problem is, that Resetting Controller is 'executed' two times, so while clicking submit there was redirection, beacuse flag was true. I have added some counting in session in order to omit the part of code which checks the flag, when you hit the submit button (second usage of the Reset Method in Resetting Controller), but it prevented only from clicking submit second time, so actually you can not use the link two times, but you can see form two times, which is not an effect I wanted to reach, but is far better than nothing. If someone has an idea how to upgrade it I will be gratefull

<?php



namespace My\UserBundle\Controller;

use FOS\UserBundle\Controller\ResettingController as FOSResettingController;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use My\UserBundle\Entity\User;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use My\UserBundle\UserEvents;

/**
 * Controller managing the resetting of the password
 *
 * @author Thibault Duplessis <[email protected]>
 * @author Christophe Coevoet <[email protected]>
 */
class ResettingController extends FOSResettingController
{


    /**
     * Request reset user password: submit form and send email
     */
    public function sendEmailAction(Request $request)
    {
        $username = $request->request->get('username');

        /** @var $user UserInterface */
        $user = $this->get('fos_user.user_manager')->findUserByUsernameOrEmail($username);

        if (null === $user) {
            return $this->render('FOSUserBundle:Resetting:request.html.twig', array(
                'invalid_username' => $username
            ));
        }

        if ($user->isPasswordRequestNonExpired($this->container->getParameter('fos_user.resetting.token_ttl'))) {
            return $this->render('FOSUserBundle:Resetting:passwordAlreadyRequested.html.twig');
        }

        if (null === $user->getConfirmationToken()) {
            /** @var $tokenGenerator \FOS\UserBundle\Util\TokenGeneratorInterface */
            $tokenGenerator = $this->get('fos_user.util.token_generator');
            $user->setConfirmationToken($tokenGenerator->generateToken());
        }

        $this->get('fos_user.mailer')->sendResettingEmailMessage($user);
        $user->setPasswordRequestedAt(new \DateTime());
        $user->setPasswordRequestedClicked(false);
        $this->get('fos_user.user_manager')->updateUser($user);

        $_SESSION['views'] = 1;

        return new RedirectResponse($this->generateUrl('fos_user_resetting_check_email',
            array('email' => $this->getObfuscatedEmail($user))
        ));
    }


    /**
     * Reset user password
     */
    public function resetAction(Request $request, $token)
    {
        /** @var $formFactory \FOS\UserBundle\Form\Factory\FactoryInterface */
        $formFactory = $this->get('fos_user.resetting.form.factory');
        /** @var $userManager \FOS\UserBundle\Model\UserManagerInterface */
        $userManager = $this->get('fos_user.user_manager');
        /** @var $dispatcher \Symfony\Component\EventDispatcher\EventDispatcherInterface */
        $dispatcher = $this->get('event_dispatcher');

        $user = $userManager->findUserByConfirmationToken($token);


        //Here there is a reaction for using expired token (column confirmation token === null) - redirection to page with possibility of sending another one.
        if (null === $user) {
               return $this->redirectToRoute('fos_user_invalid_token_click');

        }



        if ($_SESSION['views'] == 1){
            $event = new GetResponseUserEvent($user, $request);
            $dispatcher->dispatch(UserEvents::RESETTING_RESET_CLICK_CHECK, $event);

            if (null !== $event->getResponse()) {
                return $event->getResponse();
            } else {
                $user->setPasswordRequestedClicked(true);
                $userManager->updateUser($user);
                $_SESSION['views']++;
                $_SESSION['views']++;
            }
        } else {
            $_SESSION['views']++;
        }

        if ($_SESSION['views'] == 5){
            $event = new GetResponseUserEvent($user, $request);
            $dispatcher->dispatch(UserEvents::RESETTING_RESET_CLICK_CHECK, $event);

            if (null !== $event->getResponse()) {
                return $event->getResponse();
            }
        }



        $event = new GetResponseUserEvent($user, $request);
        $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_INITIALIZE, $event);

        if (null !== $event->getResponse()) {
            return $event->getResponse();
        }



        $form = $formFactory->createForm();
        $form->setData($user);




        $form->handleRequest($request);


        if ($form->isValid()) {
            $event = new FormEvent($form, $request);
            $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_SUCCESS, $event);


            $userManager->updateUser($user);

            if (null === $response = $event->getResponse()) {
                $url = $this->generateUrl('fos_user_profile_show');
                $response = new RedirectResponse($url);
            }

            $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_COMPLETED, new FilterUserResponseEvent($user, $request, $response));

            return $response;
        }

        $userManager->updateUser($user);
        return $this->render('FOSUserBundle:Resetting:reset.html.twig', array(
            'token' => $token,
            'form' => $form->createView(),
        ));
    }



    public function InvalidTokenTtlMessageAction() {
        return $this->render('UserBundle:Resetting:invalidTokenTtlRes.html.twig');
    }

    public function InvalidTokenClickMessageAction() {
        return $this->render('UserBundle:Resetting:invalidTokenClickRes.html.twig');
    }


}

My listener:

  <?php



namespace My\UserBundle\EventListener;

use FOS\UserBundle\EventListener\ResettingListener as FOSResettingListener;
use FOS\UserBundle\FOSUserEvents;
use My\UserBundle\UserEvents;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\Event\GetResponseUserEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use My\UserBundle\Entity\User;

class ResettingListener extends FOSResettingListener
{
    private $router;
    private $tokenTtl;

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

    public static function getSubscribedEvents()
    {
        return array(
            UserEvents::RESETTING_RESET_CLICK_CHECK => 'onResettingClickCheck',
            FOSUserEvents::RESETTING_RESET_INITIALIZE => 'onResettingResetInitialize',
            FOSUserEvents::RESETTING_RESET_SUCCESS => 'onResettingResetSuccess',

        );
    }

    public function onResettingClickCheck(GetResponseUserEvent $event){
        //checking if link hasn't expired due to its usage
        if ($event->getUser()->isPasswordRequestedClicked() === true){
            $event->setResponse(new RedirectResponse($this->router->generate('fos_user_invalid_token_click')));
        }
    }

    public function onResettingResetInitialize(GetResponseUserEvent $event)
    {
        //checking if link hasn't expired due to exceeding token Ttl
        if (!$event->getUser()->isPasswordRequestNonExpired($this->tokenTtl)) {
            $event->setResponse(new RedirectResponse($this->router->generate('fos_user_invalid_token_ttl')));
        }

    }

    public function onResettingResetSuccess(FormEvent $event)
    {

        /** @var $user \FOS\UserBundle\Model\UserInterface */
        $user = $event->getForm()->getData();

        $user->setConfirmationToken(null);
        $user->setPasswordRequestedAt(null);
        $user->setEnabled(true);



    }
}

and my User entity:

 <?php

namespace My\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use My\BackendBundle\Entity;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\Common\Collections\ArrayCollection;


/**
 * User
 *
 * @ORM\Table(name="user")
 * @ORM\Entity(repositoryClass="My\UserBundle\Repository\UserRepository")
 */
class User extends BaseUser
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer", nullable = false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;


    /**
     * @Gedmo\Slug(fields={"username"})
     * @ORM\Column(length=128, unique=true)
     */
    private $slug;


    /**
     *
     * @ORM\ManyToMany(targetEntity="\My\BackendBundle\Entity\Event", mappedBy="users")
     * @ORM\JoinColumn(name="id", referencedColumnName="id", nullable=false)
     * @ORM\OrderBy({"date"="ASC"})
     *
     */
    protected $events;



    /**
     * @var \Doctrine\Common\Collections\ArrayCollection $event_org
     * @ORM\OneToMany(targetEntity="\My\BackendBundle\Entity\Event", mappedBy="user_org", cascade={"all"})
     */
    protected $event_org;



    /**
     * @var \DateTime
     * @ORM\Column(name="confirmation_token_requested_at", type="datetime")
     */
    protected $confirmationTokenRequestedAt;


    /**
     * @var boolean
     * @ORM\Column(name="password_requested_clicked", type="boolean", nullable=true)
     */
    protected $passwordRequestedClicked;





    public function __toString()
    {
        return $this->getUsername();
    }


    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    public function __construct()
    {
        parent::__construct();
        $this->event_org = new \Doctrine\Common\Collections\ArrayCollection();


    }

    /**
     * Add event
     *
     * @param \My\BackendBundle\Entity\Event $event
     *
     * @return User
     */
    public function addEvent(\My\BackendBundle\Entity\Event $event)
    {
        $this->events[] = $event;
        $event->addUser($this);

        return $this;
    }

    /**
     * Remove event
     *
     * @param \My\BackendBundle\Entity\Event $event
     */
    public function removeEvent(\My\BackendBundle\Entity\Event $event)
    {
        $this->events->removeElement($event);
    }

    /**
     * Get events
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getEvents()
    {
        return $this->events;
    }






    /**
     * Set slug
     *
     * @param string $slug
     *
     * @return User
     */
    public function setSlug($slug)
    {
        $this->slug = $slug;

        return $this;
    }

    /**
     * Get slug
     *
     * @return string
     */
    public function getSlug()
    {
        return $this->slug;
    }

    /**
     * Add eventOrg
     *
     * @param \My\BackendBundle\Entity\Event $eventOrg
     *
     * @return User
     */
    public function addEventOrg(\My\BackendBundle\Entity\Event $eventOrg)
    {
        $this->event_org[] = $eventOrg;

        return $this;
    }

    /**
     * Remove eventOrg
     *
     * @param \My\BackendBundle\Entity\Event $eventOrg
     */
    public function removeEventOrg(\My\BackendBundle\Entity\Event $eventOrg)
    {
        $this->event_org->removeElement($eventOrg);
    }

    /**
     * Get eventOrg
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getEventOrg()
    {
        return $this->event_org;
    }

        /**
     * Set confirmationTokenRequestedAt
     *
     * @param \DateTime $confirmationTokenRequestedAt
     *
     * @return User
     */
    public function setConfirmationTokenRequestedAt(\DateTime $date = null)
    {
        $this->confirmationTokenRequestedAt = $date;

        return $this;
    }

    /**
     * Gets the timestamp that the user requested a confirmation_token.
     *
     * @return null|\DateTime
     */
    public function getConfirmationTokenRequestedAt()
    {
        return $this->confirmationTokenRequestedAt;
    }

    public function isConfirmationTokenNonExpired($ttl)
    {

        return $this->getConfirmationTokenRequestedAt() instanceof \DateTime &&
        $this->getConfirmationTokenRequestedAt()->getTimestamp() + $ttl > time();
    }

    /**
     * Set passwordRequestedClicked
     *
     * @param boolean $passwordRequestedClicked
     *
     * @return User
     */
    public function setPasswordRequestedClicked($boolean)
    {
        $this->passwordRequestedClicked = (Boolean) $boolean;

        return $this;
    }

    /**
     * Get passwordRequestedClicked
     *
     * @return boolean
     */
    public function getPasswordRequestedClicked()
    {
        return $this->passwordRequestedClicked;
    }

    /**
     * Checks whether the user has used password request.
     *
     *
     * @return Boolean true if the user is enabled, false otherwise
     */
    public function isPasswordRequestedClicked() {
        return $this->passwordRequestedClicked;
    }
}

If someone would like to get code for remaining problems, please write me a message and I will provide it here :).

Resetting controller:

<?php
namespace My\UserBundle\Controller;

use FOS\UserBundle\Controller\ResettingController as FOSResettingController;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\Model\UserInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use My\UserBundle\Entity\User;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use My\UserBundle\UserEvents;

class ResettingController extends FOSResettingController

{

public function sendEmailAction(Request $request)
{
    $username = $request->request->get('username');

    $user = $this->get('fos_user.user_manager')->findUserByUsernameOrEmail($username);

    if (null === $user) {
        return $this->render('FOSUserBundle:Resetting:request.html.twig', array(
            'invalid_username' => $username
        ));
    }

    if ($user->isPasswordRequestNonExpired($this->container->getParameter('fos_user.resetting.token_ttl'))) {
        return $this->render('FOSUserBundle:Resetting:passwordAlreadyRequested.html.twig');
    }

    if (null === $user->getConfirmationToken()) {
        $tokenGenerator = $this->get('fos_user.util.token_generator');
        $user->setConfirmationToken($tokenGenerator->generateToken());
    }

    $this->get('fos_user.mailer')->sendResettingEmailMessage($user);
    $user->setPasswordRequestedAt(new \DateTime());
    $user->setPasswordRequestedClicked(false);
    $this->get('fos_user.user_manager')->updateUser($user);

    $_SESSION['views'] = 1;

    return new RedirectResponse($this->generateUrl('fos_user_resetting_check_email',
        array('email' => $this->getObfuscatedEmail($user))
    ));
}


public function resetAction(Request $request, $token)
{
    $formFactory = $this->get('fos_user.resetting.form.factory');
    $userManager = $this->get('fos_user.user_manager');
    $dispatcher = $this->get('event_dispatcher');

    $user = $userManager->findUserByConfirmationToken($token);


    //Here there is a reaction for using expired token (column confirmation token === null) - redirection to page with possibility of sending another one.
    if (null === $user) {
           return $this->redirectToRoute('fos_user_invalid_token_click');

    }

    if ($_SESSION['views'] == 1){
        $event = new GetResponseUserEvent($user, $request);
        $dispatcher->dispatch(UserEvents::RESETTING_RESET_CLICK_CHECK, $event);

        if (null !== $event->getResponse()) {
            return $event->getResponse();
        } else {
            $user->setPasswordRequestedClicked(true);
            $userManager->updateUser($user);
            $_SESSION['views']++;
            $_SESSION['views']++;
        }
    } else {
        $_SESSION['views']++;
    }

    if ($_SESSION['views'] == 5){
        $event = new GetResponseUserEvent($user, $request);
        $dispatcher->dispatch(UserEvents::RESETTING_RESET_CLICK_CHECK, $event);

        if (null !== $event->getResponse()) {
            return $event->getResponse();
        }
    }

    $event = new GetResponseUserEvent($user, $request);
    $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_INITIALIZE, $event);

    if (null !== $event->getResponse()) {
        return $event->getResponse();
    }


    $form = $formFactory->createForm();
    $form->setData($user);

    $form->handleRequest($request);


    if ($form->isValid()) {
        $event = new FormEvent($form, $request);
        $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_SUCCESS, $event);


        $userManager->updateUser($user);

        if (null === $response = $event->getResponse()) {
            $url = $this->generateUrl('fos_user_profile_show');
            $response = new RedirectResponse($url);
        }

        $dispatcher->dispatch(FOSUserEvents::RESETTING_RESET_COMPLETED, new FilterUserResponseEvent($user, $request, $response));

        return $response;
    }

    $userManager->updateUser($user);
    return $this->render('FOSUserBundle:Resetting:reset.html.twig', array(
        'token' => $token,
        'form' => $form->createView(),
    ));
}



public function InvalidTokenTtlMessageAction() {
    return $this->render('UserBundle:Resetting:invalidTokenTtlRes.html.twig');
}

public function InvalidTokenClickMessageAction() {
    return $this->render('UserBundle:Resetting:invalidTokenClickRes.html.twig');
}
}