Ré-identification par jeton

Présentation

Les applications web démarrent, en général, par une phase d'identification de l'utilisateur. Dans la plupart des cas, la session est maintenue ouverte entre le navigateur et le serveur en recourant à un identifiant de session, conservé à la fois dans le serveur et le navigateur (via un cookie). Tant que l'utilisateur n'a pas fermé son navigateur et qu'il continue à utiliser le logiciel, la session est maintenue, et il n'a pas besoin de s'identifier de nouveau.

Pour des applications qui doivent fonctionner en mode déconnecté, ou qui ne gèrent pas les identifiants de session, ou qui fonctionnent uniquement avec des requêtes de type Ajax, ce mécanisme ne peut pas être utilisé. Sans identifiant de session maintenu entre le client et le serveur, un autre système doit être mis en œuvre pour éviter à l'utilisateur de devoir s'identifier lors de chaque requête.

Dans ce cas, un jeton, contenant des informations d'identifications, est fourni au logiciel client. Ce jeton est alors transmis dans chaque requête.

La classe PHP Token (token.class.php), présentée ici, permet de générer un jeton et de le relire, en garantissant qu'il a bien été généré par le serveur.

Contraintes de sécurité

Ne pas stocker le mot de passe

L'identification est basée, dans la plupart des cas, sur un couple login/mot de passe. Si le login est une information non confidentielle (il est souvent issu soit d'une adresse mél, soit d'une concaténation du prénom et du nom), il n'en est pas de même pour le mot de passe, qui doit être protégé.

Une première approche serait de fournir, dans chaque requête, le login et le mot de passe, et de rejouer l'identification à chaque fois. Cela implique, pour l'application cliente, de stocker le mot de passe, ce qui n'est pas forcément très pertinent : il peut être compliqué d'en assurer la confidentialité à l'échelle d'un ordinateur. De plus, cela implique de rejouer, pour chaque requête, la phase d'identification, qui peut nécessiter l'interrogation de serveurs tiers (des annuaires) et donc allonger le temps de traitement. Enfin, cela limite l'identification à un mécanisme unique de gestion d'identification par login/mot de passe, alors que d'autres mécanismes pourraient être envisagés (carte à puce, identification en deux phases, etc.).

Le jeton utilisé doit donc contenir simplement une information permettant de retrouver le login de l'utilisateur.

Utiliser le chiffrement asymétrique

La seconde contrainte concerne la nécessité de s'assurer que le jeton fourni par le client est bien celui qui a été généré par le serveur d'identification. La manière la plus simple de s'en assurer consiste à s'appuyer sur le chiffrement asymétrique.

Le chiffrement asymétrique est basé sur l'utilisation de deux clés. Toute information chiffrée avec l'une ne peut être déchiffrée qu'avec l'autre.

Dans la pratique, le serveur conserve une clé, la clé privée. La seconde est diffusée aux clients qui en ont besoin, le cas échéant en l'encapsulant dans un certificat, qui permet de garantir son origine.

Tout message chiffré avec la clé privée peut être déchiffré avec la clé publique. Si le serveur chiffre le jeton avec sa clé privée, celui-ci ne pourra être déchiffré qu'avec sa clé publique, ce qui en garantit l'origine.

Ce mécanisme permet même de confier l'identification à un serveur tiers : il suffit que le serveur applicatif dispose de la clé publique du serveur d'identification pour pouvoir déchiffrer le jeton et récupérer le login.

Cette approche a également l'avantage de ne pas nécessiter un stockage du jeton dans le serveur d'identification (ou pourrait vouloir recourir à un jeton aléatoire associé à un login), ce qui simplifie le codage.

Enfin, les serveurs web applicatifs disposent déjà de certificats (clés privée et publique signée), nécessaires pour l'établissement des connexions en mode HTTPS.

Prévoir une durée maximale de vie pour le jeton

