11 files + 107 − 36 Inline Compare changes Side-by-side Inline Show whitespace changes Files 11 public_html/config/lang/de-de.php +2 −2 Original line number Diff line number Diff line Loading @@ -21,8 +21,8 @@ return [ "delete" => "Löschen", "edit" => "Bearbeiten", "help" => "Hilfe (en)", "no-data" => "No data available", "none" => "None", "no-data" => "Keine Daten vorhanden", "none" => "Keine", "reset" => "Zurücksetzen", "test" => "Testen", "view" => "Ansehen", Loading public_html/config/lang/en-en.php +2 −2 Original line number Diff line number Diff line Loading @@ -21,8 +21,8 @@ return [ "delete" => "Delete", "edit" => "Edit", "help" => "Help", "no-data" => "Keine Daten vorhanden", "none" => "Keine", "no-data" => "No data available", "none" => "None", "reset" => "Reset", "test" => "Test", "view" => "View", Loading public_html/example/testapp/classes/mfa-ensure.php +4 −11 Original line number Diff line number Diff line Loading @@ -3,22 +3,15 @@ * mfa-ensure.php * * @author Axel Hahn <axel.hahn@unibe> * @package IML-Appmonitor * */ if(!($_SERVER['REMOTE_USER']??false)){ return true; } $aConfig = @include "mfaconfig.php"; if(!($aConfig['api']??false)){ return true; } require_once __DIR__.'/mfaclient.class.php'; $mfa = new mfaclient($aConfig, ($_SERVER['REMOTE_USER']??'')); $mfa = new mfaclient(); $mfa->debug($aConfig['debug']??false); $iHttpStatus=$mfa->ensure(); // mfa was skipped? Enable this line to see the reason // echo $mfa->showStatus(); No newline at end of file public_html/example/testapp/classes/mfaclient.class.php +73 −14 Original line number Diff line number Diff line <?php /** * * MFA CLIENT CLASS * * Connect a web app with MFA server * * Source: https://git-repo.iml.unibe.ch/iml-open-source/ * Docs: https://os-docs.iml.unibe.ch/mfa-client/index.html * License: GNU GPL 3.0 * * 2025-06-11 <axel.hahn@unibe.ch> initial version * 2025-06-30 <axel.hahn@unibe.ch> set version 1.0.1 in user agenmt in http requests */ class mfaclient { protected string $_sVersion = "1.0.1"; protected array $aConfig = []; // protected string $sSessionvarname = "mfaclient"; Loading @@ -13,6 +28,8 @@ class mfaclient protected bool $bDebug = false; protected array $aStatus = []; /** * Intialize mfa client - optional set config and user * Loading @@ -20,17 +37,14 @@ class mfaclient * @see setUser * * @param array $aConfig optional: configuration with app id and base url * @param string $sUser optional: user id that was logged in */ public function __construct(array $aConfig = [], string $sUser = "") public function __construct(array $aConfig = []) { $this->loadConfig(); if ($aConfig) { $this->setConfig($aConfig); } if ($sUser) { $this->setUser($sUser); } $this->setUser($this->aConfig['user']??''); } Loading Loading @@ -77,7 +91,7 @@ class mfaclient // } curl_setopt($ch, CURLOPT_TIMEOUT, $iTimeout); curl_setopt($ch, CURLOPT_USERAGENT, 'IML MFA client' . __CLASS__); curl_setopt($ch, CURLOPT_USERAGENT, "IML MFA client PHP v$this->_sVersion"); curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); Loading Loading @@ -274,7 +288,6 @@ class mfaclient if (file_exists($sCfgfile)) { $aTmp = include $sCfgfile; $this->aConfig = $aTmp??[]; $this->setUser($aTmp['user']??''); } } /** Loading @@ -300,7 +313,8 @@ class mfaclient } /** * Logout * Logout; unset user in session scope * * @return void */ public function logout() Loading Loading @@ -362,11 +376,24 @@ class mfaclient session_start(); } if (($_SESSION['mfa']['user'] ?? '') == $this->sUser) { $this->aStatus[] = 'User still has a valid session after solving a challenge.'; return 200; } else { $this->logout(); } foreach(['api', 'appid', 'shared_secret', 'user'] as $sKey){ if(!isset($this->aConfig[$sKey])){ $this->aStatus[] = "Skip: Key '$sKey' was not set in config."; return 200; } if(!$this->aConfig[$sKey]){ $this->aStatus[] = "Skip: Key '$sKey' is empty in config."; return 200; } } $aMfaReturn = $this->check(); $this->_wd(__METHOD__ . "<br>Http request to mfa api<pre>" . print_r($aMfaReturn, 1) . "</pre>"); $aBody = json_decode($aMfaReturn['response']['body'] ?? '', 1); Loading @@ -393,6 +420,8 @@ class mfaclient ); } $this->aStatus[] = 'User solved the session now.'; $_SESSION['mfa']['user'] = $this->sUser; session_write_close(); Loading @@ -403,7 +432,8 @@ class mfaclient /** * Get an html button to open mfa setup page * * @param string $sSubmitBtn * @param string $sSubmitBtn optional: html code for a submit button; default: '<button>MFA Setup</button>' * @param string $sBackUrl optional: url to return from mfa server to the application; default: current url * @return void */ public function getButtonSetup(string $sSubmitBtn = '<button>MFA Setup</button>', $sBackUrl = ''): string Loading @@ -412,7 +442,7 @@ class mfaclient // print_r($aBody); $sUrl = $aBody['setup'] ?? ''; if ($sUrl) { $sBackUrl = $sBackUrl ?: $_SERVER['HTTP_REFERER']; $sBackUrl = $sBackUrl ?: ( "http".(($_SERVER['HTTPS']??'') === 'on' ? "s" : "")."://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); return $this->jumpform($sUrl, $sSubmitBtn, $sBackUrl); } else { return $aBody['message']??''; Loading @@ -423,10 +453,9 @@ class mfaclient /** * Open User settings to setup mfa methods * * @param string $sUrl * @param string $sSubmitBtn * @param string $sUrl url to open * @param string $sSubmitBtn html code for a submit button * @return void */ public function openSetup(string $sUrl = '', string $sSubmitBtn = '<button>MFA Setup</button>', $sBackUrl = '') { if (!$sUrl) { Loading @@ -438,6 +467,7 @@ class mfaclient $this->_jump($sUrl, $sSubmitBtn, $sBackUrl); } } */ /** * Get IP of current client (to be sent to MFA server) Loading @@ -464,6 +494,24 @@ class mfaclient return $ipaddress; } /** * return current config * @return array */ public function getConfig(): array { return $this->aConfig; } /** * return current status * @return array */ public function getStatus(): array { return $this->aStatus; } /** * get list of urls from MFA server * Loading @@ -474,5 +522,16 @@ class mfaclient return $this->_api("urls"); } /** * show current status if you want to find out why mfa was skipped * @example <code>echo $mfa->showStatus();</code> * @return string */ public function showStatus(): string { return 'MFA status: <ul><li>' . implode('</li><li>', $this->aStatus) .'</li></ul>' ; } } public_html/example/testapp/index.php +2 −1 Original line number Diff line number Diff line Loading @@ -87,7 +87,8 @@ switch ($sAction) { // $aConfig = include "classes/mfaconfig.php"; // $sOut.="Config:<pre>".print_r($aConfig,1)."</pre>"; $mfa = new mfaclient([], $sUser); $mfa = new mfaclient(); $mfa->setUser($sUser); // $aMfaReturn=$mfa->startMfa(); $aMfaReturn=$mfa->check(); Loading public_html/index.php +1 −1 Original line number Diff line number Diff line Loading @@ -4,7 +4,7 @@ $sTplPage = file_get_contents('config/page_tpl.html'); $aReplacements = [ '{{TITLE}}' => 'MFA Server', '{{VERSION}}' => 'v0.21', '{{VERSION}}' => 'v0.22', '{{APP}}' => '', '{{NAV}}' => '', '{{LANG}}' => '', Loading public_html/js/functions.js +18 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,24 @@ function getQueryParams() { return params; } /** * Hook on code input fields. * It submits a form after entering N chars or return key * * @param {object} oField object of input field (this) * @param {int} iSize optional: size of code; default: 6 */ function hookEnterCode(oField, iSize=6){ var sCode=oField.value; if(sCode.length==iSize){ oField.form.submit(); } if (window.keyCode == 13) { oField.form.submit(); } } /** * Apply search filter on a table * Loading public_html/pages/challenge-email.php +1 −1 Original line number Diff line number Diff line Loading @@ -99,7 +99,7 @@ $CONTENT.=" <form method=\"post\" class=\"pure-form\" action=\"$sPostUrl\"> <fieldset> <input type=\"hidden\" name=\"action\" value=\"verify\"> <input type=\"text\" class=\"big\" name=\"code\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <input type=\"text\" class=\"big\" name=\"code\" onkeyup=\"hookEnterCode(this,6);\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <br> <button class=\"pure-button button-primary\" >{{ico.send}} {{send}}</button> </fieldset> Loading public_html/pages/challenge-totp.php +1 −1 Original line number Diff line number Diff line Loading @@ -55,7 +55,7 @@ $CONTENT.=" <form method=\"post\" class=\"pure-form\" action=\"".$_SERVER['REQUEST_URI']."\"> <fieldset> <input type=\"hidden\" name=\"action\" value=\"verify\"> <input type=\"text\" class=\"big\" name=\"code\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <input type=\"text\" class=\"big\" name=\"code\" onkeyup=\"hookEnterCode(this,6);\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <br> <button class=\"pure-button button-primary\" >{{ico.send}} {{send}}</button> </fieldset> Loading public_html/pages/setup-email.php +1 −1 Original line number Diff line number Diff line Loading @@ -79,7 +79,7 @@ if(count($_POST)){ <form method=\"post\" class=\"pure-form\" action=\"".$_SERVER['REQUEST_URI']."\"> <input type=\"hidden\" name=\"action\" value=\"verify-code\"> <input type=\"text\" class=\"big\" name=\"code\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <input type=\"text\" class=\"big\" name=\"code\" onkeyup=\"hookEnterCode(this,6);\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <br> <button class=\"pure-button\" onclick=\"history.back();\">{{ico.back}} {{back}}</button> <button class=\"pure-button button-primary\" >{{ico.send}} {{send}}</button> Loading public_html/pages/setup-totp.php +2 −2 Original line number Diff line number Diff line Loading @@ -112,7 +112,7 @@ switch ($sAction) { // ----- dialog to setup a new TOTP secret case "create": $sInstance=$oApp->getLabel().' ('.$oApp->getUrl().')'; $sInstance=$oApp->getLabel().' ('.parse_url($oApp->getUrl(), PHP_URL_HOST).')'; $sUser=$oUser->getId(); // TODO: Issuer from config Loading Loading @@ -146,7 +146,7 @@ switch ($sAction) { <form method=\"post\" class=\"pure-form\" action=\"".$_SERVER['REQUEST_URI']."\"> <input type=\"hidden\" name=\"action\" value=\"verify\"> <input type=\"hidden\" name=\"secret\" value=\"$sSecret\"> <input type=\"text\" class=\"big\" name=\"code\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <input type=\"text\" class=\"big\" name=\"code\" onkeyup=\"hookEnterCode(this,6);\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <br> <button class=\"pure-button\" onclick=\"history.back();\">{{ico.back}} {{back}}</button> <button class=\"pure-button button-primary\" >{{ico.send}} {{send}}</button> Loading
public_html/config/lang/de-de.php +2 −2 Original line number Diff line number Diff line Loading @@ -21,8 +21,8 @@ return [ "delete" => "Löschen", "edit" => "Bearbeiten", "help" => "Hilfe (en)", "no-data" => "No data available", "none" => "None", "no-data" => "Keine Daten vorhanden", "none" => "Keine", "reset" => "Zurücksetzen", "test" => "Testen", "view" => "Ansehen", Loading
public_html/config/lang/en-en.php +2 −2 Original line number Diff line number Diff line Loading @@ -21,8 +21,8 @@ return [ "delete" => "Delete", "edit" => "Edit", "help" => "Help", "no-data" => "Keine Daten vorhanden", "none" => "Keine", "no-data" => "No data available", "none" => "None", "reset" => "Reset", "test" => "Test", "view" => "View", Loading
public_html/example/testapp/classes/mfa-ensure.php +4 −11 Original line number Diff line number Diff line Loading @@ -3,22 +3,15 @@ * mfa-ensure.php * * @author Axel Hahn <axel.hahn@unibe> * @package IML-Appmonitor * */ if(!($_SERVER['REMOTE_USER']??false)){ return true; } $aConfig = @include "mfaconfig.php"; if(!($aConfig['api']??false)){ return true; } require_once __DIR__.'/mfaclient.class.php'; $mfa = new mfaclient($aConfig, ($_SERVER['REMOTE_USER']??'')); $mfa = new mfaclient(); $mfa->debug($aConfig['debug']??false); $iHttpStatus=$mfa->ensure(); // mfa was skipped? Enable this line to see the reason // echo $mfa->showStatus(); No newline at end of file
public_html/example/testapp/classes/mfaclient.class.php +73 −14 Original line number Diff line number Diff line <?php /** * * MFA CLIENT CLASS * * Connect a web app with MFA server * * Source: https://git-repo.iml.unibe.ch/iml-open-source/ * Docs: https://os-docs.iml.unibe.ch/mfa-client/index.html * License: GNU GPL 3.0 * * 2025-06-11 <axel.hahn@unibe.ch> initial version * 2025-06-30 <axel.hahn@unibe.ch> set version 1.0.1 in user agenmt in http requests */ class mfaclient { protected string $_sVersion = "1.0.1"; protected array $aConfig = []; // protected string $sSessionvarname = "mfaclient"; Loading @@ -13,6 +28,8 @@ class mfaclient protected bool $bDebug = false; protected array $aStatus = []; /** * Intialize mfa client - optional set config and user * Loading @@ -20,17 +37,14 @@ class mfaclient * @see setUser * * @param array $aConfig optional: configuration with app id and base url * @param string $sUser optional: user id that was logged in */ public function __construct(array $aConfig = [], string $sUser = "") public function __construct(array $aConfig = []) { $this->loadConfig(); if ($aConfig) { $this->setConfig($aConfig); } if ($sUser) { $this->setUser($sUser); } $this->setUser($this->aConfig['user']??''); } Loading Loading @@ -77,7 +91,7 @@ class mfaclient // } curl_setopt($ch, CURLOPT_TIMEOUT, $iTimeout); curl_setopt($ch, CURLOPT_USERAGENT, 'IML MFA client' . __CLASS__); curl_setopt($ch, CURLOPT_USERAGENT, "IML MFA client PHP v$this->_sVersion"); curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); Loading Loading @@ -274,7 +288,6 @@ class mfaclient if (file_exists($sCfgfile)) { $aTmp = include $sCfgfile; $this->aConfig = $aTmp??[]; $this->setUser($aTmp['user']??''); } } /** Loading @@ -300,7 +313,8 @@ class mfaclient } /** * Logout * Logout; unset user in session scope * * @return void */ public function logout() Loading Loading @@ -362,11 +376,24 @@ class mfaclient session_start(); } if (($_SESSION['mfa']['user'] ?? '') == $this->sUser) { $this->aStatus[] = 'User still has a valid session after solving a challenge.'; return 200; } else { $this->logout(); } foreach(['api', 'appid', 'shared_secret', 'user'] as $sKey){ if(!isset($this->aConfig[$sKey])){ $this->aStatus[] = "Skip: Key '$sKey' was not set in config."; return 200; } if(!$this->aConfig[$sKey]){ $this->aStatus[] = "Skip: Key '$sKey' is empty in config."; return 200; } } $aMfaReturn = $this->check(); $this->_wd(__METHOD__ . "<br>Http request to mfa api<pre>" . print_r($aMfaReturn, 1) . "</pre>"); $aBody = json_decode($aMfaReturn['response']['body'] ?? '', 1); Loading @@ -393,6 +420,8 @@ class mfaclient ); } $this->aStatus[] = 'User solved the session now.'; $_SESSION['mfa']['user'] = $this->sUser; session_write_close(); Loading @@ -403,7 +432,8 @@ class mfaclient /** * Get an html button to open mfa setup page * * @param string $sSubmitBtn * @param string $sSubmitBtn optional: html code for a submit button; default: '<button>MFA Setup</button>' * @param string $sBackUrl optional: url to return from mfa server to the application; default: current url * @return void */ public function getButtonSetup(string $sSubmitBtn = '<button>MFA Setup</button>', $sBackUrl = ''): string Loading @@ -412,7 +442,7 @@ class mfaclient // print_r($aBody); $sUrl = $aBody['setup'] ?? ''; if ($sUrl) { $sBackUrl = $sBackUrl ?: $_SERVER['HTTP_REFERER']; $sBackUrl = $sBackUrl ?: ( "http".(($_SERVER['HTTPS']??'') === 'on' ? "s" : "")."://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); return $this->jumpform($sUrl, $sSubmitBtn, $sBackUrl); } else { return $aBody['message']??''; Loading @@ -423,10 +453,9 @@ class mfaclient /** * Open User settings to setup mfa methods * * @param string $sUrl * @param string $sSubmitBtn * @param string $sUrl url to open * @param string $sSubmitBtn html code for a submit button * @return void */ public function openSetup(string $sUrl = '', string $sSubmitBtn = '<button>MFA Setup</button>', $sBackUrl = '') { if (!$sUrl) { Loading @@ -438,6 +467,7 @@ class mfaclient $this->_jump($sUrl, $sSubmitBtn, $sBackUrl); } } */ /** * Get IP of current client (to be sent to MFA server) Loading @@ -464,6 +494,24 @@ class mfaclient return $ipaddress; } /** * return current config * @return array */ public function getConfig(): array { return $this->aConfig; } /** * return current status * @return array */ public function getStatus(): array { return $this->aStatus; } /** * get list of urls from MFA server * Loading @@ -474,5 +522,16 @@ class mfaclient return $this->_api("urls"); } /** * show current status if you want to find out why mfa was skipped * @example <code>echo $mfa->showStatus();</code> * @return string */ public function showStatus(): string { return 'MFA status: <ul><li>' . implode('</li><li>', $this->aStatus) .'</li></ul>' ; } }
public_html/example/testapp/index.php +2 −1 Original line number Diff line number Diff line Loading @@ -87,7 +87,8 @@ switch ($sAction) { // $aConfig = include "classes/mfaconfig.php"; // $sOut.="Config:<pre>".print_r($aConfig,1)."</pre>"; $mfa = new mfaclient([], $sUser); $mfa = new mfaclient(); $mfa->setUser($sUser); // $aMfaReturn=$mfa->startMfa(); $aMfaReturn=$mfa->check(); Loading
public_html/index.php +1 −1 Original line number Diff line number Diff line Loading @@ -4,7 +4,7 @@ $sTplPage = file_get_contents('config/page_tpl.html'); $aReplacements = [ '{{TITLE}}' => 'MFA Server', '{{VERSION}}' => 'v0.21', '{{VERSION}}' => 'v0.22', '{{APP}}' => '', '{{NAV}}' => '', '{{LANG}}' => '', Loading
public_html/js/functions.js +18 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,24 @@ function getQueryParams() { return params; } /** * Hook on code input fields. * It submits a form after entering N chars or return key * * @param {object} oField object of input field (this) * @param {int} iSize optional: size of code; default: 6 */ function hookEnterCode(oField, iSize=6){ var sCode=oField.value; if(sCode.length==iSize){ oField.form.submit(); } if (window.keyCode == 13) { oField.form.submit(); } } /** * Apply search filter on a table * Loading
public_html/pages/challenge-email.php +1 −1 Original line number Diff line number Diff line Loading @@ -99,7 +99,7 @@ $CONTENT.=" <form method=\"post\" class=\"pure-form\" action=\"$sPostUrl\"> <fieldset> <input type=\"hidden\" name=\"action\" value=\"verify\"> <input type=\"text\" class=\"big\" name=\"code\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <input type=\"text\" class=\"big\" name=\"code\" onkeyup=\"hookEnterCode(this,6);\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <br> <button class=\"pure-button button-primary\" >{{ico.send}} {{send}}</button> </fieldset> Loading
public_html/pages/challenge-totp.php +1 −1 Original line number Diff line number Diff line Loading @@ -55,7 +55,7 @@ $CONTENT.=" <form method=\"post\" class=\"pure-form\" action=\"".$_SERVER['REQUEST_URI']."\"> <fieldset> <input type=\"hidden\" name=\"action\" value=\"verify\"> <input type=\"text\" class=\"big\" name=\"code\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <input type=\"text\" class=\"big\" name=\"code\" onkeyup=\"hookEnterCode(this,6);\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <br> <button class=\"pure-button button-primary\" >{{ico.send}} {{send}}</button> </fieldset> Loading
public_html/pages/setup-email.php +1 −1 Original line number Diff line number Diff line Loading @@ -79,7 +79,7 @@ if(count($_POST)){ <form method=\"post\" class=\"pure-form\" action=\"".$_SERVER['REQUEST_URI']."\"> <input type=\"hidden\" name=\"action\" value=\"verify-code\"> <input type=\"text\" class=\"big\" name=\"code\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <input type=\"text\" class=\"big\" name=\"code\" onkeyup=\"hookEnterCode(this,6);\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <br> <button class=\"pure-button\" onclick=\"history.back();\">{{ico.back}} {{back}}</button> <button class=\"pure-button button-primary\" >{{ico.send}} {{send}}</button> Loading
public_html/pages/setup-totp.php +2 −2 Original line number Diff line number Diff line Loading @@ -112,7 +112,7 @@ switch ($sAction) { // ----- dialog to setup a new TOTP secret case "create": $sInstance=$oApp->getLabel().' ('.$oApp->getUrl().')'; $sInstance=$oApp->getLabel().' ('.parse_url($oApp->getUrl(), PHP_URL_HOST).')'; $sUser=$oUser->getId(); // TODO: Issuer from config Loading Loading @@ -146,7 +146,7 @@ switch ($sAction) { <form method=\"post\" class=\"pure-form\" action=\"".$_SERVER['REQUEST_URI']."\"> <input type=\"hidden\" name=\"action\" value=\"verify\"> <input type=\"hidden\" name=\"secret\" value=\"$sSecret\"> <input type=\"text\" class=\"big\" name=\"code\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <input type=\"text\" class=\"big\" name=\"code\" onkeyup=\"hookEnterCode(this,6);\" value=\"\" autofocus required size=\"8\" placeholder=\"123456\"><br> <br> <button class=\"pure-button\" onclick=\"history.back();\">{{ico.back}} {{back}}</button> <button class=\"pure-button button-primary\" >{{ico.send}} {{send}}</button> Loading