Autorisation PHP avec JWT (JSON Web Tokens)

Il fut un temps où la seule façon de s’authentifier auprès d’une application était de fournir ses informations d’identification (généralement un nom d’utilisateur ou une adresse e-mail et un mot de passe) et une session était ensuite utilisée pour maintenir l’état de l’utilisateur jusqu’à ce qu’il se déconnecte. Un peu plus tard, nous avons commencé à utiliser des API d’authentification. Et plus récemment encore, les JWT, ou JSON Web Tokens, ont été de plus en plus utilisés comme un autre moyen d’authentifier les demandes adressées à un serveur.

Dans cet article, vous apprendrez ce que sont les JWTs et comment les utiliser avec PHP pour effectuer des requêtes d’utilisateurs authentifiés.

JWTs versus Sessions

Mais d’abord, pourquoi les sessions ne sont pas une bonne chose ? Eh bien, il y a trois raisons principales :

  • Les données sont stockées en texte clair sur le serveur.
    Même si les données ne sont généralement pas stockées dans un dossier public, toute personne ayant un accès suffisant au serveur peut lire le contenu des fichiers de session.
  • Ils impliquent des requêtes de lecture/écriture du système de fichiers.
    Chaque fois qu’une session démarre ou que ses données sont modifiées, le serveur doit mettre à jour le fichier de session. Il en va de même chaque fois que l’application envoie un cookie de session. Si vous avez un grand nombre d’utilisateurs, vous pouvez vous retrouver avec un serveur lent, à moins d’utiliser des options alternatives de stockage de session, comme Memcached et Redis.
  • Applications distribuées/clusterisées.
    Comme les fichiers de session sont, par défaut, stockés sur le système de fichiers, il est difficile de disposer d’une infrastructure distribuée ou en grappe pour les applications à haute disponibilité – celles qui nécessitent l’utilisation de technologies telles que les équilibreurs de charge et les serveurs en grappe. D’autres supports de stockage et des configurations spéciales doivent être mis en œuvre – et ce, en étant pleinement conscient de leurs implications.

JWT

Maintenant, commençons à apprendre les JWTs. La spécification des jetons Web JSON (RFC 7519) a été publiée pour la première fois le 28 décembre 2010, et sa dernière mise à jour date de mai 2015.

Les JWT présentent de nombreux avantages par rapport aux clés API, notamment :

  • Les clés d’API sont des chaînes de caractères aléatoires, alors que les JWT contiennent des informations et des métadonnées. Ces informations et métadonnées peuvent décrire un large éventail de choses, comme l’identité d’un utilisateur, les données d’autorisation et la validité du jeton dans un laps de temps donné ou par rapport à un domaine.
  • Les JWT ne nécessitent pas d’autorité centralisée d’émission ou de révocation.
  • Les JWT sont compatibles avec OAUTH2.
  • Les données JWT peuvent être inspectées.
  • Les JWTs ont des contrôles d’expiration.
  • Les JWT sont destinés à des environnements où l’espace est limité, comme les en-têtes d’autorisation HTTP.
  • Les données sont transmises au format JavaScript Object Notation (JSON).
  • Les JWT sont représentés en utilisant Encodage Base64url

À quoi ressemble un JWT ?

Voici un exemple de JWT :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MTY5MjkxMDksImp0aSI6ImFhN2Y4ZDBhOTVjIiwic2NvcGVzIjpbInJlcG8iLCJwdWJsaWNfcmVwbyJdfQ.XCEwpBGvOLma4TCoh36FU7XhUbcskygS81HE1uHLf0E

À première vue, il semble que la chaîne de caractères soit juste des groupes aléatoires de caractères concaténés avec un point ou un caractère de point. En tant que telle, elle peut sembler peu différente d’une clé API. Cependant, si vous regardez de plus près, il y a trois chaînes distinctes.

L’en-tête JWT

La première chaîne est l’en-tête JWT. Il s’agit d’un Base64 Le jeton est une chaîne JSON encodée par URL. Elle spécifie l’algorithme cryptographique utilisé pour générer la signature, ainsi que le type du jeton, qui est toujours défini comme suit JWT. L’algorithme peut être soit symétrique ou asymétrique.

