2

Using Symfony2 I have implemented an AJAX action to manage some bookmarks (add/remove) in my application. So a user needs to be authenticated to proceed. I have a solution that redirects user to login page but I think it would be better to use an event to handle this redirection.

Actual solution :

Check of user's authentication is done the same way that in FOSUserBundle.

Routing :

fbn_guide_manage_bookmark:
    path:  /bookmark/manage
    defaults: { _controller: FBNGuideBundle:Guide:managebookmark }
    options:
        expose: true
    requirements:
        _method:  POST 

Controller :

public function manageBookmarkAction(Request $request)
{
    if ($request->isXmlHttpRequest()) {

        $user = $this->getUser();

        if (!is_object($user) || !$user instanceof UserInterface) {            
            return new JsonResponse(array('status' => 'login'));
        } 

        // DO THE STUFF
    }   
}

jQuery :

$(function() {
    $('#bookmark').click(function() {
        $.ajax({
            type: 'POST',                  
            url: Routing.generate('fbn_guide_manage_bookmark'),
            data : xxxx, // SOME DATA
            success: function(data) {                
                if (data.status == 'login') {
                    var redirect = Routing.generate('fos_user_security_login');
                    window.location.replace(redirect);
                } else {
                    // DO THE STUFF       
                }
            },
        });
    }); 
});

Other solution ? :

In order not verify at controller level that user is authenticated, I would protect my route in security configuration file :

Security :

security:
    access_control:
        - { path: ^/(fr|en)/bookmark/manage, role: ROLE_USER }

Controller :

public function manageBookmarkAction(Request $request)
{
    if ($request->isXmlHttpRequest()) {

        $user = $this->getUser();

        // THIS VERIFCATION SHOULD NOW BE REMOVED
        /*
        if (!is_object($user) || !$user instanceof UserInterface) {            
            return new JsonResponse(array('status' => 'login'));
        } 
        */

        // DO THE STUFF
    }   
}   

Basically, when trying this solution, Symfony2 redirects internally ton login page as you can see with Firebug :

enter image description here

So my questions are :

  1. Does Symfony2 throws an event or an exception before redirection ? This would permits to use a listener to catch the event and set a JSON response for example ?
  2. In this case, what kind of response should be prepared ? Something like my first solution of something using a HTTP header code like 302 (or something else). How to handle this at AJAX level ?

I could see some exception event solution based but I think it is necessary to throw the exception at controller level and this is what I would like to avoid. Here is an example :

https://github.com/winzou/AssoManager/blob/master/src/Asso/AMBundle/Listener/AjaxAuthenticationListener.php

Cruz
  • 695
  • 8
  • 21

3 Answers3

10

Here is a solution (see here for details) :

Security :

firewalls:
        main:
            pattern:   ^/
            anonymous: true
            provider: fos_userbundle
            entry_point: fbn_user.login_entry_point
            #...
    access_control:
        - { path: ^/(fr|en)/bookmark/manage, role: ROLE_USER }

Services :

services:

    fbn_user.login_entry_point:
        class: FBN\UserBundle\EventListener\LoginEntryPoint
        arguments: [ @router ]

Service class :

namespace FBN\UserBundle\EventListener;

use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

/**
 * When the user is not authenticated at all (i.e. when the security context has no token yet), 
 * the firewall's entry point will be called to start() the authentication process. 
 */

class LoginEntryPoint implements AuthenticationEntryPointInterface
{
    protected $router;

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

    /**
     * This method receives the current Request object and the exception by which the exception 
     * listener was triggered. 
     * 
     * The method should return a Response object
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        if ($request->isXmlHttpRequest()) {  

            return new JsonResponse('',401);

        }

        return new RedirectResponse($this->router->generate('fos_user_security_login'));
    }
}

jQuery :

$(function() {
    $('#bookmark').click(function() {
        // DATA PROCESSING
        $.ajax({
            type: 'POST',                  
            url: Routing.generate('fbn_guide_manage_bookmark'),
            data : xxxx, // SOME DATA,
            success: function(data) {
                // DO THE STUFF 
            },
            error: function(jqXHR, textStatus, errorThrown) {
                switch (jqXHR.status) {
                    case 401:
                        var redirectUrl = Routing.generate('fos_user_security_login');
                        window.location.replace(redirectUrl);
                        break;
                    case 403: // (Invalid CSRF token for example)
                        // Reload page from server
                        window.location.reload(true);                        
                }               
            },
        });
    }); 
});
Community
  • 1
  • 1
Cruz
  • 695
  • 8
  • 21
  • great answer. I had been listening to the `kernel.controller` event, but security handled request earlier and I got login page html as ajax answer. But with this approach, it returns json now which I could handle. – Oleg Abrazhaev Aug 18 '17 at 12:10
1
  1. Yes, the event can be handled as described in this answer: https://stackoverflow.com/a/9182954/982075

  2. Use HTTP Status Code 401 (Unauthorized) or 403 (Forbidden)

    You can use the error function in jquery to handle the response

    $.ajax({
        type: 'POST',                  
        url: Routing.generate('fbn_guide_manage_bookmark'),
        data : xxxx, // SOME DATA
        error: function() {
            alert("Your session has expired");
        }
    });
    
Community
  • 1
  • 1
Marcel Burkhard
  • 3,453
  • 1
  • 29
  • 35
  • That sounds pretty good. I am actually implementing the solution. Coming back to accept your proposal when it's done. – Cruz Jul 13 '15 at 15:12
  • Access denied handler is only called if the user has unsufficient privilege to access the resource. See [here](http://stackoverflow.com/questions/11968354/symfony2-why-access-denied-handler-doesnt-work). "When the user is not authenticated at all, the firewall's entry point will be called to "start" the authentication process" (from Symfony doc). See [here](http://stackoverflow.com/questions/17428987/what-is-the-best-way-to-notify-a-user-after-an-access-control-rule-redirects/17432089#17432089) for details. The complete code for my problem in the following answer. – Cruz Jul 14 '15 at 16:57
0

I solved this for Symf4 (shouldn't be very different from others). The exception listener will provide JSON response for the POST before redirect happens. In other cases it will still redirect as usual. You can customize further how to handle exceptions in the listener.

=======================================================

sevices:
exeption_listener:
    class: Path\To\Listener\ExeptionListener
    arguments: ['@security.token_storage']
    tags:
        - { name: kernel.event_listener, event: kernel.exception }

=======================================================

Listener/ExeptionListener.php

<?php

namespace Tensor\UserBundle\Listener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;

use Symfony\Component\HttpFoundation\JsonResponse;

class ExeptionListener implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        // return the subscribed events, their methods and priorities
        return array(
           KernelEvents::EXCEPTION => array(
               array('processException', 10),
               array('logException', 0),
               array('notifyException', -10),
           )
        );
    }

    public function processException(GetResponseForExceptionEvent $event)
    {
        // ...
        if (!$event->isMasterRequest()) {
            // don't do anything if it's not the master request
            return;
        }
        $request = $event->getRequest();
        if( $request->getMethod() === 'POST' ){
            $event->setResponse(new JsonResponse(array('error'=>$event->getException()->getMessage()), 403));
        }
    }

    public function logException(GetResponseForExceptionEvent $event)
    {
        // ...
    }

    public function notifyException(GetResponseForExceptionEvent $event)
    {
        // ...
    }
}
user9371353
  • 1
  • 1
  • 1