Table des matières
Créer votre propre authentification double-facteur avec TOTP
L'authentification d'un utilisateur se fait habituellement avec login et mot de passe. On peut améliorer la sécurité en ajoutant des facteurs. On en compte habituellement 3:
- Ce que je sais (Mon mot de passe).
- Ce que je possède (un object, une clé, un token RSA…).
- Ce que je suis (biométrie).
Si la biométrie nécessite souvent du matériel spécialisé (lecteur d'empreinte digitale, iris, rétinienne…), on peut facilement se créer l'équivalent d'un token d'authentification avec son smartphone (ce que je possède). Il existe des applications respectant un protocole standardisé pour cela: OTP (One Time Password). L'intérêt de l'OTP est que vous n'avez pas besoin de connexion entre votre client OTP (votre smartphone) et le serveur. Ils peuvent générer et contrôler la validité des OTP de manière déconnectée. Ce système peut donc fonctionner même si vous n'avez pas de réseau GSM à portée.
Ce protocole permet de créer un code qui ne sera utilisable qu'une seule fois. Nous allons juste ajouter un champ de saisie dans le formulaire de login. Vous allez voir, ce n'est vraiment pas compliqué.
Côté serveur, nous allons utiliser otphp.
Côté client, FreeOTP (disponible sous Android et iOS).
Quel type d'OTP ?
Il existe deux type d'OTP: HOTP et TOTP.
HOTP est basé sur un compteur. A chaque utilisation, le compteur est incrémenté côté client et côté serveur. L'inconvénient est que s'il y a un raté, il faut re-synchroniser les compteurs.
TOTP est basé sur la date et l'heure. A partir du moment où le client et le serveur sont synchronisés, on peut générer autant d'OTP qu'on le souhaite. Ces OTP ont une durée de validité limitée (qu'on peut paramétrer).
Nous allons implémenter TOTP.
Premier test
Installer et configurer la partie client (FreeOTP)
Installez FreeOTP sur votre smartphone (pour Android, pour iOS) et lancez-le
Tout en haut à droite, appuyez sur la petite clé avec le signe '+' pour ajouter un OTP. Il va vous proposer de scanner un QR-Code. Nous n'en avons pas: Choisissez "Manual entry".
Remplissez les champs:
- Issuer: toto (Mettez-y votre nom, votre email ou le nom de votre société.)
- ID: 1. (Vous pourrez mettre d'autres valeur si vous voulez avoir plusieurs OTP différents pour les différencier.)
- Secret: "DEKGGLZJ" (pour l'exemple). C'est le secret qui sera partagé par le client et le serveur. Personne ne doit le connaître ! Il doit être aléatoire, et il faut l'entrer en base32. La base32 n'est pas très courante, mais vous pouvez entrer des caractères aléatoires (A-Z 2-7): Dès que le bouton "Add" n'est plus grisé, c'est que votre valeur est valide base32. Notez cette valeur, vous devrez la recopier dans la partie serveur.
- Type: Time-based (TOTP)
- Algorithm: Laissez "SHA1"
- Interval: 60. C'est la durée de validité du code OTP en secondes (Nous y reviendrons).
- Digits: Laissez 6.
- Valider sur "Add".
Vous devez maintenant voir un rectangle blanc avec des tirets. Appuyez dessus: Un code à 6 chiffre s'affiche, avec un compte-à-rebours à droite: Il vous indique la durée de vie restante de ce code.
Maintenant installons la partie serveur.
Récupérer et installer la librairie côté serveur
Récupérez le zip de la librairie otphp sur GitHub et décompressez-le.
Récupérez seulement les répertoire lib
et vendor
et placez-les dans un sous-répertoire otphp
.
Vous devez donc obtenir:
index.php (votre application) \otphp \lib hotp.php otp.php otphp.php totp.php \vendor base32.php libs.php
Vérifier et synchroniser l'heure du serveur
Si votre serveur et votre téléphone ne sont pas synchrones au niveau de l'heure, les codes TOTP seront valides moins longtemps que prévu, voir même pas du tout. Affichez l'heure de votre serveur, et ajustez au besoin la valeur $decalage
afin que l'heure obtenue soit exactement celle de votre téléphone (idéalement à la seconde près).
<?php $decalage = 0; // (en secondes) date_default_timezone_set('Europe/Paris'); $maintenant = time() + $decalage; echo date('Y-m-d H:i:s',$maintenant); ?>
Tester la génération d'un TOTP
Maintenant que les heures sont synchro, ajoutons la génération de l'OTP et affichons-le:
<?php $decalage = 0; // (en secondes) date_default_timezone_set('Europe/Paris'); $maintenant = time() + $decalage; echo date('Y-m-d H:i:s',$maintenant); echo '<br>'; require_once dirname(__FILE__).'/otphp/lib/otphp.php'; $totp = new \OTPHP\TOTP("DEKGGLZJ",array('interval'=>60)); echo $totp->at($maintenant); ?>
Remarquez que nous avons remis les mêmes paramètres:
- Le secret commun (
DEKGGLZJ
) - L'interval à 60 secondes (
'interval'⇒60
) - Par défaut, l'algo est SHA1 et le code sur 6 chiffres (Donc inutile de le préciser).
Il devrait vous afficher quelque chose du genre:
2014-07-11 22:53:41 812059
Si l'heure affichée correspond bien à l'heure de votre smartphone, la seconde ligne devrait vous afficher un code identique à celui que vous affiche l'application FreeOTP. Cette valeur n'est pas valide plus de 60 secondes.
Si ce n'est pas le cas, c'est un manque de bol (vous avez généré un code juste à la limite de la période de temps), ou alors vous avez fait une erreur sur l'un de ces trois paramètres:
- Heure exacte entre serveur et smartphone.
- Secret commun
- Interval
En cas de doute, supprimez l'OTP dans l'application FreeOTP et re-crééez-en une nouvelle.
Sécuriser un formulaire de login
Maintenant que le codes OTP sont générés à l'identique entre serveur et smartphone, nous allons nous en servir pour sécuriser un formulaire de login existant.
Notre application
Nous allons créer une application fictive qui n'a qu'un seul écran (l'écran de login) et capable de faire une seule chose: Vous dire si votre login et mot de passe sont corrects.
<?php date_default_timezone_set('Europe/Paris'); ?> <form method="POST"> Login: <input type="text" name="login"><br> Mot de passe: <input type="password" name="password"><br> <input type="submit" value="Login"><br> </form> <hr> <?php // Fonction qui renvoie true si login et mot de passe sont corrects function checkLoginPassword($login,$password) { if ($login=='toto' && $password=='titi') return true; return false; } // Traitement du formulaire de login: if (!empty($_POST['login'])) { if ( checkLoginPassword($_POST['login'], $_POST['password'] ) ) echo "Login OK !"; else echo "Echec login"; } ?>
Vous noterez que notre application est particulièrement stupide: Login et mot de passe sont codés en dur dans le programme. Mais peu importe, c'est suffisant pour la démonstration. Si vous entrez bien toto/titi, elle vous répondre: Login OK !
.
Modification du formulaire de login
Nous allons maintenant ajouter l'OTP: Ajoutons d'abord un champ OTP au formulaire de login.
OTP: <input type="password" name="otp"><br>
Le formulaire devient:
<form method="POST"> Login: <input type="text" name="login"><br> Mot de passe: <input type="password" name="password"><br> OTP: <input type="password" name="otp"><br> <input type="submit" value="Login"><br> </form>
Modification du traitement du formulaire
Nous allons ajouter la fonction suivante:
// Vérifie la valeur OTP function checkOTP($otp) { $decalage = 0; // (en secondes) $maintenant = time() + $decalage; require_once dirname(__FILE__).'/otphp/lib/otphp.php'; $totp = new \OTPHP\TOTP("DEKGGLZJ",array('interval'=>60)); return $totp->verify($otp,$maintenant); }
Cette fonction renvoie true
si le code OTP est valide.
Ajoutons maintenant la vérification de l'OTP à celle du login/mot de passe:
if ( checkLoginPassword($_POST['login'], $_POST['password'] ) && checkOTP($_POST['otp']) )
Le code entier devient donc:
<?php date_default_timezone_set('Europe/Paris'); ?> <form method="POST"> Login: <input type="text" name="login"><br> Mot de passe: <input type="password" name="password"><br> OTP: <input type="password" name="otp"><br> <input type="submit" value="Login"><br> </form> <hr> <?php // Fonction qui renvoie true si login et mot de passe sont corrects function checkLoginPassword($login,$password) { if ($login=='toto' && $password=='titi') return true; return false; } // Vérifie la valeur OTP function checkOTP($otp) { $decalage = 0; // (en secondes) $maintenant = time() + $decalage; require_once dirname(__FILE__).'/otphp/lib/otphp.php'; $totp = new \OTPHP\TOTP("DEKGGLZJ",array('interval'=>60)); return $totp->verify($otp,$maintenant); } // Traitement du formulaire de login: if (!empty($_POST['login'])) { if ( checkLoginPassword($_POST['login'], $_POST['password'] ) && checkOTP($_POST['otp']) ) echo "Login OK !"; else echo "Echec login"; } ?>
Test
Voici donc notre nouveau formulaire:
Entrez login, mot de passe, puis tapez l'application FreeOTP pour qu'elle vous génère un code. Entrez ce code dans le champ 'OTP' et validez: Votre login devrait être accepté.
Vous pouvez refaire le test avec le bon login, le bon mot de passe mais un code OTP 6 chiffres au hasard: Votre login sera refusé même si le mot de passe est bon.
C'est tout l'intérêt d'OTP: Si une personne vous vole votre mot de passe, elle ne pourra pas l'utiliser pour se connecter car elle n'a pas le générateur OTP de votre smartphone. Elle n'a pu voler qu'un seul des deux facteurs (ce que je sais). Cela est utile si le mot de passe est volé sur le réseau, ou sur la machine (keylogger) ou si quelqu'un vous voit le taper.
Conseils
- Le code OTP ne doit jamais remplacer le mot de passe. Il ne peut que le compléter.
- Bien entendu, on ne laisse jamais les mots de passe et secrets OTP directement dans les sources. Stockez-les ailleurs !
- Je vous recommande de toujours afficher la date de l'heure sur votre formulaire de login. Vous pourrez ainsi immédiatement voir s'il y a un décalage avec votre téléphone.
- Il est facile de mettre la vérification OTP sous forme d'une librairie bien à part que vous pourrez appeler depuis différentes applications php. La double authentification sera alors facile à ajouter à la plupart des applications php existantes:
- Simple ajout d'un champ
OTP
dans le formulaire de login. - Un
require_once
pour inclure votre librairie. - Appel à votre fonction
checkOTP()
là où le logiciel vérifie login et mot de passe.
Exemple
DokuWiki
Voici en exemple comment sécuriser le formulaire de login de DokuWiki avec TOTP. La bonne manière de faire aurait été de créer un plugin d'authentification spécifique, mais j'ai choisi de taper directement dans le code (ce qui obligera à refaire la manip à la prochaine mise à jour, mais c'est assez rapide à faire: 6 lignes à modifier).
Dans inc/html.php
:
--- html.php.original Mon Jul 14 13:49:34 2014 +++ html.php Mon Jul 14 19:51:35 2014 @@ -47,6 +47,7 @@ $form->addHidden('do', 'login'); $form->addElement(form_makeTextField('u', ((!$INPUT->bool('http_credentials')) ? $INPUT->str('u') : ''), $lang['user'], 'focus__this', 'block')); $form->addElement(form_makePasswordField('p', $lang['pass'], '', 'block')); + $form->addElement(form_makePasswordField('otp', 'OTP', '', 'block')); if($conf['rememberme']) { $form->addElement(form_makeCheckboxField('r', '1', $lang['remember'], 'remember__me', 'simple')); }
Dans inc/auth.php
:
--- auth.php.original Mon Jul 14 13:49:33 2014 +++ auth.php Mon Jul 14 20:14:55 2014 @@ -110,6 +110,7 @@ $evdata = array( 'user' => $INPUT->str('u'), 'password' => $INPUT->str('p'), + 'otp' => $INPUT->str('otp'), 'sticky' => $INPUT->bool('r'), 'silent' => $INPUT->bool('http_credentials') ); @@ -179,6 +180,7 @@ return auth_login( $evdata['user'], $evdata['password'], + $evdata['otp'], $evdata['sticky'], $evdata['silent'] ); @@ -213,7 +215,7 @@ * @param bool $silent Don't show error on bad auth * @return bool true on successful auth */ -function auth_login($user, $pass, $sticky = false, $silent = false) { +function auth_login($user, $pass, $otp, $sticky = false, $silent = false) { global $USERINFO; global $conf; global $lang; @@ -228,7 +230,8 @@ if(!empty($user)) { //usual login - if($auth->checkPass($user, $pass)) { + require_once realpath(dirname(__FILE__).'/../../myotp/myotp.php'); // Lien vers votre librairie OTP + if($auth->checkPass($user, $pass) && checkOTP($otp)) { // make logininfo globally available $INPUT->server->set('REMOTE_USER', $user); $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
Codiad
Dans index.php
:
--- index.php.original Sun Jul 13 07:56:04 2014 +++ index.php Tue Jul 15 14:21:30 2014 @@ -125,6 +125,9 @@ <label><span class="icon-lock login-icon"></span> Password</label> <input type="password" name="password"> + <label><span class="icon-lock login-icon"></span>OTP</label> + <input type="password" name="otp"> + <div class="language-selector"> <label><span class="icon-picture login-icon"></span> Theme</label> <select name="theme" id="theme">
Dans components/user/class.user.php
:
--- class.user.php.original Sun Jul 13 07:56:04 2014 +++ class.user.php Tue Jul 15 14:25:43 2014 @@ -54,6 +54,12 @@ if($user['project']!=''){ $_SESSION['project'] = $user['project']; } } } + + if ($pass) + { + require_once realpath(dirname(__FILE__).'/../../../myotp/myotp.php'); // Lien vers votre librairie OTP + if (!checkOTP($_POST['otp'])) $pass = false; + } if($pass){ echo formatJSEND("success",array("username"=>$this->username)); } else{ echo formatJSEND("error","Incorrect Username or Password"); }