Cruz
Cruz

Reputation: 725

Symfony2 : AJAX request : How to handle authentication when needed?

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

Upvotes: 2

Views: 6094

Answers (3)

user9371353
user9371353

Reputation: 1

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)
    {
        // ...
    }
}

Upvotes: 0

Cruz
Cruz

Reputation: 725

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);                        
                }               
            },
        });
    }); 
});

Upvotes: 11

Marcel Burkhard
Marcel Burkhard

Reputation: 3523

  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");
        }
    });
    

Upvotes: 1

Related Questions