How to Fix Symfony2 Ajax Login Redirect

You probably noticed that sometimes an Ajax request will return the login page instead of the actual content is should return. This happens when the user has beed logged out in the background and the current page does not reflect that (it could happen if the session expired or if the user simply logged out from another browser window/tab).

Here's a quick way to fix this: we will create an event listener that will catch this authentication exception, check for an Ajax request and, if found, it will return a 403 http code instead of redirecting to the login page. The JavaScript code will then know to reload the page and thus redirect to login in case of 403 instead of loading and showing the received content to the user.

Here's the Symfony2 event listener:

<?php
// src/AppBundle/EventListener/AjaxAuthenticationListener.php

namespace AppBundle\EventListener;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
/**
 */
class AjaxAuthenticationListener
{

    /**
     * Handles security related exceptions.
     *
     * @param GetResponseForExceptionEvent $event An GetResponseForExceptionEvent instance
     */
    public function onCoreException(GetResponseForExceptionEvent $event)
    {
        $exception = $event->getException();
        $request = $event->getRequest();

        if ($request->isXmlHttpRequest()) {
            if ($exception instanceof AuthenticationException || $exception instanceof AccessDeniedException) {
                $event->setResponse(new Response('', 403));
            }
        }
    }
}

As always, we will have to register it as a service:

services:
    ajax.authentication.listener:
        class: AppBundle\EventListener\AjaxAuthenticationListener
        tags:
          - { name: kernel.event_listener, event: kernel.exception, method: onCoreException, priority: 1000 }

In the JavaScript code we add the following to make jQuery treat the Ajax errors by reloading the window in case of a 403 error. What will actually happen is that the user will end on the login page as he is no longer authenticated.

$(document).ready(function() {
    $(document).ajaxError(function (event, jqXHR) {
        if (403 === jqXHR.status) {
            window.location.reload();
        }
    });
});

 

 


How To Enable Email Confirmation On Fosuserbundle Profile Edit

We all know and use FOSUserBundle in our Symfony applications, so much it became kind of a standard. It provides everything you need for user management: login, registration, email confirmation and much more control over the access of the user in your application. But we found a thing missing from this awesome package: email confirmation after the initial email address has been changed through a profile edit. In the following lines we will show you how to extend the FOSUserBundle to implement this.

This post assumes you are familiar (even advanced) with the Symfony framework and FOSUserBundle.

To get started we will need a listener to be triggered when a profile edit has happened, FOSUserBundle fires two events that we are interested in: FOSUserEvents::PROFILE_EDIT_INITIALIZE and FOSUserEvents::PROFILE_EDIT_SUCCESS. The first one is triggered before the actual profile data is changed so we will use that to get a hold on the original email address. When the second event is fired, we will compare the initial email address with the current one and, if they are not the same, we will start the confirmation process:

<?php
// src/AppBundle/EventListener/ProfileEditListener.php

namespace AppBundle\EventListener;

use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use FOS\UserBundle\Event\GetResponseUserEvent;
use FOS\UserBundle\Mailer\MailerInterface;
use FOS\UserBundle\Util\TokenGeneratorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

class ProfileEditListener implements EventSubscriberInterface
{
    private $oldEmail;
    private $mailer;
    private $tokenGenerator;
    private $router;
    private $session;
    private $tokenStorage;

    public function __construct(MailerInterface $mailer, TokenGeneratorInterface $tokenGenerator, UrlGeneratorInterface $router, SessionInterface $session, TokenStorageInterface $tokenStorage)
    {
        $this->mailer = $mailer;
        $this->tokenGenerator = $tokenGenerator;
        $this->router = $router;
        $this->session = $session;
        $this->tokenStorage = $tokenStorage;
    }

    public static function getSubscribedEvents()
    {
        return array(
            FOSUserEvents::PROFILE_EDIT_INITIALIZE => 'onProfileEditInitialize',
            FOSUserEvents::PROFILE_EDIT_SUCCESS => 'onProfileEditSuccess'
        );
    }

    public function onProfileEditInitialize(GetResponseUserEvent $event)
    {
        $this->oldEmail = $event->getUser()->getEmail();
    }
    
    public function onProfileEditSuccess(FormEvent $event)
    {
        $user = $event->getForm()->getData();
        if ($user->getEmail() !== $this->oldEmail)
        {
            // disable user
            $user->setEnabled(false);

            // send confirmation token to new email
            $user->setConfirmationToken($this->tokenGenerator->generateToken());
            $this->mailer->sendConfirmationEmailMessage($user);

            // force user to log-out
            $this->tokenStorage->setToken();

            // redirect user to check email page
            $this->session->set('fos_user_send_confirmation_email/email', $user->getEmail());
            $url = $this->router->generate('fos_user_registration_check_email');
            $event->setResponse(new RedirectResponse($url));
        }
    }
}

Now, add this to your services.yml file and you're good to go:

    app.profile_edit_listener:
        class: AppBundle\EventListener\ProfileEditListener
        arguments: [@fos_user.mailer, @fos_user.util.token_generator, @router, @session, @security.token_storage]
        tags:
            - { name: kernel.event_subscriber }

One last thing: you will probably want to change the email template that is sent to the user with the confirmation link. You can overwrite it by creating app/Resources/FOSUserBundle/views/Registration/email.txt.twig and put what you need in there (use the original one from vendor/friendsofsymfony/user-bundle/Resources/views/Registration/email.txt.twig to see how to get the confirmation link).