[OAuth2][Symfony] Injection du token de sécurité automatique

Après avoir mis en place le bundle HWIOAuthBundle pour utiliser l’Oauth 2, vous devez récupérer toutes vos données via l’API rest en question. Et pour chaque requête vous devez passer le token dans les HEADERS de la requête. Nous allons voir ici comment injecter ce token automatiquement.

Pré requit

Je considère que vous avez bien configuré votre bundle HWIOAuthBundle. Vous arrivez donc à connecter vos utilisateurs via l’API et vous avez un utilisateur connecté dans Symfony avec le token dans les attribues de l’utilisateur. Voici un extrait de ma classe user (sans les accesseurs) :

namespace AppBundle\Entity;

use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="user")
 */
class User extends BaseUser
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;
    
    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $googleId;
    
    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $googleAccessToken;
    
    
    public function __construct()
    {
        parent::__construct();
        // your own logic
    }
}

A ce stade, vous avez donc votre utilisateur connecté et donc accès une instance de votre classe User avec le googleAccessToken de renseigné.

Afin de centraliser la gestion des erreurs et pouvoir passer le token automatiquement, j’ai choisi de créer un service pour faire mes requêtes. De plus, dans mon cas, le service utilisé n’est pas google, mais un service relativement lent à répondre. J’ai donc prévu la possibilité de mettre en cache le résultat de mes requêtes.

