OTP : Improve : tighten the security + cosmetic

set double authentication : create, send link, usable with
FreeOTP (opensource) AEgis (opensource) and Google Authenticator

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 20:21:39 +02:00
parent bf503299c3
commit 0aa9c4ed28
32 changed files with 1904 additions and 276 deletions

View file

@ -36,6 +36,12 @@ $cn=Dossier::connect();
$g_parameter=new Noalyss_Parameter_Folder($cn);
$g_user=new Noalyss_user($cn);
$g_user->check(true);
/**
* check if 2FA is completed
*/
if ( ! $g_user->is_double_identified()) {
exit();
}
IDate::set_firstDate($g_user->get_first_week_day());
ITva_Popup::set_vat_code($g_user->get_vat_code_preference());

View file

@ -96,7 +96,12 @@ else
$g_user = new Noalyss_user($cn);
$g_user->check(true);
}
/**
* check if 2FA is completed
*/
if ( ! $g_user->is_double_identified()) {
exit();
}
IDate::set_firstDate($g_user->get_first_week_day());
ITva_Popup::set_vat_code($g_user->get_vat_code_preference());

View file

@ -61,6 +61,12 @@ $q=noalyss_str_replace(",",".",$q);
$cn=Dossier::connect();
$User=new Noalyss_user($cn);
$User->Check();
/**
* check if 2FA is completed
*/
if ( ! $User->is_double_identified()) {
exit();
}
$User->check_dossier(Dossier::id());
// Retrieve the rate of vat, it $t == -1 it means no VAT

View file

@ -3690,8 +3690,39 @@ margin:1px;
*/
#info_passid {
position:absolute;
background-color: rgba(246, 180, 157, 0.58);
color:red;
background-color: rgba(246, 180, 157, 0.80);
color:#b90707;
font-size:100%;
z-index:10;
top:200px;
left:326px;
}
@media (min-width: 576px) {
#info_passid {
top:305px;
left:326px;
}
}
/* MeDium */
@media (min-width: 768px) {
#info_passid {
top:305px;
left:326px;
}
}
/* LarGe */
@media (min-width: 992px) {
#info_passid {
top:305px;
left:326px;
}
}
/* eXtraLarge */
@media (min-width: 1200px) {
#info_passid {
top:305px;
left:30%;
}
}
/** followup : detail : list of concerned actions **/

View file

@ -32,6 +32,12 @@ global $g_user;
$http=new \HttpInput();
$g_user=new Noalyss_user($cn);
$g_user->Check();
/**
* check if 2FA is completed
*/
if ( ! $g_user->is_double_identified()) {
exit();
}
$g_user->check_dossier($http->get('gDossier'));
$res=$cn->exec_sql("select distinct code,coalesce(description,code) description from get_profile_menu($1) where code ~* $2 or description ~* $2 order by code limit 5 ",array($g_user->get_profile(),$http->post("acs")));
$nb=Database::num_row($res);

View file