Un algorithme symétrique utilise une clé unique pour créer et vérifier le jeton. La clé est partagée entre le créateur du JWT et le consommateur de celui-ci. Il est essentiel de s’assurer que seuls le créateur et le consommateur connaissent le secret. Autrement, n’importe qui peut créer un jeton valide.

Un site algorithme asymétrique utilise une clé privée pour signer le jeton et une clé publique pour le vérifier. Ces algorithmes doivent être utilisés lorsqu’un secret partagé n’est pas pratique ou que les autres parties ont seulement besoin de vérifier l’intégrité du jeton.

La charge utile du JWT

La deuxième chaîne est la charge utile du JWT. Il s’agit également d’une chaîne JSON codée en Base64 et en URL. Elle contient des champs standard, appelés « claims ». Il existe trois types de revendications : enregistré, public et privé.

Revendications enregistrées sont prédéfinies. Vous pouvez en trouver la liste dans la RFC de JWT. En voici quelques-uns, couramment utilisés :

  • iat: l’horodatage de l’émission du jeton.
  • key: une chaîne unique, qui pourrait être utilisée pour valider un jeton, mais qui va à l’encontre de l’absence d’une autorité émettrice centralisée.
  • iss: une chaîne contenant le nom ou l’identifiant de l’émetteur. Peut être un nom de domaine et peut être utilisé pour rejeter les jetons d’autres applications.
  • nbf: un horodatage de la date à laquelle le jeton doit commencer à être considéré comme valide. Doit être égal ou supérieur à iat.
  • exp: un horodatage de la date à laquelle le jeton doit cesser d’être valide. Doit être supérieur à iat et nbf.

Revendications publiques peuvent être définies comme bon vous semble. Cependant, elles ne peuvent pas être les mêmes que les revendications enregistrées, ou les revendications de revendications publiques déjà existantes. Vous pouvez créer des revendications privées à volonté. Elles sont uniquement destinées à être utilisées entre deux parties : un producteur et un consommateur.

La signature du JWT

La signature du JWT est un mécanisme cryptographique conçu pour sécuriser les données du JWT avec une signature numérique unique au contenu du jeton. La signature garantit l’intégrité du JWT afin que les consommateurs puissent vérifier qu’il n’a pas été altéré par un acteur malveillant.

La signature du JWT est une combinaison de trois éléments :

  • l’en-tête du JWT
  • la charge utile du JWT
  • une valeur secrète

Ces trois éléments sont signés numériquement (non cryptées) en utilisant l’algorithme spécifié dans l’en-tête du JWT. Si nous décodons l’exemple ci-dessus, nous aurons les chaînes JSON suivantes :

L’en-tête du JWT

{
    "alg": "HS256",
    "typ": "JWT"
}

Les données du JWT

{
    "iat": 1416929109,
    "jti": "aa7f8d0a95c",
    "scopes": [
        "repo",
        "public_repo"
    ]
}

Essayer jwt.io pour vous-même, où vous pouvez jouer avec l’encodage et le décodage de vos propres JWTs.

Utilisons les JWTs dans une application PHP

Maintenant que vous avez appris ce que sont les JWT, il est temps d’apprendre à les utiliser dans une application PHP.

Il existe de nombreuses façons d’intégrer les JWT, mais voici comment nous allons procéder.

Toutes les demandes adressées à l’application, à l’exception de la page de connexion et de déconnexion, doivent être authentifiées via un JWT. Si un utilisateur fait une demande sans JWT, il sera redirigé vers la page de connexion.

Après qu’un utilisateur ait rempli et soumis le formulaire de connexion, le formulaire sera soumis via JavaScript au endpoint de connexion, authenticate.php dans notre application. Le point final extrait les informations d’identification (un nom d’utilisateur et un mot de passe) de la demande et vérifie leur validité.

S’ils le sont, il génère un JWT et le renvoie au client. Lorsque le client reçoit un JWT, il le stocke et l’utilise pour chaque demande future adressée à l’application.

Pour un scénario simpliste, il n’y aura qu’une seule ressource que l’utilisateur pourra demander – un fichier PHP appelé à juste titre resource.php. Il ne fera pas grand-chose, se contentant de renvoyer une chaîne de caractères contenant l’horodatage actuel au moment de la demande.