À partir du moment où le jeton contient le résultat de l'identification (le login), tout possesseur de celui-ci peut envoyer des requêtes à l'application. Si aucune durée de vie du jeton n'est prévue, cela revient à donner un accès permanent, même si l'utilisateur change de mot de passe.

Il est préférable d'intégrer une durée maximale de vie. Cette information doit également être chiffrée pour qu'elle ne puisse pas être modifiée, et intégrée dans le fichier Json qui contient le login. La classe Token intègre à cet effet une valeur Expire, exprimée en timestamp (nombre de secondes depuis le 1er janvier 1970), qui correspond à l'heure d'expiration du jeton.

La durée devra être adaptée au besoin. Pour un travail permanent avec des lecteurs portatifs (type smartphone ou tablette), on peut prévoir une durée correspondant à une journée de travail : l'utilisateur s'identifie le matin, et n'a plus besoin de le faire pour le reste de la journée.

Fonctionnement détaillé

La classe Token, fournie dans le fichier token.class.php va permettre de créer un jeton, de le chiffrer, puis de le déchiffrer pour obtenir le login.

Les données sont encodées dans le format JSON. Le fichier chiffré est, quant à lui, encodé en base 64 pour pouvoir être transféré en mode texte.

Schéma général

Fonctions utilisables

function __construct($privateKey = "", $pubKey = "")

Constructeur de la classe. Il est possible de lui fournir les noms des fichiers contenant les clés privées et publiques (par défaut : /etc/ssl/private/ssl-cert-snakeoil.key et /etc/ssl/certs/ssl-cert-snakeoil.pem).

function createToken($login, $validityDuration = -1)

Retourne le jeton généré, à partir du login fourni et de la durée de validité (en secondes). Si la durée de validité n'est pas fournie, le jeton aura une durée de vie de 24 heures.

function openToken($token)

Retourne le login à partir du jeton fourni, après l'avoir décodé et déchiffré, et vérifié sa date de validité.

function openTokenFromJson($jsonData)

Fonction utilitaire, qui appelle la fonction précédente en extrayant le jeton d'un fichier Json.

Test de fonctionnement

Ce script permet de vérifier le fonctionnement de la classe. Il s'auto-appelle, et enchaîne trois phases :

    • écran de saisie d'un login et d'un mot de passe
    • simulation de vérification du login, et génération d'un jeton
    • envoi du jeton et déchiffrement, pour récupérer le login
<?php
include_once 'token.class.php';
/**
* Pseudo-fonction pour demontrer la verification du login
* @param string $login
* @param string $password
* @return boolean
*/
function verifyLogin($login, $password) {
// script de verification du login
return true;
}
/*
* instanciation de la classe
*/
$tokenClass = new Token ();
if (! isset ( $_GET ["token"] ) && ! isset ( $_GET ["login"] )) {
/*
* 1ere etape
* Formulaire de saisie du login
*/
echo '<html><form method="get" action="createToken.php">';
echo 'login : <input name="login"><br>';
echo 'password : <input type="password" name="password"><br>';
echo '<input type="submit"></form></html>';
} else {
if (isset ( $_GET ["login"] )) {
/*
* 2nde etape
* generation du token et envoi au navigateur
*/
if (verifyLogin ( $_GET ["login"], $_GET ["password"] )) {
try {
/*
* Creation du token avec une duree de validite d'une heure
*/
$token = $tokenClass->createToken ( $_GET["login"], 3600 );
echo $token."<br>";
/*
* Preparation du formulaire de retour du test
*/
$contenu = json_decode ( $token, true );
echo '<html><form method="get" action="createToken.php">';
echo '<input name="token" value="' . $contenu ["token"] . '"><br>';
echo '<input type="submit"></form></html>';
} catch ( Exception $e ) {
echo $e->getMessage ();
}
}
} else {
/*
* 3eme etape
* Traitement du token pour lire le contenu
*/
try {
$login = $tokenClass->openToken ( $_GET ["token"] );
echo "login : " . $login;
} catch ( Exception $e ) {
echo $e->getMessage ();
}
}
}