@ -96,6 +96,13 @@ if ( DEBUGNOALYSS > 1 ) {
$g_parameter=new Noalyss_Parameter_Folder($cn);
$g_user->Check();
if ( ! $g_user->is_double_identified()) {
echo "<h2>"._('Vous êtes déconnecté')."</h2>";
$backurl=$_SERVER['REQUEST_URI'];
$url="index.php?".http_build_query(array('reconnect'=>1,'backurl'=>urlencode($backurl)));
redirect($url);
exit();
}
$g_user->check_dossier(Dossier::id());
load_all_script();
/* Check Browser version if < IE6 then unsupported */

View file

@ -49,6 +49,12 @@ $gDossier=dossier::id();
$g_parameter=new Noalyss_Parameter_Folder($cn);
mb_internal_encoding("UTF-8");
$g_user->Check();
/**
* check if 2FA is completed
*/
if ( ! $g_user->is_double_identified()) {
exit();
}
$action=$g_user->check_dossier($gDossier);
$hi=new HttpInput();

View file

@ -43,6 +43,12 @@ $http=new HttpInput();
$cn=Dossier::connect();
$g_user=new Noalyss_user($cn);
$g_user->check();
/**
* check if 2FA is completed
*/
if ( ! $g_user->is_double_identified()) {
exit();
}
$only_plugin=$g_user->check_dossier(dossier::id());

View file

@ -49,6 +49,12 @@ $cn=Dossier::connect();
global $g_user;
$g_user=new Noalyss_User($cn);
$g_user->check();
/**
* check if 2FA is completed
*/
if ( ! $g_user->is_double_identified()) {
exit();
}
$g_user->check_dossier(dossier::id());
set_language();

View file

@ -66,6 +66,12 @@ require_once('class/noalyss_user.class.php');
global $g_user;
$g_user=new Noalyss_user($cn);
$g_user->check();
/**
* check if 2FA is completed
*/
if ( ! $g_user->is_double_identified()) {
exit();
}
$g_user->check_dossier(dossier::id());
set_language();

View file

@ -211,11 +211,21 @@ if (strlen(domaine)>0)
{
$my_domain=sprintf(_("Domaine")." : %s", domaine);
}
/**
* if request to recover password
*/
if (defined("RECOVER")&&isset($_REQUEST['recover']))
{
require_once '../include/recover.php';
}
/*
* if request to see OTP qrcode
*/
if ( isset($_GET['otp'])){
require_once '../include/otp-link.php';
return;
}
// reconnect , create a variable to reconnect properly in login.php
$goto="";
if (isset($_REQUEST['reconnect'])&&isset($_REQUEST['backurl']))

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -72,16 +72,41 @@ if ( isset ($_POST["p_user"] ) )
exit();
}
}
// if auth method = 1 send an email with number but only for PC access
if ($User->get_access_mode() =='PC' && $User->get_authent_method() == 1) {
// send an email and get the uuid of the request
$uuid=$User->send_code_otp ();
// var $backurl (string url) url before being disconnected
$backurl=(isset($_POST['backurl']))?$_POST['backurl']:"";
// display form to enter digit from email
$User->input_otp($uuid,$backurl);
// end
return;
}
// if auth method = 2 , only for PC access ask code from freeOTP
if ($User->get_access_mode() =='PC' && $User->get_authent_method() ==2 ) {
// var $backurl (string url) url before being disconnected
$backurl=(isset($_POST['backurl']))?$_POST['backurl']:"";
// display form to enter digit from email
$User->input_otp(url:$backurl);
// end
return;
}
if ($User->get_access_mode()=='PC')
{
// force the nocache
// retrieve the previous locationforce the nocache
$backurl=NOALYSS_URL.'/user_login.php?v='.microtime(true);
if ( isset ($_POST['backurl'])) {
$backurl=urldecode($_POST['backurl']);
// check that backurl is valid
$backurl=preg_replace('/^.*\?/','',$backurl);
$backurl=NOALYSS_URL."/do.php?$backurl";
}
if ( isset ($_POST['backurl'])) {
$backurl=urldecode($_POST['backurl']);
// check that backurl is valid
$backurl=preg_replace('/^.*\?/','',$backurl);
$backurl=NOALYSS_URL."/do.php?$backurl";
}
header("Location: $backurl");
exit();
} else {
@ -107,8 +132,83 @@ else
exit();
}
$User=new Noalyss_user($rep);
/**
* OTP is asked and authentication method is via OTP
*/
if (isset($_POST['to_validate']) || $User->get_authent_method() != 0) {
// remove also old one
$rep->exec_sql("delete from otp_send_secret where os_valid_time < now()");
try {
$request = $http->post("rq", "string", "");
$vrf_code = $http->post("vrf_code");
// if code was sent by email
if ($request != "") {
// find the row concerning this request
$os_id = $rep->get_value("select os_id from otp_send_secret
where os_request=$1
and use_id=$2
",
[$request, $User->id]);
if ($os_id == "") {
echo "Désolé, votre code a expiré";
echo "<META HTTP-EQUIV=\"REFRESH\" content=\"0;url=index.php?v=".microtime(true)."\">";
return;
}
$otp_send_secret = new Otp_Send_Secret_SQL($rep, $os_id);
if (
$vrf_code == $otp_send_secret->get('os_code')
|| $User->check_otp($vrf_code)
) {
$User->set_identified();
// var $backurl (string url) url before being disconnected
$backurl=NOALYSS_URL.'/user_login.php?v='.microtime(true);
if ( isset ($_POST['backurl'])) {
$backurl=urldecode($_POST['backurl']);
// check that backurl is valid
$backurl=preg_replace('/^.*\?/','',$backurl);
$backurl=NOALYSS_URL."/do.php?$backurl";
}
header("Location: $backurl");
return;
} else {
// var $backurl (string url) url before being disconnected
$backurl=(isset($_POST['backurl']))?$_POST['backurl']:"";
$User->input_otp(uuid:$request,url:$backurl);
return;
}
} else {
// connection avec freeOTP / Google Authenticator
if ($User->check_otp($vrf_code) == true) {
$User->set_identified();
// var $backurl (string url) url before being disconnected
$backurl=NOALYSS_URL.'/user_login.php?v='.microtime(true);
if ( isset ($_POST['backurl'])) {
// var $backurl (string url) url before being disconnected
$backurl=urldecode($_POST['backurl']);
// check that backurl is valid
$backurl=preg_replace('/^.*\?/','',$backurl);
$backurl=NOALYSS_URL."/do.php?$backurl";
}
header("Location: $backurl");
return;
} else {
// var $backurl (string url) url before being disconnected
$backurl=NOALYSS_URL.'/user_login.php?v='.microtime(true);
$User->input_otp(url:$backurl);
return;
}
}
} catch (Exception $exc) {
record_log($exc);
}
}
$User->Check();
echo "<META HTTP-EQUIV=\"REFRESH\" content=\"0;url=user_login.php?v=".microtime(true)."\">";