Il y a plusieurs façons d’utiliser les JWTs lors des requêtes. Dans notre application, le JWT sera envoyé dans le fichier l’en-tête d’autorisation Bearer.

Si vous n’êtes pas familier avec l’autorisation du porteur, il s’agit d’une forme d’authentification HTTP, où un jeton (tel qu’un JWT) est envoyé dans un en-tête de requête. Le serveur peut inspecter le jeton et déterminer si l’accès doit être accordé au « porteur » du jeton.

Voici un exemple d’en-tête :

Authorization: Bearer ab0dde18155a43ee83edba4a4542b973

Pour chaque requête reçue par notre application, PHP va tenter d’extraire le jeton de l’en-tête Bearer. S’il est présent, il est alors validé. S’il est valide, l’utilisateur verra la réponse normale pour cette requête. En revanche, si le JWT est invalide, l’utilisateur ne sera pas autorisé à accéder à la ressource.

Veuillez noter que JWT a été pas conçu pour remplacer les cookies de session.

Conditions préalables

Pour commencer, nous devons avoir PHP et composer installé sur nos systèmes.

Dans le répertoire racine du projet, exécutez composer install. Cela va permettre d’extraire Firebase PHP-JWT, une bibliothèque tierce qui simplifie le travail avec les JWT, ainsi que laminas-config conçu pour simplifier l’accès aux données de configuration dans les applications.

Le formulaire de connexion

Exemple de formulaire de connexion utilisant HTML et JavaScript

Une fois la bibliothèque installée, passons en revue le code de connexion dans le fichier authenticate.php. Nous commençons par effectuer la configuration habituelle, en nous assurant que l’autoloader généré par Composer est disponible.

<?php

declare(strict_types=1);

use FirebaseJWTJWT;

require_once('../vendor/autoload.php');

Après avoir reçu la soumission du formulaire, les informations d’identification sont validées par rapport à une base de données ou à un autre magasin de données. Pour les besoins de cet exemple, nous supposerons qu’ils sont valides, et nous définirons le paramètre $hasValidCredentials à true.

<?php

// extract credentials from the request

if ($hasValidCredentials) {

Ensuite, nous initialisons un ensemble de variables qui seront utilisées pour générer le JWT. Gardez à l’esprit que puisqu’un JWT peut être inspecté côté client, ne pas d’y inclure des informations sensibles.

Une autre chose qui vaut la peine d’être soulignée, encore une fois, est que $secretKey ne serait pas initialisé de cette manière. Vous le définissez probablement dans l’environnement et l’extrayez, en utilisant une bibliothèque telle que phpdotenv ou dans un fichier de configuration. J’ai évité de le faire dans cet exemple, car je veux me concentrer sur le code JWT.

Ne le divulguez jamais et ne le stockez pas sous contrôle de version comme git !

$secretKey  = 'bGS6lzFqvvSQ8ALbOxatm7/Vk7mLQyzqaS34Q4oR1ew=';
$issuedAt   = new DateTimeImmutable();
$expire     = $issuedAt->modify('+6 minutes')->getTimestamp();      // Ajoute 60 secondes
$serverName = "your.domain.name";
$username   = "username";                                           // Récupéré à partir des données POST filtré

$data = [
    'iat'  => $issuedAt->getTimestamp(),         // Issued at:  : heure à laquelle le jeton a été généré
    'iss'  => $serverName,                       // Émetteur
    'nbf'  => $issuedAt->getTimestamp(),         // Pas avant..
    'exp'  => $expire,                           // Expiration
    'userName' => $username,                     // Nom d'utilisateur
];

Une fois les données utiles prêtes, nous utilisons la fonction statique php-jwt encode pour créer le JWT.

La méthode :

  • transforme le tableau en JSON
  • produit les en-têtes
  • signe la charge utile
  • encode la chaîne finale

Il prend trois paramètres :

  • les informations sur la charge utile
  • la clé secrète
  • l’algorithme à utiliser pour signer le jeton

En appelant echo sur le résultat de la fonction, le jeton généré est retourné :

<?php
    // Encoder le tableau en une chaîne JWT.
    echo JWT::encode(
        $data,
        $secretKey,
        'HS512'
    );
}

