Implémenter l’authentification par JWT

Après l’activation du CORS, et toujours dans l’optique de préparer notre CakePHP à fournir des données REST, nous allons activer une authentification dite « stateless ».

Notre objectif est de fournir une API de type REST à notre application. Cette API REST permettra les points suivants :

  • Authentifier un utilisateur
  • Fournir une liste de données au format JSON via une requête GET

L’API sera accessible sous la forme http://monserveur/api/donnees

Pour permettre l’accès à l’API par une application mobile, une application hybride ou un client Angular2, nous devons :

Une authentification « stateless », à la différence d’une authentification « stateful », n’oblige pas le serveur à stocker une session pour un utilisateur qui s’est authentifié.

Avec une authentification Stateful, le serveur doit conserver l’état de la connexion utilisateur qui vient de s’authentifier. Il conserve cet état au travers d’une session côté serveur. Un cookie de session est ensuite envoyé au client / navigateur. Ce cookie sera utilisé par le client / navigateur dans les requêtes vers le serveur. Le serveur utilise ensuite ce cookie pour pouvoir récupérer le contexte de l’utilisateur et agir en fonction de ses droits.

Avec l’authentification Stateless, un jeton est envoyé par le serveur vers le client une fois l’authentification réussie. Ce jeton est signé par une clef secrète (ou une clef publique/privée). Ce jeton sera utilisé lors des requêtes du client vers le serveur. Le serveur décodera et vérifiera le jeton d’authentification et autorisation ou non les actions selon ses droits. L’avantage du jeton, c’est que l’on peut y mettre autant d’informations (username, id, pays, langue, date d’expiration, etc.) que l’on veut. Dans ce type d’authentification, le serveur n’est plus obligé de stocker les informations utilisateurs dans sa session, un simple décodage du jeton suffit. Cela à l’avantage de récupérer un jeton à partir d’un serveur d’authentification et d’utiliser ce jeton vers un serveur de ressource par exemple.

Le protocole le plus souvent utilisé en stateless est le JWT (Json Web Token). L’implémentation du JWT est très souvent utilisé dans les applications mobiles, les applications REST, etc.

En séquence cela donne ceci (schéma issu du site imaginea) :

Vous trouverez un article très complet et très intéressant sur JWT sur ce blog.

Nous allons maintenant implémenter une authentification JWT dans CakePHP v3.

Avant de nous lancer comme des fous, sachez qu’il existe un plugin CakePHP v3 qui gère très bien le protocole JWT. Vous trouverez plus de détails sur ce plugin sur cette page.

Pour installer le plugin, utilisez composer :

composer require admad/cakephp-jwt-auth

Chargez ensuite ce plugin dans votre boostrap.php :

Plugin::load('ADmad/JwtAuth');

Je serais tenté de dire que le plus difficile est fait 🙂

Reste maintenant à créer une route /api sur notre serveur, cette route devra prendre en charge l’authentification JWT.

Pour cela créons un sous-répertoire « Api » dans notre répertoire « Src/Controller » et créons la classe de base AppController.php :

<?php
/**
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @link      http://cakephp.org CakePHP(tm) Project
 * @since     0.2.9
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */
namespace App\Controller\Api;

use Cake\Controller\Controller;
use Cake\Core\Configure;
use Cake\I18n\Date;
use Cake\I18n\I18n;
use Cake\I18n\Time;
use Cake\Network\Exception\UnauthorizedException;
use Cake\Utility\Security;
use Crud\Controller\ControllerTrait;
use Firebase\JWT\JWT;

/**
 * Application Controller
 *
 * Add your application-wide methods in the class below, your controllers
 * will inherit them.
 *
 * @link http://book.cakephp.org/3.0/en/controllers.html#the-app-controller
 */
class AppController extends Controller
{
    use ControllerTrait;