View file

@ -62,6 +62,12 @@ global $g_user;
$cn=Dossier::connect();
$g_user=new Noalyss_user($cn);
$g_user->Check();
/**
* check if 2FA is completed
*/
if ( ! $g_user->is_double_identified()) {
exit();
}
$g_user->check_dossier(Dossier::id());
if ( basename($_GET['op']) == 'history' )

View file

@ -53,6 +53,12 @@ include_once NOALYSS_INCLUDE . '/class/noalyss_user.class.php';
global $g_user;
$g_user=new Noalyss_user($cn);
$g_user->Check();
/**
* check if 2FA is completed
*/
if ( ! $g_user->is_double_identified()) {
exit();
}
$act=$g_user->check_dossier($gDossier);
// AC CODE = SEARCH
if ($act =='P')

View file

@ -37,6 +37,13 @@ require_once NOALYSS_INCLUDE . '/class/noalyss_user.class.php';
$User=new Noalyss_user($rep);
$User->Check();
/**
* check if 2FA is completed
*/
if ( ! $User->is_double_identified()) {
exit();
}
/* Check Browser version if < IE6 then unsupported */
$browser=$_SERVER['HTTP_USER_AGENT'];
if ( strpos($browser,'MSIE 6')!=false ||

View file

@ -35,7 +35,12 @@ $action = $http->request("action","string", "");
$rep=new Database();
$User=new Noalyss_user($rep);
$User->Check();
/**
* check if 2FA is completed
*/
if ( ! $User->is_double_identified()) {
exit();
}
$audit=true;
if ($User->admin != 1)

View file

@ -150,7 +150,8 @@ class Dossier
use_login,
use_active,
use_admin,
ag_dossier
ag_dossier,
use_auth_method
from
ac_users as ac
left join

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()
@ -1868,6 +1886,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;
}
}
?>

View file