Consommer le JWT

Récupérer une ressource en utilisant JavaScript et JWTs

Maintenant que le client dispose du jeton, vous pouvez le stocker en utilisant JavaScript ou tout autre mécanisme de votre choix. Voici un exemple de la façon de procéder en utilisant le JavaScript classique. Dans index.html après une soumission réussie du formulaire, le JWT renvoyé est stocké en mémoire, le formulaire de connexion est masqué et le bouton permettant de demander l’horodatage est affiché :

const store = {};
const loginButton = document.querySelector('#frmLogin');
const btnGetResource = document.querySelector('#btnGetResource');
const form = document.forms[0];

// Insère le jwt dans l'objet store.
store.setJWT = function (data) {
  this.JWT = data;
};

loginButton.addEventListener('submit', async (e) => {
  e.preventDefault();

  const res = await fetch('/authenticate.php', {
    method: 'POST',
    headers: {
      'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8'
    },
    body: JSON.stringify({
      username: form.inputEmail.value,
      password: form.inputPassword.value
    })
  });

  if (res.status >= 200 && res.status <= 299) {
    const jwt = await res.text();
    store.setJWT(jwt);
    frmLogin.style.display = 'none';
    btnGetResource.style.display = 'block';
  } else {
    // Handle errors
    console.log(res.status, res.statusText);
  }
});

Utilisation du JWT

Lorsque l’on clique sur le bouton « Obtenir l’horodatage actuel », une requête GET est envoyée à l’adresse suivante resource.phpqui définit le JWT reçu après authentification dans l’en-tête Authorization.

btnGetResource.addEventListener('click', async (e) => {
  const res = await fetch('/resource.php', {
    headers: {
      'Authorization': `Bearer ${store.JWT}`
    }
  });
  const timeStamp = await res.text();
  console.log(timeStamp);
});

Lorsque nous cliquons sur le bouton, une requête similaire à la suivante est effectuée :

