OTP : 2FA Authentication

Improve : tighten the security + cosmetic
set double authentication : create, send link, usable with Google Authenticator and FreeOTP

1. OTP : prevent direct access without going throught the 2FA mechanism
2. Go back to the url where you were disconnected
3. Cosmetic
This commit is contained in:
sparkyx 2025-08-03 16:47:38 +02:00
parent 4bb0a46e6b
commit 4cee79c1f2
31 changed files with 1867 additions and 270 deletions

View file

@ -1,5 +1,6 @@
<?php
use chillerlan\Authenticator\{Authenticator, AuthenticatorOptions};
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
/*
* This file is part of NOALYSS.
*
@ -48,7 +49,8 @@ class Noalyss_User
var $access_mode; //!< MOBILE or PC depending if when connecting $login contains @mobile
var $lang ; //!< user's language
var $theme ; //!< user's CSS Theme
var $authent_method; //!< authentication method use for this user
private $otp_secret; //!< string use as secret for OTP
/**
* @brief Create an user , load an existing one or if p_id == -1 search for the connected user. To have an empty
* user, give a p_id smaller than -1 or zero.
@ -377,7 +379,9 @@ class Noalyss_User
use_active,
use_admin,
use_pass,
use_email
use_email,
use_auth_method,
use_otp_secret
from ac_users ";
$cn=new Database();
$Res=$cn->exec_sql($sql.$sql_cond, $sql_array);
@ -393,18 +397,32 @@ class Noalyss_User
$this->admin=$row['use_admin'];
$this->password=$row['use_pass'];
$this->email=$row['use_email'];
$this->authent_method=$row['use_auth_method'];
$this->otp_secret=$row['use_otp_secret'];
return $this->id;
}
function save()
{
if ( $this->authent_method != 0 && $this->otp_secret == null) {
$this->generate_otp();
}
$Sql="update ac_users set use_first_name=$1, use_name=$2
,use_active=$3,use_admin=$4,use_pass=$5 ,use_email = $7 where use_id=$6";
,use_active=$3,use_admin=$4,use_pass=$5 ,use_email = $7
, use_auth_method=$8,use_otp_secret=$9
where use_id=$6";
$cn=new Database();
$Res=$cn->exec_sql($Sql,
array($this->first_name, $this->last_name, $this->active, $this->admin, $this->password,
$this->id, $this->email));
array($this->first_name //1
, $this->last_name // 2
, $this->active //3
, $this->admin //4
, $this->password //5
, $this->id //6
, $this->email //7
, $this->authent_method //8
, $this->otp_secret //9
));
}
function insert()
@ -1867,6 +1885,185 @@ class Noalyss_User
}
return $result;
}
/**
* @brief generate OTP
*/
function generate_otp()
{
$otp=new \Noalyss\OTP();
$this->otp_secret=$otp->build_secret();
}
public function get_authent_method() {
return $this->authent_method;
}
public function get_otp_secret() {
return $this->otp_secret;
}
public function set_authent_method($authent_method) {
$this->authent_method = $authent_method;
return $this;
}
public function set_otp_secret($otp_secret) {
$this->otp_secret = $otp_secret;
return $this;
}
public function set_identified()
{
$_SESSION[SESSION_KEY."db_auth"]='ok';
}
/**
* @brief check is the double authentication has been successful
* @return bool
*/
public function is_double_identified() {
if ( $this->authent_method == 0 )
{
$_SESSION[SESSION_KEY."db_auth"]='ok';
return true;
}
if ( ! isset($_SESSION[SESSION_KEY."db_auth"])) {
return false;
}
if ($_SESSION[SESSION_KEY."db_auth"] == "ok") {
return true;
}
return false;
}
/**
* @brief send a code to the user and save the expected code + UUID in
* a table OTP_SEND_SECRET
* @return bool false if fails true if succeeds
* @throws \Exception
*/
public function send_code_otp()
{
if ( $this->authent_method !=1 ) { return false; }
$mail=new \Sendmail();
$mail->set_from(ADMIN_WEB);
$mail->mailto($this->getEmail());
$mail->set_subject(_("NOALYSS : votre code secret "));
$noalyss_url=NOALYSS_URL;
if ( strlen(trim($this->otp_secret??"")) == 0 ) {
throw new \Exception("noalyss_user.send_code_otp:secret empty",1945);
}
$otp=new \Noalyss\OTP();
$code=$otp->compute_code($this->otp_secret);
$message="Bonjour,
Voici votre code secret pour NOALYSS : $code
";
try {
$uuid= guidv4();
$repository=new \Database();
// remove old for this user
$repository->exec_sql("delete from otp_send_secret where use_id=$1"
,[$this->id]);
// remove also old one
$repository->exec_sql("delete from otp_send_secret where os_valid_time < now()");
$now=new \DateTime();
$valid=new \DateTime();
$valid->modify('+20 minutes');
$otp_send_secret=new Otp_Send_Secret_SQL($repository);
$otp_send_secret->set("use_id",$this->id)
->set('os_request',$uuid)
->set("os_code",$code)
->set('os_valid_time',$valid->format('d.m.Y H:i:s'));
$otp_send_secret->save();
$mail->set_message($message);
$mail->compose();
$mail->send();
return $uuid;
} catch (Exception $ex) {
\record_log ($ex);
throw new \Exception("noalyss_user.send_code_otp",1963,$ex);
}
}
/**
* @brief send an email with link to the user
*/
function send_link_otp() {
$mail = new \Sendmail();
$mail->set_from(ADMIN_WEB);
$mail->mailto($this->getEmail());
$mail->set_subject(_("NOALYSS : Double authentification lien pour 2FA: OTP"));
$noalyss_url = NOALYSS_URL;
$uuid = guidv4();
$id = $this->getId();
/**
* save in DB first
*/
$message = "Bonjour,
Afin de pouvoir utiliser la double authentification avec 2FA: OTP, pourriez-vous
suivre ce lien et scanner le QRCode avec votre application android freeOTP ou Google Authenticator.
Ce lien ne sera actif que 12 heures.
{$noalyss_url}/index.php?otp={$uuid}
Merci d'utiliser NOALYSS
Bien cordialement,
";
try {
$repository = new \Database();
// remove old for this user
$repository->exec_sql("delete from otp_send_secret where use_id=$1"
,[$this->id]);
// remove also old one
$repository->exec_sql("delete from otp_send_secret where os_valid_time < now()");
$valid_time=new \DateTime();
$valid_time->add(new \DateInterval('PT12H'));
$otp_send_secret_sql = new \Otp_Send_Secret_SQL($repository);
$otp_send_secret_sql->set('use_id', $id)
->set('os_valid_time',$valid_time->format('d-m-Y H:i'))
->set('os_request', $uuid);
$otp_send_secret_sql->save();
$mail->set_message($message);
$mail->compose();
$mail->send();
return $uuid;
} catch (Exception $ex) {
\record_log($ex);
throw new \Exception("noalyss_user.send_link_otp",1998,$ex);
}
}
/**
* @brief FORM to enter the 6 digit enter by OTP
* @param $uuid (string UUID) UUID in the message, null if there is no message
*/
function input_otp($uuid="",$url="")
{
require_once NOALYSS_TEMPLATE."/noalyss_user-input_otp.php" ;
}
/**
* @brief check that the OTP code is the one on smartphone
* returns false if the given does not match the OTP
*/
function check_otp($code)
{
$otp=new \Noalyss\OTP();
if ( $otp->compute_code($this->otp_secret) == $code ) {
return true;
}
return false;
}
}
?>