@ -387,14 +387,13 @@ define ("VATCHECK_URL","https://ec.europa.eu/taxation_customs/vies/rest-api/");
function noalyss_class_autoloader($class)
{
$class = strtolower($class);
foreach (array("class","lib","database") as $path) {
if ( file_exists(NOALYSS_INCLUDE.'/'.$path.'/'.$class.'.class.php')) {
require_once NOALYSS_INCLUDE.'/'.$path.'/'.$class.'.class.php';
return;
}
}
$aClass = array(
"database" => "class/database.class.php",
"acc_detail" => "class/acc_operation.class.php",
@ -417,7 +416,8 @@ function noalyss_class_autoloader($class)
'noalyss\dbg'=>"lib/dbg.php",
'noalyss\file_cache'=>"lib/file_cache.class.php",
"pdfland"=>"class/pdf_land.class.php",
"noalyss\widget\widget"=>"widget/widget.php"
"noalyss\widget\widget"=>"widget/widget.php",
"noalyss\otp"=>"lib/otp.class.php"
);
if (isset ($aClass[$class])) {
require_once NOALYSS_INCLUDE . "/" . $aClass[$class];
@ -426,3 +426,5 @@ function noalyss_class_autoloader($class)
}
spl_autoload_register('\noalyss_class_autoloader', true);
require_once NOALYSS_BASE.'/vendor/autoload.php';

View file

@ -40,9 +40,11 @@ class Otp_Send_Secret_SQL extends \Table_Data_SQL {
*/
$this->name = array(
"os_id" => "os_id"
, "use_email" => "use_email"
, "os_timestamp" => "os_timestamp"
, "os_request" => "os_request"
, "use_id" => "use_id"
, 'os_valid_time'=>'os_valid_time'
,'os_code'=>'os_code'
);
/*
@ -50,12 +52,14 @@ class Otp_Send_Secret_SQL extends \Table_Data_SQL {
*/
$this->type = array(
"os_id" => "numeric"
, "use_email" => "text"
, "os_timestamp" => "date"
, "os_request" => "text"
, "use_id" => "numeric"
, 'os_valid_time'=>'date'
,'os_code'=>'text'
);
$this->default = array("os_id" => "auto");
$this->default = array("os_id" => "auto", "os_timestamp" => "auto");
$this->date_format = "DD.MM.YYYY HH24:MI:SS";
parent::__construct($p_cn, $p_id);

View file

@ -1502,9 +1502,9 @@ function generate_random_string($p_length,$special=1)
*/
function confirm_with_string($p_ctl_name,$p_car)
{
$code=generate_random_string($p_car );
$code=generate_random_string($p_car ,0);
$r = HtmlInput::hidden("ctlcode",$code);
$r.='<span style="margin-left:1.2em;margin-right:1.2em;font-size:112%;font-weight:bold;border:navy solid 1px ; padding:0.5rem">'. $code.'</span>';
$r.='<span style="margin-left:1.2em;margin-right:1.2em;font-size:120%;font-weight:bold;border:navy solid 1px ; padding:0.5rem">'. $code.'</span>';
$ctl=new IText($p_ctl_name);
$r.=$ctl->input();
return $r;
@ -1860,3 +1860,21 @@ function sanitize_filename($filename)
$new_filename=strtolower($filename_no)."-".date("Ymd-Hi").$filename_suff;
return $new_filename;
}
/**
* @brief generate an UUID
* @param $data(string) if null use randow
* @return string
*/
function guidv4($data = null) {
// Generate 16 bytes (128 bits) of random data or use the data passed into the function.
$data = $data ?? random_bytes(16);
// Set version to 0100
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
// Set bits 6-7 to 10
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
// Output the 36 character UUID.
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}

View file

@ -478,10 +478,11 @@ class HtmlInput
* @param type $div_name
* @return type
*/
static function button_hide($div_name)
static function button_hide($div_name,$class='smallbutton')
{
$a=new IButton('Fermer');
$a->label=_("Fermer");
$a->class=$class;
$a->javascript="$('".$div_name."').hide()";
$html=$a->input();

View file

@ -38,66 +38,36 @@ use chillerlan\Authenticator\{
use chillerlan\Authenticator\Authenticators\AuthenticatorInterface;
class OTP {
private $authenticator;
function __construct() {
$options = new AuthenticatorOptions;
$options->secret_length = 32;
$options->algorithm = AuthenticatorInterface::ALGO_SHA512;
$options->digits=6;
$this->authenticator = new Authenticator($options);
}
/**
* @brief build a secret key and returns it
* @return string random string of 32
*/
function build_secret() {
$options = new AuthenticatorOptions;
$options->secret_length = 32;
$options->algorithm = AuthenticatorInterface::ALGO_SHA512;
$options->digits=6;
$authenticator = new Authenticator($options);
// create a secret (stored somewhere in a *safe* place on the server. safe... hahaha jk)
$secret = $authenticator->createSecret();
$secret = $this->authenticator->createSecret();
return $secret;
}
/**
* @brief send an email with link to the user
* @param $user (\Noalyss_User )
* @brief compute a code for auth. for the user passed in parameter
* @param $user (\Noalyss_User)
*/
function send_mail(\Noalyss_User $user) {
$mail=new \Sendmail();
$mail->set_from(ADMIN_WEB);
$mail->mailto($user->getEmail());
$mail->set_subject(_("NOALYSS : Double authentification lien pour freeOTP"));
$noalyss_url=NOALYSS_URL;
$uuid=guidv4();
$id=$user->getId();
/**
* save in DB first
*/
$message="Bonjour,
Afin de pouvoir utiliser la double authentification avec freeOTP, pourriez-vous
suivre ce lien et scanner le QRCode avec votre application android freeOTP.
Ce lien ne sera actif que 24 heures.
{$noalyss_url}/index.php?otp={$uuid}
Merci d'utiliser NOALYSS
Cordialement,
Noalyss team
";
try {
$repository=new \Database();
$otp_send_secret_sql=new \Otp_Send_Secret_SQL($repository);
$otp_send_secret_sql->set('use_id',$id)
->set('os_request',$uuid);
$otp_send_secret_sql->save();
$mail->set_message($message);
$mail->compose();
$mail->send();
} catch (Exception $ex) {
}
function compute_code($secret)
{
$this->authenticator->setSecret($secret);
return $this->authenticator->code();
}
}

View file

@ -31,7 +31,97 @@ if (!file_exists($dirname . '/config.inc.php')) {
}
require_once __DIR__ . '/constant.php';
?>
<style>
/***
* LOGO
*************************************/
#logo_id {
position:static;
top : 0px;
left:0px;
width:60px;
height:auto;
margin-left:10px;
}
/** small **/
@media (min-width : 576px) {
#logo_id {
top : 10px;
left:20px;
width: 90px;
margin-left:20px;
}
}
/** medium**/
@media (min-width : 768px) {
#logo_id {
}
}
/** large**/
@media (min-width : 992px) {
#logo_id {
}
}
/** XL **/
@media (min-width : 1200px) {
#logo_id {
}
}
div.content {
}
@media (min-width: 576px) {
div.content {
margin-left: 16px;
}
}
/* MeDium */
@media (min-width: 768px) {
div.content {
margin-left: 32px;
}
}
/* LarGe */
@media (min-width: 992px) {
div.content {
margin-left: 64px;
}
}
/* eXtraLarge */
@media (min-width: 1200px) {
div.content {
margin-left: 128px;
}
}
#vrf_code {
font-size:200%;
width:9rem;
padding:0.5rem;
}
input[type=submit]{
font-size:200%;
width:9rem;
padding:0.5rem;
}
form {
width:70%;
margin-left:15%;
}
</style>
<img id="logo_id" src="image/logo9000.png" >
<div class="content">
<?php
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel;
@ -94,8 +184,20 @@ try {
$result = $writer->write($qrCode);
echo '<div style="margin:4rem">';
// generate the QRCode
echo '<h1>',_("Scanner ceci avec freeOTP"),'</h1>';
echo '<h1>',_("Scanner ceci avec votre application OTP"),'</h1>';
?>
<ol>
<li>
<a href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp" target="_blank">FreeOTP (libre)</a>
</li>
<li>
<a href="https://play.google.com/store/apps/details?id=com.beemdevelopment.aegis" target="_blank">AEgis Authenticator (libre)</a>
</li>
<li>
<a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Google Authenticator</a>
</li>
</ol>
<?php
echo '<p>';
echo _("Scanner ce QRCode avec votre application OTP afin de l'ajouter");
@ -104,13 +206,12 @@ try {
printf('<img src="data:image/png;base64,%s">', base64_encode($result->getString()));
echo '</div>';
} catch (Exception $exc) {
record_log($e);
return;
}
?>
</div>

View file

@ -0,0 +1,155 @@
<?php
/*
* This file is part of NOALYSS.
*
* NOALYSS is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* NOALYSS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NOALYSS; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
// Copyright Author Dany De Bontridder danydb@aevalys.eu 22/10/23
/**
* @file
* @brief input the 6 digit
*/
/**
* var $this (Noalyss_User)
*/
global $g_user;
$repository = new \Database();
// $uuid is set only when a message with the code
$str="";
if ( $uuid != "")
{
$request=new IHidden("rq", $uuid);
$str=$request->input();
}
$date=new \DateTime();
?>
<style>
/***
* LOGO
*************************************/
#logo_id {
position:static;
top : 0px;
left:0px;
width:60px;
height:auto;
margin-left:10px;
}
/** small **/
@media (min-width : 576px) {
#logo_id {
top : 10px;
left:20px;
width: 90px;
margin-left:20px;
}
}
/** medium**/
@media (min-width : 768px) {
#logo_id {
}
}
/** large**/
@media (min-width : 992px) {
#logo_id {
}
}
/** XL **/
@media (min-width : 1200px) {
#logo_id {
}
}
div.content {
}
@media (min-width: 576px) {
div.content {
margin-left: 16px;
}
}
/* MeDium */
@media (min-width: 768px) {
div.content {
margin-left: 32px;
}
}
/* LarGe */
@media (min-width: 992px) {
div.content {
margin-left: 64px;
}
}
/* eXtraLarge */
@media (min-width: 1200px) {
div.content {
margin-left: 128px;
}
}
#vrf_code {
font-size:200%;
width:9rem;
padding:0.5rem;
}
input[type=submit]{
font-size:200%;
width:9rem;
padding:0.5rem;
}
form {
width:70%;
margin-left:15%;
}
</style>
<img id="logo_id" src="image/logo9000.png" >
<div class="content">
<h1>Double authentification</h1>
<p>
Date et heure :
<?=$date->format('d/m/Y h:i')?>
</p>
<p>
Entrez le code que vous avez reçu par email ou sur votre application OTP.
</p>
<p>
Rafraichissez la page pour recevoir un nouveau code.
</p>
<FORM method="post" action="login.php" >
<input type="text" placeholder="999999" id="vrf_code" name="vrf_code" autocomplete="off">
<input type="hidden" name="auth" value="to_validate">
<?=$str?>
<input type="hidden" name="backurl" value="<?=$url?>">
<p>
<input type="submit" value="valider">
</p>
</FORM>
</div>