GET /resource.php HTTP/1.1
Host: yourhost.com
Connection: keep-alive
Accept: */*
X-Requested-With: XMLHttpRequest
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE0MjU1ODg4MjEsImp0aSI6IjU0ZjhjMjU1NWQyMjMiLCJpc3MiOiJzcC1qd3Qtc2ltcGxlLXRlY25vbTFrMy5jOS5pbyIsIm5iZiI6MTQyNTU4ODgyMSwiZXhwIjoxNDI1NTkyNDIxLCJkYXRhIjp7InVzZXJJZCI6IjEiLCJ1c2VyTmFtZSI6ImFkbWluIn19.HVYBe9xvPD8qt0wh7rXI8bmRJsQavJ8Qs29yfVbY-A0

En supposant que le JWT est valide, nous voyons la ressource, après quoi la réponse est écrite dans la console.

Validation du JWT

Enfin, voyons comment nous pouvons valider le jeton en PHP. Comme toujours, nous utiliserons l’autoloader de Composer. Nous pourrions alors, optionnellement, vérifier si la bonne méthode de requête a été utilisée. J’ai sauté le code pour faire cela, pour continuer à me concentrer sur le code spécifique à JWT :

<?php
chdir(dirname(__DIR__));

require_once('../vendor/autoload.php');

// Effectuez une vérification de la méthode de demande ici, si vous le souhaitez.

Ensuite, le code tente d’extraire le jeton de l’en-tête Bearer. Je l’ai fait en utilisant preg_match. Si vous n’êtes pas familier avec cette fonction, elle effectue une correspondance par expression régulière sur une chaîne de caractères.

L’expression régulière que j’ai utilisée ici tentera d’extraire le jeton de l’en-tête Bearer et de laisser tomber tout le reste. S’il n’est pas trouvé, un message HTTP 400 Bad Request est renvoyé :

if (! preg_match('/Bearers(S+)/', $_SERVER['HTTP_AUTHORIZATION'], $matches)) {
    header('HTTP/1.0 400 Bad Request');
    echo 'Token non trouvé dans la requête';
    exit;
}

Notez que, par défaut, Apache ne passera pas le HTTP_AUTHORIZATION à PHP. La raison de ce problème est la suivante:

L’en-tête d’autorisation de base n’est sécurisé que si votre connexion est effectuée via HTTPS, car sinon les informations d’identification sont envoyées en texte clair codé (non crypté) sur le réseau, ce qui pose un énorme problème de sécurité.

Je comprends parfaitement la logique de cette décision. Cependant, pour éviter une grande confusion, ajoutez ce qui suit à votre configuration Apache. Le code fonctionnera alors comme prévu. Si vous utilisez NGINX, le code devrait fonctionner quand même :

RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

Ensuite, nous essayons d’extraire le JWT correspondant, qui se trouve dans le deuxième élément de l’élément $matches . S’il n’est pas disponible, aucun JWT n’a été extrait et un message HTTP 400 Bad Request est renvoyé :

$jwt = $matches[1];
if (! $jwt) {
    // Aucun jeton n'a pu être extrait de l'en-tête d'autorisation.
    header('HTTP/1.0 400 Bad Request');
    exit;
}

Si nous arrivons à ce point, un JWT a été envoyé, nous passons donc à l’étape de décodage et de validation. Pour ce faire, nous avons à nouveau besoin de notre clé secrète, qui sera extraite de l’environnement ou de la configuration de l’application. Nous utilisons ensuite la clé statique de php-jwt. decode en lui passant le JWT, la clé secrète et un tableau d’algorithmes à utiliser pour décoder le JWT.

S’il peut être décodé avec succès, nous essayons alors de le valider. L’exemple que j’ai ici est assez simpliste, car il n’utilise que l’émetteur, et non les horodatages avant et après expiration. Dans une application réelle, vous utiliseriez probablement un certain nombre d’autres revendications.

$secretKey  = 'bGS6lzFqvvSQ8ALbOxatm7/Vk7mLQyzqaS34Q4oR1ew=';
$token = JWT::decode($jwt, $secretKey, ['HS512']);
$now = new DateTimeImmutable();
$serverName = "your.domain.name";

if ($token->iss !== $serverName ||
    $token->nbf > $now->getTimestamp() ||
    $token->exp < $now->getTimestamp())
{
    header('HTTP/1.1 401 Unauthorized');
    exit;
}

Si le jeton n’est pas valide parce que, par exemple, le jeton a expiré, l’utilisateur recevra un en-tête HTTP 401 Unauthorized et le script se terminera.

Si le processus de décodage du JWT échoue, il se peut que :

  • Le nombre de segments fournis ne correspondait pas à la norme de trois, comme décrit précédemment.
  • L’en-tête ou la charge utile n’est pas une chaîne JSON valide.
  • La signature n’est pas valide, ce qui signifie que les données ont été falsifiées !
  • Le site nbf est définie dans le JWT avec un horodatage lorsque l’horodatage actuel est inférieur à celui-ci.
  • Le site iat est définie dans le JWT avec un horodatage lorsque l’horodatage actuel est inférieur à celui-ci.
  • Le site exp est définie dans le JWT avec un horodatage lorsque l’horodatage actuel est supérieur à celui-ci.

Comme vous pouvez le voir, le JWT dispose d’un bel ensemble de contrôles qui le marqueront comme invalide, sans qu’il soit nécessaire de le révoquer manuellement ou de le vérifier par rapport à une liste de jetons valides.

Si le processus de décodage et de validation réussit, l’utilisateur sera autorisé à faire la demande et recevra la réponse appropriée.

En conclusion

C’est une introduction rapide aux JSON Web Tokens, ou JWTs, et comment les utiliser dans des applications basées sur PHP. A partir de là, vous pouvez essayer d’implémenter les JWTs dans votre prochaine API, peut-être en essayant d’autres algorithmes de signature qui utilisent des clés asymétriques comme RS256, ou en les intégrant dans un serveur d’authentification OAUTH2 existant pour être la clé de l’API.

Nouveau Tutoriel

Newsletter

Ne manquez jamais les nouveaux conseils, tutoriels et autres.

Pas de spam, jamais. Nous ne partagerons jamais votre adresse électronique et vous pouvez vous désabonner à tout moment.