Configurer la sécurité de Symfony2 pour une connexion 100% AJAX

Avec l'arrivée de nouvelles technologies comme Angular JS, il n'est pas rare d'avoir un backend 100% basé sur des APIs et la gestion des problématiques de connexion est assez important. Dans ce post nous ne parlerons pas de l'échange entre 2 applications car le principe est vraiment d'utiliser la même session PHP donc le même domaine. Il n'y a pas d'échange de token, ce n'est pas nécessaire dans notre cas.

 Introduction

Ce que nous voulons faire est relativement simple:

  • Emettre une réponse avec un code d'erreur 401 lorsque l'on souhiate accéder à une page sécurisée.
  • Emmetre une réponse avec une code d'erreur 401 lorsque la procédure de login n'a pas fonctionné
  • Emmetre un code de réponse 200 avec une réponse JSON lorsque le login s'est bien déroulé.

Par défaut Symfony2 fonctionne avec des redirections vers la page de login configurée dans notre firewall (fichier security.yml) il faut donc développer quelques outils simple pour faire fonctionner le tout proprement. Celà se fera avec 3 classes:

  • Une implémentation de la classe AuthenticationSuccessHandlerInterface, qui va nous permettre de renvoyer une réponse au format JSON et un code de succès 200
  • Une implémentation de la classe AuthenticationFailureHandler, qui va nous permettre de renvoyer une 401 avec les erreurs d'authentification. Généralement on a juste besoin de savoir que ce n'est pas bon pour éviter de donner trop de détails aux potentiels logiciels malveillants.
  • Une implémentation de la classe AuthenticationEntryPoint qui va nous permettre d'éviter la redirection vers la page de login et renvoyer un code 401.

C'est assez simple en définitive, mais il faut le savoir. La dernière étape sera de configurer la sécurité de votre application. Dans notre cas nous mettrons toutes les routes d'API sécurisées derrière l'URL /frontend-api. Mais vous pouvez faire ça comme vous le préférez !

L'implémentation de AuthenticationSuccessHandlerInterface

<?php

/*
 * This file is part of the UCS package.
 *
 * Copyright 2014 Nicolas Macherey (nicolas.macherey@gmail.com)
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace UCS\Bundle\SecurityBundle\Handler;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;

/**
 * class AuthenticationSuccessHandler
 *
 * @author Nicolas Macherey <nicolas.macherey@gmail.com>
 */
class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function onAuthenticationSuccess( Request $request, TokenInterface $token )
    {
        $response = new JsonResponse(['success' => true, 'username' => $token->getUser()->getUsername()], 200);
        $response->headers->set('Content-Type', 'application/json');

        return $response;
    }
}

Franchement simple... et rien de bien méchant ! Il faut bien sur déclarer ce service dans le service container... En yaml ça donne:

services:
  ucs.security.authentication_success_handler:
      class: UCS\Bundle\SecurityBundle\Handler\AuthenticationSuccessHandler

L'implémentation de la classe AuthenticationFailureHandlerInterface

<?php

namespace UCS\Bundle\SecurityBundle\Handler;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;

/**
 * class AuthenticationFailureHandler
 *
 * @author Nicolas Macherey <nicolas.macherey@gmail.com>
 */
class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
    /**
     * {@inheritdoc}
     */
    public function onAuthenticationFailure( Request $request, AuthenticationException $exception )
    {
        $response = new JsonResponse(['success' => false], 401);
        $response->headers->set('Content-Type', 'application/json');

        return $response;
    }
}

Idem très simple on renvoi simplement notre 401... Il faut bien sur déclarer ce service dans le service container... En yaml ça donne:

services:
  ucs.security.authentication_failure_handler:
      class: UCS\Bundle\SecurityBundle\Handler\AuthenticationFailureHandler

L'implémentation de la classe AuthenticationEntryPointInterface

<?php

namespace UCS\Bundle\SecurityBundle\EntryPoint;

/* Imports */
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;

/**
 * Class AuthenticationEntryPoint
 * Returns a 401 if the user is not logged in instead of redirecting to the login page
 *
 * @author Nicolas Macherey <nicolas.macherey@gmail.com>
 */
class AuthenticationEntryPoint implements AuthenticationEntryPointInterface
{
    /**
     * {@inheritdoc}
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        $response = new Response('', 401);
        $response->headers->set('Content-Type', 'application/json');

        return $response;
    }
}

Voilà encore une implémentation très compliquée de la classe... Il faut bien sur déclarer ce service dans le service container... En yaml ça donne:

services:
  ucs.security.authentication_entry_point:
      class: UCS\Bundle\SecurityBundle\EntryPoint\AuthenticationEntryPoint

Finalisation configuration de notre firewall

Pour finir il faut faire un peut de config... Je vous laisse lire la doc pour expliquer à Symfony quel provider vous utiliserez dans notre cas nous l'avons appelé 'frontend'... Voilà le résultat de notre configuration de sécurité:

security:
    encoders:
        # define your encoders here

    providers:
        # define your providers here
        frontend:
            # Add your config

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt|error)|css|images|js)/
            security: false
        frontend:
            entry_point: ucs.security.authentication_entry_point
            pattern: ^/frontend-api
            anonymous: false
            provider: frontend
            context: _security_frontend
            form_login:
                check_path: /frontend-api/login_check
                failure_handler: ucs.security.authentication_failure_handler
                success_handler: ucs.security.authentication_success_handler
            remember_me:
                key: %secret%
        default:
            anonymous: ~
            provider: frontend
            context: _security_frontend

    access_control:
        - { path: ^/frontend-api, roles: ROLE_USER }

Ce qu'on peut remarquer, c'est que nous n'avons nullement besoin de définir les routes dans le routing car seule la route login_check est utilisée et nous n'en avons pas besoin d'autre... (sauf pour le logout). Nous avons juste a préciser dans la configuration les services que nous voulons utiliser.

Conclusion

Voilà... Vous avez en quelques minutes configuré votre firewall pour travailler avec un front 100% ajax type AngularJS ou Ajax. C'est simple mais si on ne le sait pas on y arrive pas ! Bien sur ce sont des services... Vous pouvez faire ce que vous voulez dedans !

Comments

comments powered by Disqus