    /**
     * Initialization hook method.
     *
     * Use this method to add common initialization code like loading components.
     *
     * e.g. `$this->loadComponent('Security');`
     *
     * @return void
     */
    public function initialize()
    {
        parent::initialize();
        I18n::locale('fr_FR');
        Time::setToStringFormat('dd/MM/Y HH:mm');
        Date::setToStringFormat('dd/MM/YYYY');
        $this->loadComponent('RequestHandler');
        $this->loadComponent('Crud.Crud', ['listeners' => ['Crud.Api',
                                                           'Crud.ApiPagination',
                                                           'Crud.ApiQueryLog'],
                                           'actions'   => ['Crud.Index',
                                                           'Crud.View']]);
        $this->loadComponent('Auth', ['storage'              => (Configure::read('debug') ? "Session" : "Memory"),
                                      'authenticate'         => [
                                          'Form'              => ['fields' => ['username' => 'username',
                                                                               'password' => 'password']],
                                          'ADmad/JwtAuth.Jwt' => ['userModel'       => 'UserManager.Users',
                                                                  'fields'          => ['username' => 'id'],
                                                                  'parameter'       => 'token',
                                                                  'queryDatasource' => TRUE],
                                      ],
                                      'unauthorizedRedirect' => FALSE,
                                      'checkAuthIn'          => 'Controller.initialize',
                                      'loginRedirect'        => FALSE,
                                      'logoutRedirect'       => FALSE,
                                      'loginAction'          => FALSE]);
        $this->Auth->allow(['token']);
    }

    /**
     * Authentifie l'utilisateur et génère un JWT en cas de succès
     */
    public function token()
    {
        $user = $this->Auth->identify();
        if (!$user) {
            throw new UnauthorizedException('Invalid email or password');
        }

        $this->set(['success'    => TRUE,
                    'data'       => ['token' => $token = JWT::encode(['id'  => $user['id'],
                                                                      'sub' => $user['id'],
                                                                      'exp' => time() + 604800], Security::salt())],
                    '_serialize' => ['success',
                                     'data']]);
    }
}

Quelques explications :
‘userModel’ => ‘UserManager.Users’
Cette configuration indique quel modèle contient les informations utilisateurs. Pour ma part je me suis fait un plugin de gestion utilisateur (UserManager), ce qui explique la notation par point.

‘parameter’ => ‘token’
Par défaut le plugin récupère le JWT soit à partir du header « Request », soit à partir d’une variable dans une query GET, dans ce cas, il cherchera la clef définie par « parameter », dans notre cas ce serait « token »

‘queryDatasource’ => TRUE
Cela va indiquer au plugin JWT de récupérer les informations utilisateurs à partir de la table Users. Si l’on place ce paramètre à FALSE, il ne va retourner que les informations embarquées dans le JWT (le « payload »)

Une fois le plugin configuré, nous avons créé une fonction « token » qui se chargera d’authentifier l’utilisateur et de générer un JWT au client.

La variable importante ici est « token » :

JWT::encode([‘id’ => $user[‘id’],’sub’ => $user[‘id’],’exp’ => time() + 604800],Security::salt())

Nous utilisons le Helper du plugin pour générer le JWT. les données « sub » sont des données « payload » (qui sont rajoutées dans le JWT, mais optionnelles, pour notre convenance). Vous constaterez que nous chiffrons le JWT avec notre sel (salt) de l’application, et nous donnons une date d’expiration de 7 jours (604800 secondes).

La partie cliente va appeler /api/token en spécifiant dans les données POST le « username » et le « password » de l’utilisateur. Si l’authentification est réussie, le serveur va renvoyer le jeton JWT dans la variable « token ». C’est ce jeton que le client devra systématiquement utiliser dans les prochaines requêtes vers le serveur. Ce jeton devra être placé dans le Header « Authorization » avec le paramètre « Bearer » suivi du jeton.

La semaine prochaine, nous ferons un test d’authentification avec POSTMAN et nous verrons comment créer des routes REST pour récupérer des données de notre serveur en JSON.

Si vous avez des questions, n’hésitez pas à m’envoyer un mail, à poster un commentaire ou à écrire sur le forum de la communauté francophone. Je me ferais un plaisir de vous répondre !

Stay tuned !

Laisser un commentaire

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