View file

@ -40,13 +40,14 @@ if ( isset ($_POST["ADD"]) )
$new_user=new Noalyss_user($cn,0);
$new_user->first_name=$http->post('FNAME');
$new_user->last_name=$http->post('LNAME');
$new_user->setAdmin(0);
$login=$http->post('LOGIN');
$login=str_replace("'","",$login);
$login=str_replace('"',"",$login);
$login=str_replace(" ","",$login);
$login=strtolower($login);
$new_user->login=$login;
//$new_user->auth_method=
$new_user->email=$http->post('EMAIL',"string",'');
if ( trim($login)=="" || strlen($login)<5)
{
@ -104,6 +105,7 @@ if ($sbaction == "save")
$UserChange->active = $http->post('Actif');
$UserChange->admin = $http->post('Admin');
$UserChange->email = $http->post('email');
$UserChange->authent_method = $http->post('auth_method',"number");
if ($UserChange->active ==-1 || $UserChange->admin ==-1)
{
die ('Missing data');
@ -118,15 +120,36 @@ if ($sbaction == "save")
$msg.="$result <br/>";
}
alert($msg);
require_once NOALYSS_INCLUDE.'/user_detail.inc.php';
return;
} else {
$UserChange->setPassword(md5($_POST['password']));
$UserChange->save();
}
}
else
{
$UserChange->save();
}
// if request new secret for OTP
if ( $http->post("generate_otp","number",0)==1){
$UserChange->generate_otp();
}
// save user change
$UserChange->save();
if ( $UserChange->authent_method !=0 && filter_var($UserChange->email, FILTER_VALIDATE_EMAIL) === false ) {
echo_warning(_("Email invalide"));
require_once NOALYSS_INCLUDE.'/user_detail.inc.php';
return;
}
// send a email with link if authen_method !=0 (freeOTP)
if ( $UserChange->authent_method !=0
&& filter_var($UserChange->email, FILTER_VALIDATE_EMAIL) == true
&& $http->post('email_otp','number',0) == 1
) {
// delete previous send for this user
$cn->exec_sql('delete from otp_send_secret where use_id=$1',[$UserChange->getId()]);
// send a email with a link to the qcode
$UserChange->send_link_otp();
}
}
}
@ -178,16 +201,16 @@ if ( isset($_REQUEST['det']) && $sbaction=="")
return;
}
?>
<div id="create_user" style="display:none;width:30%;margin-right: 20%" class="inner_box">
<span id="info_passid"></span>
<div id="create_user" style="display:none;margin-right: 20%;position:float;" class="inner_box2">
<?php echo HtmlInput::title_box(_('Ajout Utilisateur'),"create_user","hide");?>
<form action="admin-noalyss.php?action=user_mgt" method="POST" onsubmit="return check_form()">
<div style="text-align: center">
<span style="position:absolute;font-size:75%" id="info_passid"></span>
<TABLE class="result" >
<TR><TD style="text-align: right"> <?php echo _('login')?></TD><TD><INPUT id="input_login" class="input_text" TYPE="TEXT" NAME="LOGIN"></TD></tr>
<TR><TD style="text-align: right"> <?php echo _('Prénom')?></TD><TD><INPUT class="input_text" TYPE="TEXT" NAME="FNAME"></TD></tr>
<TR><TD style="text-align: right"> <?php echo _('Nom')?></TD><TD><INPUT class="input_text" TYPE="TEXT" NAME="LNAME"></TD></TR>
<TR><TD style="text-align: right"> <?php echo _('login')?></TD><TD><INPUT id="input_login" class="input_text" TYPE="TEXT" NAME="LOGIN" autocomplete=“off” required></TD></tr>
<TR><TD style="text-align: right"> <?php echo _('Prénom')?></TD><TD><INPUT class="input_text" TYPE="TEXT" NAME="FNAME" autocomplete=“off”></TD></tr>
<TR><TD style="text-align: right"> <?php echo _('Nom')?></TD><TD><INPUT class="input_text" TYPE="TEXT" NAME="LNAME" autocomplete=“off” required></TD></TR>
<TR>
<TD style="text-align: right"> <?php echo _('Mot de passe')?>
<?=\Icon_Action::tips("Mot de passe : longueur minimale = 8 dont au moins 1 majuscule, 1 minuscule,1 chiffre et 1 car.spécial")?>
@ -195,10 +218,16 @@ if ( isset($_REQUEST['det']) && $sbaction=="")
</TD>
<TD> <INPUT id="input_password" class="input_text" TYPE="TEXT" NAME="PASS"
onkeyup="check_password_strength('input_password','info_passid',true)"
autocomplete=“off”
required
>
</TD></TR>
<TR><TD style="text-align: right"> <?php echo _('Email')?></TD><TD> <INPUT class="input_text" TYPE="TEXT" NAME="EMAIL"></TD></TR>
<tr>
<td></td>
<td></td>
<tr>
</TABLE>
<?php
@ -230,7 +259,7 @@ echo HtmlInput::button_action(_("Fermer"), "$('create_user').style.display='none
</script>
</div>
<div>
<?php
echo '<p>';
echo HtmlInput::button_action(_("Ajout utilisateur"), "$('create_user').show();","cu");
@ -246,15 +275,15 @@ $repo=new Dossier(0);
$compteur=0;
$header=new Sort_Table();
$url=basename($_SERVER['PHP_SELF'])."?action=".$_REQUEST['action'];
$header->add(_("Login"), $url," order by use_login asc", "order by use_login desc","la", "ld");
$header->add(_("Nom"), $url," order by use_name asc,use_first_name asc", "order by use_name desc,use_first_name desc","na", "nd");
$header->add(_('Dossier'),$url,' order by ag_dossier asc','order by ag_dossier desc',
$header->add(_("Login"), $url," order by use_login asc", "order by use_login desc","la", "ld"); // 0
$header->add(_("Nom"), $url," order by use_name asc,use_first_name asc", "order by use_name desc,use_first_name desc","na", "nd"); // 1
$header->add(_("Authentification"), $url," order by use_auth_method asc,use_login asc", "order by use_auth_method desc ,use_login asc","auth", "nauth"); // 2
$header->add(_('Dossier'),$url,' order by ag_dossier asc','order by ag_dossier desc', //3
'da','dd');
$header->add(_("Actif"), $url," order by use_active asc", "order by use_active desc","aa", "ad");
$header->add(_("Email"), $url," order by use_email asc,use_name asc,use_first_name asc", "order by use_email desc,use_name desc,use_first_name desc","na", "nd");
$ord=(isset($_REQUEST['ord']))?$_REQUEST['ord']:'la';
$header->add(_("Actif"), $url," order by use_active asc", "order by use_active desc","aa", "ad"); // 4
$header->add(_("Email"), $url," order by use_email asc,use_name asc,use_first_name asc", "order by use_email desc,use_name desc,use_first_name desc","na", "nd"); //5
$ord=$http->request("ord","string","la");
$sql=$header->get_sql_order($ord);
$a_user=$repo->get_user_folder($sql);
if ( !empty ($a_user) )
@ -268,12 +297,14 @@ if ( !empty ($a_user) )
echo '<th>'.$header->get_header(0).'</th>';
echo '<th>'.$header->get_header(1).'</th>';
echo th(_("Prénom"));
echo '<th>'.$header->get_header(5).'</th>';
echo '<th>'.$header->get_header(2).'</th>';
echo '<th>'.$header->get_header(4).'</th>';
echo '<th>'.$header->get_header(3).'</th>';
echo "<th>"._('Type')."</th>";
echo '<th>'.$header->get_header(2).'</th>';
echo '</tr>';
$a_auth=[0=>_("Mot de passe"),1=>'Email et OTP',2=>'OTP'];
foreach ( $a_user as $r_user)
{
$compteur++;
@ -296,6 +327,8 @@ if ( !empty ($a_user) )
echo td($r_user['use_name']);
echo td($r_user['use_first_name']);
echo td($r_user['use_email']);
echo td($a_auth[$r_user['use_auth_method']]);
echo td($Active);
$type=($r_user['use_admin']==1)?_("Administrateur"):_("Utilisateur");
echo "<td>".$type."</td>";

View file

@ -117,6 +117,40 @@ $it_pass->value="";
?>
</td>
</tr>
<tr>
<td>
<?php echo _('Double authentification');?>
</td>
<td>
<?php
$auth_method=new ISelect('auth_method');
$auth_method->value=array(
array('value'=>0,'label'=>_('Aucune')),
array('value'=>1,'label'=>_('email + OTP')),
array('value'=>2,'label'=>_('OTP')),
);
$auth_method->selected=$UserChange->authent_method;
echo $auth_method->input();
?>
</td>
</tr>
<tr>
<td>
<?=_("Action Double Authentification")?>
</td>
<td>
<span style="display:block">
<input type="checkbox" name="generate_otp" value="1"><?=_("Renouveller clef secrète")?>
</span>
<span style="display:block">
<input type="checkbox" name="email_otp" value="1"><?=_("envoyer email pour 2FA avec OTP ")?>
<span class="text-muted"><?=_("Un email sera envoyé avec un lien pour initialiser 2FA avec OTP")?>>
</span>
</td>
</tr>
</table>
<input type="hidden" name="sbaction" id="sbaction" value="save">
@ -136,7 +170,7 @@ $it_pass->value="";
<input type="hidden" name="sbaction" value="delete">
<p class="info" id="codedel_div">
<?php
echo _("Pour effacer , confirmez en retapant le code");
echo _("Pour effacer , confirmez en retapant le code, respectez les majuscules et minuscules");
echo confirm_with_string('userdel','5');
?>
@ -146,7 +180,7 @@ $it_pass->value="";
<input type="Submit" class="button" NAME="DELETE" VALUE="<?=_("Confirmer")?>">
</li>
<li>
<?=HtmlInput::button_hide('delete_user_div')?>
<?=HtmlInput::button_hide('delete_user_div','button')?>
</li>
</ul>
</FORM>

View file

@ -1,12 +1,30 @@
ALTER TABLE public.ac_users ADD use_auth_method int2 DEFAULT 0 NULL;
COMMENT ON COLUMN public.ac_users.use_auth_method IS '0 = plain text, 1 = OTP , digit sent by email, 2=freeOTP';
ALTER TABLE public.ac_users ADD use_otp_secret text NULL;
COMMENT ON COLUMN public.ac_users.use_otp_secret IS 'string base32 generated for OTP';
ALTER TABLE public.ac_users ADD CONSTRAINT ac_users_unique UNIQUE (use_otp_secret);
update public.ac_users set use_auth_method=0;
ALTER TABLE public.ac_users ALTER COLUMN use_auth_method SET NOT NULL;
begin;
ALTER TABLE ac_dossier drop COLUMN dos_jnt_user ;
delete from jnt_use_dos where jnt_id in (select priv_jnt from priv_user where priv_priv='X');
delete from jnt_use_dos where use_id in (select use_id from ac_users where use_admin=1 or use_active=0);
ALTER TABLE ac_users ADD COLUMN use_email text;
COMMENT ON COLUMN ac_users.use_email IS 'Email of the user';
drop table priv_user;
select upgrade_repo(16);
alter table
rollback;
CREATE TABLE public.otp_send_secret (
os_id int8 GENERATED ALWAYS AS IDENTITY( INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1 NO CYCLE) NOT NULL, -- PK
os_timestamp timestamptz DEFAULT now() NOT NULL, -- Timestamp of the email sent
os_request text NULL, -- Unique identifier when request to scan qrcode
use_id int4 NOT NULL, -- FK to ac_users
os_code varchar(8) NULL,
os_valid_time timestamp NOT NULL,
CONSTRAINT otp_send_secret_pk PRIMARY KEY (os_id),
CONSTRAINT otp_send_secret_unique UNIQUE (use_id)
);
COMMENT ON TABLE public.otp_send_secret IS 'sent to user for scanning a QRCODE for FreeOTP
or digit to connect, depends of ac_users use_auth_method.';
-- Column comments
COMMENT ON COLUMN public.otp_send_secret.os_id IS 'PK';
COMMENT ON COLUMN public.otp_send_secret.os_timestamp IS 'Timestamp of the email sent';
COMMENT ON COLUMN public.otp_send_secret.os_request IS 'Unique identifier when request to scan qrcode';
COMMENT ON COLUMN public.otp_send_secret.os_code IS 'contains code sent by email';
COMMENT ON COLUMN public.otp_send_secret.use_id IS 'FK to ac_users';

View file

@ -23,6 +23,7 @@ class NoalyssUserTest extends TestCase
* @var User
*/
protected $object;
private $cn;
/**
* Sets up the fixture, for example, opens a network connection.
@ -239,4 +240,21 @@ class NoalyssUserTest extends TestCase
$_SESSION[SESSION_KEY.'use_admin']=1;
$user->admin=1;
}
/**
* @testdox Generate OTP : check key length and uniqueness
*/
function testGenerateOTP()
{
$this->object->generate_otp();
$secret=$this->object->get_otp_secret();
$this->assertTrue(strlen($secret) == 52,"Fails to generate OTP 32 char ".$secret." len = ".strlen($secret));
for ($i=0;$i<100;$i++){
$this->object->generate_otp();
$this->assertTrue($secret != $this->object->get_otp_secret(),"Generate twice the same secret");
}
}
}