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:
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).
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.
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:
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é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
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); ?>
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:
DEKGGLZJ
)'interval'⇒60
)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:
En cas de doute, supprimez l'OTP dans l'application FreeOTP et re-crééez-en une nouvelle.
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.
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 !
.
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>
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"; } ?>
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.
OTP
dans le formulaire de login.require_once
pour inclure votre librairie.checkOTP()
là où le logiciel vérifie login et mot de passe.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
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"); }