En ce qui concerne les requêtes, je passe par la bibliothèque curl/curl (https://packagist.org/packages/curl/curl) qui simplifie les appels.

Nous allons répartir les tâches pour avoir des composants réutilisables :

  • Un service ‘CurlRequestService’ qui va utiliser Curl pour faire les requêtes demandés et retourner le résultat, gérer les erreurs potentielles, recevoir les entêtes des requêtes, mettre en cache les résultats des requêtes.
  • Un service de cache qui sera injecté dans CurlRequestService (cela peut être un cache de session, de fichier, de base de données, memcache, …)
  • Un service qui va se charger de générer les entêtes pour les requêtes de notre API, dans notre cas, il prendra en paramètre tokenStorage symfony pour récupérer le token de l’API rest et aura une méthode pour récupérer les entête sous forme de tableau

La classe CurlRequestService

Nous avons donc un constructeur qui prend en paramètre le cache a potentiellement utilisé (je ne l’utilise pas volontairement dans cet article pour ne pas surchargé le code).

La méthode setDefaultHeader permet d’injecter les entête à utiliser pour chaque requêtes.

sendRequestAndGetResult est le coeur du service, cette fonction est une mini-passerelle pour Curl, c’est également elle qui devra gérer le cache dans la version finale. addHeadersToCurl permet juste de passer le tableau d’entête à curl



<?php /** * User: Jeremy_R * Date: 02/02/2018 * Time: 15:47 */ namespace UtilitiesBundle\Http; use Http\Discovery\Exception\NotFoundException; use Psr\Log\LoggerInterface; use Symfony\Component\Cache\Adapter\AbstractAdapter; /** * Class RequestService * Cette classe permet de faire des requettes http avec une gestion de mise en cache (si un 'gestionnaire de cache est passé en paramètre') * * @package UtilitiesBundle\Http */ class CurlRequestService { /** * @var AbstractAdapterobjet objet de mise en cache */ protected $cache; /** * @var int Durée d'expiration par defaut du cache */ protected $defaultExpirationTime; /** * @var permet de logguer les différents smesages d'erreurs */ protected $logger; /** * @var array */ protected $defaultHeader; /** * CurlRequestService constructor. * * @param AbstractAdapter|null $cache permet de mettre en cache le résultat d'une requête * @param int $defaultExpirationTime Temps en seconde par defaut pour le cache */ public function __construct(AbstractAdapter $cache = null, LoggerInterface $logger, $defaultExpirationTime = 600) { $this->cache = $cache;
		$this->logger = $logger;
		$this->defaultExpirationTime = $defaultExpirationTime;
	}
	
	/**
	 * @param array $defaultHeader          entete par defaut
	 * @return $this
	 */
	public function setDefaultHeader(array $defaultHeader)
	{
		$this->defaultHeader = $defaultHeader;
		return $this;
	}
	
	/**
	 * @param string $methode
	 * @param string $url
	 * @param array $parameters
	 * @param array $header
	 */
	public function sendRequestAndGetResult(string $methode, string $url, array $parameters = [], array $header = [] )
	{
		if (!in_array($methode, ['get', 'post', 'put', 'patch', 'delete']))
		{
			throw new \Exception('Methode not valid : ' . $methode);
		}
		$curl = new \Curl\Curl();
		$request_header = array_merge($header, $this->defaultHeader);
		$this->addHeadersToCurl($curl, $request_header);
		
		$curl->$methode($url, $parameters);
		
		if ($curl->error)
		{
			if ($curl->error_code == "404")
			{
				$this->logger->error('Ressource non trouvé : ', [
					'code' => $curl->error_code,
					'curl_response' => $curl->response,
					'methode' => $methode,
					'url' => $url,
					'parameters' => $parameters,
					'request_header' => $request_header,
				]);
				throw new NotFoundException('Ressource non trouvé : ' . $methode . ' - ' .$url);
			}
			elseif($curl->error_code == "401")
			{
				// L'utilisateur n'est pas authorisé, le token est expirer est-ce que l'on déconnecte l'utilisateur
				// throw new ExpiredTokenException(); // pour simplifier l'article, je parlais de cette partie dans un second article
			}
			else
			{
				$this->logger->critical('Une erreur interne est survenue: ', [
					'code' => $curl->error_code,
					'curl_response' => $curl->response,
					'methode' => $methode,
					'url' => $url,
					'parameters' => $parameters,
					'request_header' => $request_header,
				]);
				throw new \Exception('Une erreur interne est survenue : ' . $methode . ' - ' .$url);
			}
		}
		else
		{
			$result = json_decode($curl->response);
		}
		
		return $result;
		// traiter les erreurs, configurer le service dans le service.yml + créer un service qui injecte le token utilisateur dans les defaultHeaders
	}
	
	/**
	 * @param \Curl\Curl $curl          objet de requête curl
	 * @param array $headers            Tableau des header de la requête
	 */
	protected function addHeadersToCurl(\Curl\Curl $curl, array $headers) : void
	{
		foreach ($headers as $key => $value)
		{
			$curl->setHeader($key, $value);
		}
	}
	
}

Déclarons ce service auprès de Symfony :

services:
	...........................
	utilities.http.curlRequest:
        public: true
        class: Ageo\UtilitiesBundle\Http\CurlRequestService
        arguments: [null, '@logger']

Ce service pourrait être utilisé seul pour faire des requêtes. Exemple d’appelle d’un service get dans un controller :


$curlRequest = $this->get('utilities.http.curlRequest');
$assureData = $curlRequest->sendRequestAndGetResult('get', 'http://localhost/Service/api/getuserData', [
	'assureId' => $user->getGoogleId(),
]);

Mais notre but est tout autre 😈 (enfin tout autre, il ne faut peut être pas exagérer non plus …). Pour rappel, nous souhaitons injecter en entête le jeton d’accès a l’API. Pour faire cela, nous allons créer un autre service qui va prendre le ‘tokenStorage’ de l’utilisateur connecté à Symfony et le mettre en forme pour le passer en paramètre à notre service créer précédemment. Le constructeur isole les données de l’utilisateur connecter et ‘getSecurityHeaders’ retourne un tableau avec le type de données authorisée et le jeton d’autorisation.

<?php /** * User: Jeremy_R * Date: 05/02/2018 * Time: 12:09 */ namespace AppBundle\Security; class InjectHeaderFromUserData { protected $user; public function __construct($token_storage) { $this->user = $token_storage->getToken()->getUser();
	}
	
	public function getSecurityHeaders() : array
	{
		return [
			'Accept ' => 'application/json',
			'Authorization  ' => 'Bearer ' . $this->user->getSgsanteAccessToken(),
		];
	}
}

Dans la foulée, on déclare ce service :

services:
	.........
	app.inject_header_from_user_data:
        class: AppBundle\Security\InjectHeaderFromUserData
        arguments: ['@security.token_storage']

Il ne nous reste plus qu’à injecter le résultat de la méthode ‘getSecurityHeaders’ dans ‘CurlRequestService’ par sa méthode ‘setDefaultHeader’.

❗ Hein !? Je vais devoir faire quelque chose de ce genre a chaque appel ? oO


$curlRequest = $this->get('utilities.http.curlRequest');
	$injectHeaderFromUserData = $this->get('app.inject_header_from_user_data');
	$curlRequest->setDefaultHeader($injectHeaderFromUserData->getSecurityHeaders());
	$assureData = $curlRequest->sendRequestAndGetResult('get', 'http://localhost/AssureRest/api/assure', [
		'assureId' => $user->getGoogleId(),
	]);

Hé bien non rassurez-vous, les concepteurs de Symfony ont prévu de pouvoir injecter le résultat d’une méthode d’un service dans un autre service. On reprend la déclaration du service CurlRequestService pour le compléter :


services:
	...........................
	utilities.http.curlRequest:
        public: true
        class: Ageo\UtilitiesBundle\Http\CurlRequestService
        arguments: [null, '@logger']
        calls:
            - [setDefaultHeader, ["@=service('app.inject_header_from_user_data').getSecurityHeaders()"]] 

Pour rappel, les ‘calls’ sont appelés lorsque l’on récupère le service via le gestionnaire de service. Ici, la méthode ‘setDefaultHeader’ va être appelée avec pour argument, « @=service(‘app.inject_header_from_user_data’).getSecurityHeaders() », qui est donc l’appel à la méthode ‘getSecurityHeaders’ du service ‘app.inject_header_from_user_data’

 

Voilà, vous pouvez désormais appeler l’api sans vous soucier de passer le jeton de sécurité à chacune de vos requêtes.

PS: a l’utilisation, vous verrez que si le jeton de sécurité expire, l’API vous renverra une erreur 401. Notre service actuel ne gère pas ce code d’erreur et vous aurez dons un résultat erroné 😥 Dans le prochain article nous verrons comment gérer ce code et rediriger l’utilisateur sur une page d’erreur qui le redirigera vers la page de login !

 

Ajouter un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *