Dans ce tutoriel je vous propose de découvrir comment mettre en place un système d'authentification à 2 facteurs en PHP en utilisant le principe du TOTP, Time-Based One-Time Password Algorithm.
Le principe de base de cet algorithme est de générer un code unique à partir d'une clef secrète et de la date courante. L'utilisateur va d'abord se connecter sur le site une première fois avec son nom d'utilisateur et son mot de passe, il va alors générer une clef unique qui sera sauvegardée sur le serveur mais aussi localement. Lorsque l'utilisateur voudra se connecter, l'application locale lui permettra de générer un code unique, valable pendant une certaine durée, qui permettra de s'assurer de la validité de son authentification.
Comment sont générés ces codes ?
Il faut que votre site puisse vérifier les codes générés par votre application ou votre jeton. Ces codes ne sont pas aléatoires, Ils sont issus d’un algorithme décrit par les RFC 4426 et RFC 6238.
La dernière RFC est une évolution de la première et permet d’utiliser les hashages sha-2
(sha256
, sha512
par exemple) et n’utilise pas de compteur pour générer les codes, mais l’heure actuelle.
On ne va pas expliquer ici dans le détail cet algorithme, mais on notera qu’il faut les données suivantes :
- Une fonction de hashage (md5, sha-1, sha256…)
- Un secret encodé en base 32
- Le nombre de chiffres à générer (en général 6 ou 8)
- Un intervalle de temps entre chaque génération de code
Pour que ce type d'authentification fonctionne il est impératif que l'horloge du mobile et du serveur soit à peu près synchroniseé (le fuseau horaire n'a pas d'importance car on va calculer le nombre de périodes de temps écoulées, on ne se base pas sur l'heure). Pour éviter les problèmes le serveur peut accepter des codes qui correspondent aux timestamps ±1.
Afin de ne pas avoir à développer d'application spécifique on va se baser sur les règles compatibles avec google authenticator.
- Un intervalle de temps de 30 secondes
- Une méthode de hashage SHA-1
- Des codes à 6 chiffres
Heureusement quelqu'un à déja fait le travail pour nous et propose une librairie pour gérer la génération et la vérification de ces codes : christian-riesen/otp.
Modification de la base de données
Chaque utilisateur doit avoir le choix d’utiliser ou non ce système. Nous allons donc ajouter un champ totp_key
à notre table utilisateur pour enregistrer la clef secrète.
Si ce champ est null
, l’utilisateur se connectera comme auparavant avec un simple nom d’utilisateur et un mot de passe. Sinon, il devra en plus saisir le code généré par l’application.
ALTER TABLE `users` ADD COLUMN `totp_key` VARCHAR(255) NULL;
On permettra donc à nos utilisateurs de mettre à jour leur compte pour activer cette fonction en détectant si leur compte possède déjà une clef TOTP.
<?php if(!$user['totp_key']): ?>
<a href="______">Activer l'authentification à 2 facteurs</a>
<?php endif; ?>
Sur cette page on va générer une clef secrète que l'on transmettra à l'utilisateur à l'aide d'un QR code. On garde la clef secrète en mémoire et on va demander à l'utilisateur de rentrer un code afin de vérifier que le code soit bien enregistré dans son application.
<?php
$secret = GoogleAuthenticator::generateRandom();
$session->write('secret', $secret);
$qrcode = GoogleAuthenticator::getQrCodeUrl('totp', "Localdev", $secret);
?>
<p>
<img src="<?= $qrcode; ?>" alt=""/>
</p>
<form method="POST">
<input type="text" name="code" placeholder="Code de vérification">
<button type="submit">Envoyer</button>
</form>
Une fois le code soumis on va le valider, et si il correspond au code que l'on a dans la session on le sauvegardera la clef secrète dans la base de données.
<?php
$secret = $session->read("secret");
$otp = new Otp();
if ($otp->checkTotp(Base32::decode($secret), $_POST['code']){
// UPDATE users SET totp_key = $secret WHERE id = user_id
// et on met à jour la sesson avec cette nouvelle information
}
Modification du processus de connexion
Maintenant que certains de nos utilisateurs peuvent mettre en place le système d'authentification à 2 facteurs il faut modifier le processus de connexion lorsque cette fonction est activée.
<?php
// Lorsque l'on connecte notre utilisateur on regarde si on a une clef totp
$user = $this->Auth->identify();
if($user){
// Si l'utilisateur a une clef totp sur son compte on le redirige vers un nouveau formulaire
// en gardant en mémoire son $id utilisateur
if($user['totp_key']{
$this->request->session()->write('user_id', $user['id']);
return $this->redirect(['action' => 'loginTotp']);
} else {
// sinon on connecte l'utilisateur et on l'envoie sur son compte
$this->Auth->setUser($user);
return $this->redirect(['action' => 'account']);
}
}else{
$this->Flash->error('Mot de passe incorrect :(');
}
Si l'utilisateur a une clef TOTP sur son compte alors on lui demandera de rentrer un code afin de valider le processus d'authentification.
<form method="POST">
<input type="text" name="code" placeholder="Code de vérification">
<button type="submit">Envoyer</button>
</form>
Et on vérifiera, comme on l'a fait précedemment, si le code est valide par rapport à la clef secrète que l'on a dans son compte utilisateur.
$user = $this->Users->get($session->read('user_id'));
$otp = new Otp();
$secret = $user->totp_key;
if ($otp->checkTotp(Base32::decode($secret), $_POST['code']){
// la clef est valide, dans ce cas on connecte l'utilisateur
$this->Auth->setUser($user->toArray());
return $this->redirect(['action' => 'account']);
}else{
// sinon on lui demande de retenter sa chance
$this->Flash->error('Ce code ne correspond pas');
}
Et volla, vous savez maintenant mettre en place une authentification à 2 facteurs en utilisant une clef basée sur le temps.
Pour quelle utilisation ?
La mise en place d'une telle authentification ne conviendra évidemment pas à tous les sites car elle rajoute une étape supplémantaire à l'identification de vos utilisateurs. Ceci étant dis si vous gérez un site contenant des informations sensibles concernant vos utilisateurs, mettre en place cette sécurité peut être intéressant car l'actualité l'a démontré, les mots de passes sont une barrière qu'il est plutôt facile de franchir.