diff --git a/ajax/getKey.php b/ajax/getKey.php index 3422a167b111cdfc60a49047da4204f6c9fca7a2..622ef80b13d85015abf68eadf224b0e824c8717c 100644 --- a/ajax/getKey.php +++ b/ajax/getKey.php @@ -39,8 +39,8 @@ function ($userId, $title) { $keyData['key'] = Security::decrypt($secrets[$title]['key']); $keyData['qrCode'] = $Google2FA->getQRCodeInline( - 'QUIQQER', - $AuthUser->getAttribute('email'), + $_SERVER['SERVER_NAME'], + $AuthUser->getUsername(), $keyData['key'] ); @@ -50,10 +50,10 @@ function ($userId, $title) { $keyData['recoveryKeys'] = array(); - foreach ($secrets[$title]['recoveryKeys'] as $recoveryCode) { - $keyData['recoveryKeys'][] = trim(Security::decrypt($recoveryCode)); + foreach ($secrets[$title]['recoveryKeys'] as $k => $recoveryKeyData) { + $recoveryKeyData['key'] = trim(Security::decrypt($recoveryKeyData['key'])); + $keyData['recoveryKeys'][] = $recoveryKeyData; } - } catch (QUI\Auth\Google2Fa\Exception $Exception) { QUI::getMessagesHandler()->addError( QUI::getLocale()->get( diff --git a/ajax/regenerateRecoveryKeys.php b/ajax/regenerateRecoveryKeys.php new file mode 100644 index 0000000000000000000000000000000000000000..71f619005eae8481b010eff45deb295ebb4700d2 --- /dev/null +++ b/ajax/regenerateRecoveryKeys.php @@ -0,0 +1,88 @@ +<?php + +use QUI; +use PragmaRX\Google2FA\Google2FA; +use QUI\Utils\Security\Orthos; +use QUI\Security; +use QUI\Auth\Google2Fa\Auth; + +/** + * Re-generate a set of recovery keys for a user authentication key + * + * @param string $title - key title + * @return bool - success + */ +QUI::$Ajax->registerFunction( + 'package_quiqqer_authgoogle2fa_ajax_regenerateRecoveryKeys', + function ($userId, $title) { + $AuthUser = QUI::getUsers()->get((int)$userId); + $title = Orthos::clear($title); + $EditUser = QUI::getUserBySession(); + + // @todo Check user edit permission of session user + + try { + $secrets = json_decode($AuthUser->getAttribute('quiqqer.auth.google2fa.secrets'), true); + + if (empty($secrets)) { + $secrets = array(); + } + + if (!isset($secrets[$title])) { + throw new QUI\Auth\Google2Fa\Exception(array( + 'quiqqer/authgoogle2fa', + 'exception.ajax.getKey.title.not.found', + array( + 'title' => $title, + 'user' => $AuthUser->getUsername(), + 'userId' => $AuthUser->getId() + ) + )); + } + + $secrets[$title]['recoveryKeys'] = Auth::generateRecoveryKeys(); + + $AuthUser->setAttribute( + 'quiqqer.auth.google2fa.secrets', + json_encode($secrets) + ); + + $AuthUser->save(); + } catch (QUI\Auth\Google2Fa\Exception $Exception) { + QUI::getMessagesHandler()->addError( + QUI::getLocale()->get( + 'quiqqer/authgoogle2fa', + 'message.ajax.regenerateRecoveryKeys.error', + array( + 'error' => $Exception->getMessage() + ) + ) + ); + + return false; + } catch (\Exception $Exception) { + QUI::getMessagesHandler()->addError( + QUI::getLocale()->get( + 'quiqqer/authgoogle2fa', + 'message.ajax.general.error' + ) + ); + + return false; + } + + QUI::getMessagesHandler()->addSuccess( + QUI::getLocale()->get( + 'quiqqer/authgoogle2fa', + 'message.ajax.regenerateRecoveryKeys.success', + array( + 'title' => $title + ) + ) + ); + + return true; + }, + array('userId', 'title'), + 'Permission::checkAdminUser' +); diff --git a/bin/controls/KeyData.html b/bin/controls/KeyData.html index 94bb5cddac9ad4c2e25f6ddf4cabe4a2f8bb0b7e..c80883dab1520411268b652f308c0e66f99d4ebb 100644 --- a/bin/controls/KeyData.html +++ b/bin/controls/KeyData.html @@ -1,12 +1,4 @@ <table class="quiqqer-auth-google2fa-register-showkey-data data-table data-table-flexbox"> - <thead> - <tr> - <th colspan="2"> - {{tableHeader}} - </th> - </tr> - </thead> - <tbody> <tr> <td> @@ -42,12 +34,13 @@ </tr> <tr> <td> - <label class="field-container"> + <div class="field-container"> <span class="field-container-item">{{labelRecoveryKeys}}</span> <div class="field-container-field quiqqer-auth-google2fa-register-showkey-recoverykeys"> <ul></ul> + <div class="quiqqer-auth-google2fa-register-showkey-recoverykeys-regenerate-btn"></div> </div> - </label> + </div> </td> </tr> </tbody> diff --git a/bin/controls/Settings.css b/bin/controls/Settings.css index bf7582d094828764d3153386a26fcfd9188a43c3..89ec216ed088f8a67024bcbc2600805e03d1449d 100644 --- a/bin/controls/Settings.css +++ b/bin/controls/Settings.css @@ -27,4 +27,12 @@ .quiqqer-auth-google2fa-register-generatekey input { width: 100%; +} + +.quiqqer-auth-google2fa-register-showkey-data .field-container-item { + width: 150px; +} + +.quiqqer-auth-google2fa-register-showkey-recoverykeys ul { + padding-left: 20px; } \ No newline at end of file diff --git a/bin/controls/Settings.js b/bin/controls/Settings.js index 647361c235cf070c0308766a951f80c76f530bc3..9e7d77cc225881d583b27d52b6e6647b71e64654 100644 --- a/bin/controls/Settings.js +++ b/bin/controls/Settings.js @@ -156,10 +156,6 @@ define('package/quiqqer/authgoogle2fa/bin/controls/Settings', [ * Event: onInject */ $onInject: function () { - - console.log(this.getAttribute('uid')); - console.log(this.getElm().get('data-qui-options-uid')); - this.resize(); this.refresh(); }, @@ -309,12 +305,65 @@ define('package/quiqqer/authgoogle2fa/bin/controls/Settings', [ var KeyData; var Row = this.$Grid.getDataByRow(row); + var FuncShowRegenerateWarning = function () { + var WarnPopup = new QUIConfirm({ + title : QUILocale.get(lg, 'controls.settings.showkey.regenerate.warning.title'), + maxHeight : 200, + maxWidth : 500, + icon : 'fa fa-repeat', + backgroundClosable: false, + + // buttons + buttons : true, // {bool} [optional] show the bottom button line + //closeButtonText : Locale.get('qui/controls/windows/Popup', 'btn.close'), + titleCloseButton: false, // {bool} show the title close button + content : false, + events : { + onOpen : function () { + Popup.Loader.show(); + + var Content = WarnPopup.getContent(); + + Content.set( + 'html', + QUILocale.get(lg, 'controls.settings.showkey.regenerate.warning') + ); + }, + onSubmit: function () { + QUIAjax.post( + 'package_quiqqer_authgoogle2fa_ajax_regenerateRecoveryKeys', + function (success) { + Popup.Loader.hide(); + WarnPopup.close(); + + if (!success) { + return; + } + + Popup.close(); + self.$showKey(row); + }, { + 'package': 'quiqqer/authgoogle2fa', + title : Row.title, + userId : self.getAttribute('uid') + } + ); + }, + onClose : function () { + Popup.Loader.hide(); + } + } + }); + + WarnPopup.open(); + }; + var Popup = new QUIConfirm({ - title : QUILocale.get( - lg, 'controls.settings.showkey.title' - ), - maxHeight : 720, - maxWidth : 650, + title : QUILocale.get(lg, 'controls.settings.showkey.template.tableHeader', { + title: Row.title + }), + maxHeight : 710, + maxWidth : 665, icon : 'fa fa-key', // {false|string} [optional] icon of the window backgroundClosable: true, // {bool} [optional] closes the window on click? standard = true @@ -334,9 +383,6 @@ define('package/quiqqer/authgoogle2fa/bin/controls/Settings', [ key : KeyData.key, createUser : KeyData.createUser, createDate : KeyData.createDate, - tableHeader : QUILocale.get(lg, lgPrefix + 'tableHeader', { - title: Row.title - }), labelQrCode : QUILocale.get(lg, lgPrefix + 'labelQrCode'), labelKey : QUILocale.get(lg, lgPrefix + 'labelKey'), labelCreateUser : QUILocale.get(lg, lgPrefix + 'labelCreateUser'), @@ -360,10 +406,35 @@ define('package/quiqqer/authgoogle2fa/bin/controls/Settings', [ ); for (var i = 0, len = KeyData.recoveryKeys.length; i < len; i++) { - new Element('li', { - html: KeyData.recoveryKeys[i] - }).inject(RecoveryListElm); + var RecoveryKeyData = KeyData.recoveryKeys[i]; + + var LiElm = new Element('li').inject(RecoveryListElm); + + var KeyTextElm = new Element('span', { + html: RecoveryKeyData.key + }).inject(LiElm); + + if (RecoveryKeyData.used) { + KeyTextElm.setStyle('text-decoration', 'line-through'); + + new Element('span', { + html: ' (' + RecoveryKeyData.usedDate + ')' + }).inject(LiElm); + } } + + // Re-generate recovery keys btn + new QUIButton({ + textimage: 'fa fa-repeat', + text : QUILocale.get(lg, 'controls.settings.showkey.regenerate.recoverykeys.btn'), + events : { + onClick: FuncShowRegenerateWarning + } + }).inject( + Content.getElement( + '.quiqqer-auth-google2fa-register-showkey-recoverykeys-regenerate-btn' + ) + ) } } }); diff --git a/locale.xml b/locale.xml index 2c6eda33be38998aa317a740380db1d14785e5fd..02a6f1e845608f731ec3ea393485d7e4be1bd6e5 100644 --- a/locale.xml +++ b/locale.xml @@ -24,10 +24,10 @@ <de><![CDATA[Beim Verarbeiten der Anfrage ist ein unerwarteter Fehler aufgetreten. Bitte wenden Sie sich an einen Administrator.]]></de> </locale> <locale name="message.ajax.generateKey.error" html="true"> - <de><![CDATA[Beim Erstellen des Authentifizierungsschlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de> + <de><![CDATA[Beim Erstellen des Authentifizierungs-Schlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de> </locale> <locale name="message.ajax.generateKey.success"> - <de><![CDATA[Der Authentifizierungsschlüssel "[title]" wurde erfolgreich erstellt.]]></de> + <de><![CDATA[Der Authentifizierungs-Schlüssel "[title]" wurde erfolgreich erstellt.]]></de> </locale> <locale name="message.ajax.getKey.error" html="true"> <de><![CDATA[Beim Abruf des Authentifizierungs-Schlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de> @@ -39,10 +39,16 @@ <de><![CDATA[Beim Abruf der Authentifizierungs-Schlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de> </locale> <locale name="message.ajax.deleteKeys.error" html="true"> - <de><![CDATA[Beim Löschen der Authentifizierungsschlüssels ist ein Fehler aufgetreten:<br/><br/>[error]]]></de> + <de><![CDATA[Beim Löschen der Authentifizierungs-Schlüssel ist ein Fehler aufgetreten:<br/><br/>[error]]]></de> </locale> <locale name="message.ajax.deleteKeys.success"> - <de><![CDATA[Die gewählten Authentifizierungsschlüssel wurden erfolgreich gelöscht.]]></de> + <de><![CDATA[Die gewählten Authentifizierungs-Schlüssel wurden erfolgreich gelöscht.]]></de> + </locale> + <locale name="message.ajax.regenerateRecoveryKeys.error" html="true"> + <de><![CDATA[Beim Neu-Generieren der Einmal-Login-Codes ist ein Fehler aufgetreten:<br/><br/>[error]]]></de> + </locale> + <locale name="message.ajax.regenerateRecoveryKeys.success"> + <de><![CDATA[Die Einmal-Login-Codes wurden neu generiert. Die bisherigen Codes sind ab jetzt nicht mehr gültig.]]></de> </locale> <!-- Class: Auth --> @@ -58,7 +64,7 @@ <!-- Login --> <locale name="login.code"> - <de><![CDATA[Authentifizierungs-Code]]></de> + <de><![CDATA[Google Authenticator Code]]></de> </locale> <locale name="login.btn.auth"> <de><![CDATA[Authentifizieren]]></de> @@ -70,7 +76,7 @@ <!-- Control: Settings --> <locale name="controls.settings.generatekey.title"> - <de><![CDATA[Neuer Authentifizierungsschlüssel]]></de> + <de><![CDATA[Neuer Authentifizierungs-Schlüssel]]></de> </locale> <locale name="controls.settings.generatekey.title.label"> <de><![CDATA[Bezeichnung]]></de> @@ -103,7 +109,7 @@ <de><![CDATA[Erstellungsdatum]]></de> </locale> <locale name="controls.settings.showkey.template.labelRecoveryKeys"> - <de><![CDATA[Einmal-Login-Schlüssel]]></de> + <de><![CDATA[Einmal-Login-Codes]]></de> </locale> <locale name="controls.settings.deleteKeys.title"> <de><![CDATA[Authentifizierungs-Schlüssel löschen]]></de> @@ -111,5 +117,15 @@ <locale name="controls.settings.deleteKeys.info" html="true"> <de><![CDATA[Sind Sie sicher, dass Sie die folgenden Authentifizierungs-Schlüssel unwiderruflich löschen wollen? Alle mit diesen Schlüsseln verknüpften Geräte können sich danach nicht mehr mit generierten Codes zu diesen Schlüsseln authentifizieren.<br/><br/>[titles]]]></de> </locale> + <locale name="controls.settings.showkey.regenerate.recoverykeys.btn"> + <de><![CDATA[Neu generieren]]></de> + </locale> + <locale name="controls.settings.showkey.regenerate.warning.title"> + <de><![CDATA[Einmal-Login-Codes neu generieren]]></de> + </locale> + <locale name="controls.settings.showkey.regenerate.warning"> + <de><![CDATA[Sind Sie sicher, dass Sie die Einmal-Login-Codes neu generieren wollen? Alle alten Codes sind danach nicht mehr gültig und es können nur die neuen Codes verwendet werden.]]></de> + </locale> + </groups> </locales> \ No newline at end of file diff --git a/src/QUI/Auth/Google2Fa/Auth.php b/src/QUI/Auth/Google2Fa/Auth.php index 87da03a7ab3e152cb8be7027adf70c8a484d7c38..5bfe30b90bc0c48320e18bab2c29488e4e233473 100644 --- a/src/QUI/Auth/Google2Fa/Auth.php +++ b/src/QUI/Auth/Google2Fa/Auth.php @@ -92,19 +92,26 @@ public function auth($authData) } // if key did not work check for recovery keys - foreach ($secretData['recoveryKeys'] as $k2 => $recoveryKey) { - $recoveryKey = trim(Security::decrypt($recoveryKey)); + foreach ($secretData['recoveryKeys'] as $k2 => $recoveryKeyData) { + if ($recoveryKeyData['used']) { + continue; + } + + $recoveryKey = trim(Security::decrypt($recoveryKeyData['key'])); if ($recoveryKey != $authCode) { continue; } - // remove recovery key from list indefinitely - unset($secretData['recoveryKeys'][$k2]); - $authSecrets[$k] = $secretData; + // set used status of recovery key to true + $recoveryKeyData['used'] = true; + $recoveryKeyData['usedDate'] = date('Y-m-d H:i:s'); + + $secretData['recoveryKeys'][$k2] = $recoveryKeyData; + $authSecrets[$k] = $secretData; $this->User->setAttribute('quiqqer.auth.google2fa.secrets', json_encode($authSecrets)); - $this->User->save(); + $this->User->save(QUI::getUsers()->getSystemUser()); return; } @@ -148,7 +155,11 @@ public static function generateRecoveryKeys($count = 10) $Google2FA = new Google2FA(); for ($i = 0; $i < $count; $i++) { - $recoveryKeys[] = Security::encrypt(mb_substr(md5($Google2FA->generateSecretKey(16)), 0, 10)); + $recoveryKeys[] = array( + 'key' => Security::encrypt(md5($Google2FA->generateSecretKey(16))), + 'used' => false, + 'usedDate' => false + ); } return $recoveryKeys; diff --git a/src/QUI/Auth/Google2Fa/Controls/Login.css b/src/QUI/Auth/Google2Fa/Controls/Login.css index 49f4d6d0444b92a1083cd53de3b4bf838d37a37d..441d6b6bb54cdb98697d914e9b4c71dc8899cad7 100644 --- a/src/QUI/Auth/Google2Fa/Controls/Login.css +++ b/src/QUI/Auth/Google2Fa/Controls/Login.css @@ -16,6 +16,11 @@ width: 100%; } +.quiqqer-auth-google2fa-login input { + display: block; + width: 100%; +} + .quiqqer-auth-google2fa-login [type="submit"] { cursor: pointer; } \ No newline at end of file diff --git a/src/QUI/Auth/Google2Fa/Controls/Login.php b/src/QUI/Auth/Google2Fa/Controls/Login.php index 6cd730cdb238a4b621cb4ae78fe685b065b01a25..f75e731565f3a60ba01bf84496f420f7243786b3 100644 --- a/src/QUI/Auth/Google2Fa/Controls/Login.php +++ b/src/QUI/Auth/Google2Fa/Controls/Login.php @@ -20,6 +20,8 @@ class Login extends Control */ public function getBody() { + $this->addCSSFile(dirname(__FILE__) . '/Login.css'); + $Engine = QUI::getTemplateManager()->getEngine(); return $Engine->fetch(dirname(__FILE__) . '/Login.html'); } diff --git a/src/QUI/Auth/Google2Fa/Exception.php b/src/QUI/Auth/Google2Fa/Exception.php index 30306d84c99fccdfa74137cf60289c892b2c0a08..188445db30e6c333e6319922affb393a3ec99f6e 100644 --- a/src/QUI/Auth/Google2Fa/Exception.php +++ b/src/QUI/Auth/Google2Fa/Exception.php @@ -2,7 +2,7 @@ namespace QUI\Auth\Google2Fa; -use QUI\Exception as QUIExcpetion; +use QUI\Users\Exception as QUIExcpetion; class Exception extends QUIExcpetion {