forked from extern/egroupware
new user security popup incl. 2FA and token revokation
This commit is contained in:
parent
e87655394d
commit
44a0079b9d
@ -1164,9 +1164,9 @@ abstract class Framework extends Framework\Extra
|
||||
$this->_add_topmenu_item(array(
|
||||
'id' => 'password',
|
||||
'name' => 'preferences',
|
||||
'title' => lang('Password'),
|
||||
'title' => lang('Security & Password'),
|
||||
'url' => "javascript:egw.open_link('".
|
||||
self::link('/index.php?menuaction=preferences.preferences_password.change')."','_blank','400x270')",
|
||||
self::link('/index.php?menuaction=preferences.preferences_password.change')."','_blank','850x580')",
|
||||
));
|
||||
}
|
||||
/* disable help until content is reworked
|
||||
|
@ -67,6 +67,9 @@ class Login
|
||||
$tmpl->set_block('login_form','change_password');
|
||||
$tmpl->set_var('change_password', '');
|
||||
$tmpl->set_var('lang_password',lang('password'));
|
||||
$tmpl->set_var('lang_2fa',lang('2-Factor-Authentication'));
|
||||
$tmpl->set_var('lang_2fa_help', htmlspecialchars(
|
||||
lang('If you use "2-Factor-Authentication", please enter the code here.')));
|
||||
|
||||
// display login-message depending on $_GET[cd] and what's in database/header for "login_message"
|
||||
$cd_msg = self::check_logoutcode($_GET['cd']);
|
||||
|
@ -49,6 +49,27 @@ class UserAgent
|
||||
return self::$ua_mobile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert user-agent string to OS and Browser
|
||||
*
|
||||
* @param string $user_agent =null
|
||||
*/
|
||||
public static function osBrowser($user_agent=null)
|
||||
{
|
||||
$matches = $os_matches = null;
|
||||
if (preg_match_all('#([^/]+)/([0-9.]+)( \([^)]+\))? ?#i', $user_agent, $matches) && count($matches) >= 4)
|
||||
{
|
||||
if (preg_match('/((Windows|Linux|Mac OS X)( NT)?) ([0-9._]+)/', $os=$matches[3][0], $os_matches))
|
||||
{
|
||||
$os = $os_matches[1].' '.str_replace('_', '.', $os_matches[4]);
|
||||
}
|
||||
$browser = $matches[1][2] === 'Version' ? $matches[1][3] : $matches[1][2];
|
||||
$browser_version = $matches[2][2];
|
||||
return "$os\n$browser $browser_version";
|
||||
}
|
||||
return $user_agent;
|
||||
}
|
||||
|
||||
/**
|
||||
* user-agent: 'firefox', 'msie', 'edge', 'safari' (incl. iPhone), 'chrome', 'opera', 'konqueror', 'mozilla'
|
||||
*
|
||||
|
@ -63,9 +63,13 @@ class Credentials
|
||||
*/
|
||||
const SMIME = 16;
|
||||
/**
|
||||
* All credentials IMAP|SMTP|ADMIN|SMIME
|
||||
* Two factor auth secret key
|
||||
*/
|
||||
const ALL = 27;
|
||||
const TWOFA = 32;
|
||||
/**
|
||||
* All credentials
|
||||
*/
|
||||
const ALL = self::IMAP|self::SMTP|self::ADMIN|self::SMIME|self::TWOFA;
|
||||
|
||||
/**
|
||||
* Password in cleartext
|
||||
@ -113,6 +117,7 @@ class Credentials
|
||||
self::SMTP => 'acc_smtp_',
|
||||
self::ADMIN => 'acc_imap_admin_',
|
||||
self::SMIME => 'acc_smime_',
|
||||
self::TWOFA => '2fa_',
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -50,20 +50,24 @@
|
||||
<td align="right">{lang_password}: </td>
|
||||
<td><input name="passwd" tabindex="5" value="{passwd}" type="password" size="30" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">{lang_2fa}: </td>
|
||||
<td><input name="2fa_code" tabindex="6" size="30" title="{lang_2fa_help}"/></td>
|
||||
</tr>
|
||||
<!-- BEGIN change_password -->
|
||||
<tr>
|
||||
<td align="right">{lang_new_password}: </td>
|
||||
<td><input name="new_passwd" tabindex="6" type="password" size="30" /></td>
|
||||
<td><input name="new_passwd" tabindex="7" type="password" size="30" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="right">{lang_repeat_password}: </td>
|
||||
<td><input name="new_passwd2" tabindex="7" type="password" size="30" /></td>
|
||||
<td><input name="new_passwd2" tabindex="8" type="password" size="30" /></td>
|
||||
</tr>
|
||||
<!-- END change_password -->
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td>
|
||||
<input tabindex="8" type="submit" value=" {lang_login} " name="submitit" />
|
||||
<input tabindex="9" type="submit" value=" {lang_login} " name="submitit" />
|
||||
</td>
|
||||
</tr>
|
||||
<!-- BEGIN registration -->
|
||||
|
@ -87,7 +87,8 @@
|
||||
"egroupware/activesync": "self.version",
|
||||
"egroupware/adodb-php": "self.version",
|
||||
"bower-asset/diff2html": "^2.7",
|
||||
"tinymce/tinymce": "^5.0"
|
||||
"tinymce/tinymce": "^5.0",
|
||||
"pragmarx/google2fa-qrcode": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
},
|
||||
|
422
composer.lock
generated
422
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ebdf3fe200cf9fe22536ceb0054a5fba",
|
||||
"content-hash": "61dbe936707f756396f1c438b9bfa02b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "adldap2/adldap2",
|
||||
@ -56,6 +56,55 @@
|
||||
],
|
||||
"time": "2016-07-14T18:11:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Bacon/BaconQrCode.git",
|
||||
"reference": "eaac909da3ccc32b748a65b127acd8918f58d9b0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/eaac909da3ccc32b748a65b127acd8918f58d9b0",
|
||||
"reference": "eaac909da3ccc32b748a65b127acd8918f58d9b0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"dasprid/enum": "^1.0",
|
||||
"ext-iconv": "*",
|
||||
"php": "^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phly/keep-a-changelog": "^1.4",
|
||||
"phpunit/phpunit": "^6.4",
|
||||
"squizlabs/php_codesniffer": "^3.1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-imagick": "to generate QR code images"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"BaconQrCode\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "http://www.dasprids.de",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "BaconQrCode is a QR code generator for PHP.",
|
||||
"homepage": "https://github.com/Bacon/BaconQrCode",
|
||||
"time": "2018-04-25T17:53:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bower-asset/cropper",
|
||||
"version": "v2.3.4",
|
||||
@ -456,6 +505,48 @@
|
||||
],
|
||||
"time": "2018-08-27T06:10:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "dasprid/enum",
|
||||
"version": "1.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/DASPRiD/Enum.git",
|
||||
"reference": "631ef6e638e9494b0310837fa531bedd908fc22b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/631ef6e638e9494b0310837fa531bedd908fc22b",
|
||||
"reference": "631ef6e638e9494b0310837fa531bedd908fc22b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^6.4",
|
||||
"squizlabs/php_codesniffer": "^3.1"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DASPRiD\\Enum\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ben Scholzen 'DASPRiD'",
|
||||
"email": "mail@dasprids.de",
|
||||
"homepage": "https://dasprids.de/"
|
||||
}
|
||||
],
|
||||
"description": "PHP 7.1 enum implementation",
|
||||
"keywords": [
|
||||
"enum",
|
||||
"map"
|
||||
],
|
||||
"time": "2017-10-25T22:45:27+00:00"
|
||||
},
|
||||
{
|
||||
"name": "egroupware/activesync",
|
||||
"version": "dev-master",
|
||||
@ -1015,6 +1106,113 @@
|
||||
"homepage": "http://www.oomphinc.com/",
|
||||
"time": "2017-03-31T16:57:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/constant_time_encoding",
|
||||
"version": "v2.2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paragonie/constant_time_encoding.git",
|
||||
"reference": "55af0dc01992b4d0da7f6372e2eac097bbbaffdb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/55af0dc01992b4d0da7f6372e2eac097bbbaffdb",
|
||||
"reference": "55af0dc01992b4d0da7f6372e2eac097bbbaffdb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^6|^7",
|
||||
"vimeo/psalm": "^1|^2"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"ParagonIE\\ConstantTime\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paragon Initiative Enterprises",
|
||||
"email": "security@paragonie.com",
|
||||
"homepage": "https://paragonie.com",
|
||||
"role": "Maintainer"
|
||||
},
|
||||
{
|
||||
"name": "Steve 'Sc00bz' Thomas",
|
||||
"email": "steve@tobtu.com",
|
||||
"homepage": "https://www.tobtu.com",
|
||||
"role": "Original Developer"
|
||||
}
|
||||
],
|
||||
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
|
||||
"keywords": [
|
||||
"base16",
|
||||
"base32",
|
||||
"base32_decode",
|
||||
"base32_encode",
|
||||
"base64",
|
||||
"base64_decode",
|
||||
"base64_encode",
|
||||
"bin2hex",
|
||||
"encoding",
|
||||
"hex",
|
||||
"hex2bin",
|
||||
"rfc4648"
|
||||
],
|
||||
"time": "2019-01-03T20:26:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "paragonie/random_compat",
|
||||
"version": "v9.99.99",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/paragonie/random_compat.git",
|
||||
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
|
||||
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.*|5.*",
|
||||
"vimeo/psalm": "^1"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
|
||||
},
|
||||
"type": "library",
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Paragon Initiative Enterprises",
|
||||
"email": "security@paragonie.com",
|
||||
"homepage": "https://paragonie.com"
|
||||
}
|
||||
],
|
||||
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
|
||||
"keywords": [
|
||||
"csprng",
|
||||
"polyfill",
|
||||
"pseudorandom",
|
||||
"random"
|
||||
],
|
||||
"time": "2018-07-02T15:55:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pear-pear.horde.org/Horde_Compress",
|
||||
"version": "2.2.2",
|
||||
@ -2358,6 +2556,228 @@
|
||||
"homepage": "http://pear.php.net/package/XML_Util",
|
||||
"time": "2017-06-28T19:21:19+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pragmarx/google2fa",
|
||||
"version": "v5.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/antonioribeiro/google2fa.git",
|
||||
"reference": "17c969c82f427dd916afe4be50bafc6299aef1b4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/17c969c82f427dd916afe4be50bafc6299aef1b4",
|
||||
"reference": "17c969c82f427dd916afe4be50bafc6299aef1b4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"paragonie/constant_time_encoding": "~1.0|~2.0",
|
||||
"paragonie/random_compat": ">=1",
|
||||
"php": ">=5.4",
|
||||
"symfony/polyfill-php56": "~1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4|~5|~6"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"component": "package",
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PragmaRX\\Google2FA\\": "src/",
|
||||
"PragmaRX\\Google2FA\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Antonio Carlos Ribeiro",
|
||||
"email": "acr@antoniocarlosribeiro.com",
|
||||
"role": "Creator & Designer"
|
||||
}
|
||||
],
|
||||
"description": "A One Time Password Authentication package, compatible with Google Authenticator.",
|
||||
"keywords": [
|
||||
"2fa",
|
||||
"Authentication",
|
||||
"Two Factor Authentication",
|
||||
"google2fa"
|
||||
],
|
||||
"time": "2019-03-19T22:44:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pragmarx/google2fa-qrcode",
|
||||
"version": "v1.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/antonioribeiro/google2fa-qrcode.git",
|
||||
"reference": "fd5ff0531a48b193a659309cc5fb882c14dbd03f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/antonioribeiro/google2fa-qrcode/zipball/fd5ff0531a48b193a659309cc5fb882c14dbd03f",
|
||||
"reference": "fd5ff0531a48b193a659309cc5fb882c14dbd03f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"bacon/bacon-qr-code": "~1.0|~2.0",
|
||||
"php": ">=5.4",
|
||||
"pragmarx/google2fa": ">=4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"khanamiryan/qrcode-detector-decoder": "^1.0",
|
||||
"phpunit/phpunit": "~4|~5|~6|~7"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"component": "package",
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PragmaRX\\Google2FAQRCode\\": "src/",
|
||||
"PragmaRX\\Google2FAQRCode\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Antonio Carlos Ribeiro",
|
||||
"email": "acr@antoniocarlosribeiro.com",
|
||||
"role": "Creator & Designer"
|
||||
}
|
||||
],
|
||||
"description": "QR Code package for Google2FA",
|
||||
"keywords": [
|
||||
"2fa",
|
||||
"Authentication",
|
||||
"Two Factor Authentication",
|
||||
"google2fa",
|
||||
"qr code",
|
||||
"qrcode"
|
||||
],
|
||||
"time": "2019-03-20T16:42:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php56",
|
||||
"version": "v1.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php56.git",
|
||||
"reference": "f4dddbc5c3471e1b700a147a20ae17cdb72dbe42"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/f4dddbc5c3471e1b700a147a20ae17cdb72dbe42",
|
||||
"reference": "f4dddbc5c3471e1b700a147a20ae17cdb72dbe42",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3",
|
||||
"symfony/polyfill-util": "~1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.11-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Php56\\": ""
|
||||
},
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"time": "2019-02-06T07:57:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-util",
|
||||
"version": "v1.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-util.git",
|
||||
"reference": "b46c6cae28a3106735323f00a0c38eccf2328897"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-util/zipball/b46c6cae28a3106735323f00a0c38eccf2328897",
|
||||
"reference": "b46c6cae28a3106735323f00a0c38eccf2328897",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.11-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Util\\": ""
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony utilities for portability of PHP codes",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compat",
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"shim"
|
||||
],
|
||||
"time": "2019-02-08T14:16:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tinymce/tinymce",
|
||||
"version": "5.0.3",
|
||||
|
@ -252,7 +252,7 @@ else
|
||||
}
|
||||
}
|
||||
$GLOBALS['sessionid'] = $GLOBALS['egw']->session->create($login, $passwd,
|
||||
$passwd_type, false, true, true); // true = let session fail on forced password change
|
||||
$passwd_type, false, true, true, $_POST['2fa_code']); // true = let session fail on forced password change
|
||||
|
||||
if (!$GLOBALS['sessionid'] && $GLOBALS['egw']->session->cd_reason == Api\Session::CD_FORCE_PASSWORD_CHANGE)
|
||||
{
|
||||
|
@ -40,6 +40,12 @@
|
||||
<input name="passwd" tabindex="5" value="{passwd}" type="password" size="30" placeholder="{lang_password}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="field_icons password"></span>
|
||||
<input name="2fa_code" tabindex="6" size="30" placeholder="{lang_2fa}" title="{lang_2fa_help}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- BEGIN remember_me_selection -->
|
||||
<tr>
|
||||
<td>
|
||||
@ -68,19 +74,19 @@
|
||||
<tr>
|
||||
<td>
|
||||
<span class="field_icons password"></span>
|
||||
<input name="new_passwd" tabindex="6" type="password" size="30" placeholder="{lang_new_password}" {autofocus_new_passwd}/>
|
||||
<input name="new_passwd" tabindex="7" type="password" size="30" placeholder="{lang_new_password}" {autofocus_new_passwd}/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="field_icons password"></span>
|
||||
<input name="new_passwd2" tabindex="7" type="password" placeholder="{lang_repeat_password}" size="30" />
|
||||
<input name="new_passwd2" tabindex="8" type="password" placeholder="{lang_repeat_password}" size="30" />
|
||||
</td>
|
||||
</tr>
|
||||
<!-- END change_password -->
|
||||
<tr>
|
||||
<td>
|
||||
<input tabindex="8" type="submit" value=" {lang_login} " name="submitit" />
|
||||
<input tabindex="9" type="submit" value=" {lang_login} " name="submitit" />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
@ -43,6 +43,12 @@
|
||||
<input name="passwd" tabindex="5" value="{passwd}" type="password" size="30" placeholder="{lang_password}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="field_icons password"></span>
|
||||
<input name="2fa_code" tabindex="6" size="30" placeholder="{lang_2fa}" title="{lang_2fa_help}"/>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- BEGIN remember_me_selection -->
|
||||
<tr>
|
||||
<td>
|
||||
|
@ -6,22 +6,27 @@
|
||||
* @link http://www.egroupware.org
|
||||
* @author Joseph Engo <jengo@phpgroupware.org>
|
||||
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
|
||||
* @version $Id$
|
||||
*/
|
||||
|
||||
use EGroupware\Api;
|
||||
use EGroupware\Api\Framework;
|
||||
use EGroupware\Api\Etemplate;
|
||||
use PragmaRX\Google2FAQRCode\Google2FA;
|
||||
use EGroupware\Api\Mail\Credentials;
|
||||
use EGroupware\OpenID\Repositories\AccessTokenRepository;
|
||||
use EGroupware\OpenID\Repositories\ScopeRepository;
|
||||
use EGroupware\OpenID\Repositories\RefreshTokenRepository;
|
||||
|
||||
class preferences_password
|
||||
{
|
||||
var $public_functions = array(
|
||||
'change' => True
|
||||
);
|
||||
const GAUTH_ANDROID = 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2';
|
||||
const GAUTH_IOS = 'https://appstore.com/googleauthenticator';
|
||||
|
||||
/**
|
||||
* Change password function
|
||||
* process change password form
|
||||
* Change password, two factor auth or revoke tokens
|
||||
*
|
||||
* @param type $content
|
||||
*/
|
||||
@ -29,34 +34,267 @@ class preferences_password
|
||||
{
|
||||
if ($GLOBALS['egw']->acl->check('nopasswordchange', 1))
|
||||
{
|
||||
Framework::window_close('There was no password change!');
|
||||
Framework::window_close('Password change is disabled!');
|
||||
}
|
||||
|
||||
if (!is_array($content))
|
||||
{
|
||||
$content= array();
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($content['button']['change'])
|
||||
{
|
||||
if (($errors = self::do_change($content['o_passwd_2'], $content['n_passwd'], $content['n_passwd_2'])))
|
||||
{
|
||||
Framework::message(implode("\n", $errors), 'error');
|
||||
$content = array();
|
||||
}
|
||||
else
|
||||
{
|
||||
Framework::refresh_opener(lang('Password changed'), 'preferences');
|
||||
Framework::window_close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$GLOBALS['egw_info']['flags']['app_header'] = lang('Change your password');
|
||||
$tmpl = new Etemplate('preferences.password');
|
||||
|
||||
$tmpl->exec('preferences.preferences_password.change', $content,array(),array(),array(),2);
|
||||
$readonlys = $sel_options = [];
|
||||
try {
|
||||
// PHP 7.1+: using SVG image backend (requiring XMLWriter) and not ImageMagic extension
|
||||
if (class_exists('BaconQrCode\Renderer\Image\SvgImageBackEnd'))
|
||||
{
|
||||
$image_backend = new \BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||
}
|
||||
$google2fa = new Google2FA($image_backend);
|
||||
$prefs = new Api\Preferences($GLOBALS['egw_info']['user']['account_id']);
|
||||
$prefs->read_repository();
|
||||
|
||||
if (!is_array($content))
|
||||
{
|
||||
$content = [];
|
||||
$content['2fa'] = $this->generateQRCode($google2fa)+[
|
||||
'gauth_android' => self::GAUTH_ANDROID,
|
||||
'gauth_ios' => self::GAUTH_IOS,
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
$secret_key = $content['2fa']['secret_key'];
|
||||
unset($content['2fa']['secret_key']);
|
||||
|
||||
switch($content['tabs'])
|
||||
{
|
||||
case 'change_password':
|
||||
if ($content['button']['save'])
|
||||
{
|
||||
if (($errors = self::do_change($content['password'], $content['n_passwd'], $content['n_passwd_2'])))
|
||||
{
|
||||
Framework::message(implode("\n", $errors), 'error');
|
||||
$content = array();
|
||||
}
|
||||
else
|
||||
{
|
||||
Framework::refresh_opener(lang('Password changed'), 'preferences');
|
||||
Framework::window_close();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'two_factor_auth':
|
||||
$auth = new Api\Auth();
|
||||
if (!$auth->authenticate($GLOBALS['egw_info']['user']['account_lid'], $content['password']))
|
||||
{
|
||||
$tmpl->set_validation_error('password', lang('Password is invalid'), '2fa');
|
||||
break;
|
||||
}
|
||||
switch(key($content['2fa']['action']))
|
||||
{
|
||||
case 'show':
|
||||
$content['2fa'] = $this->generateQRCode($google2fa, false);
|
||||
break;
|
||||
case 'reset':
|
||||
$content['2fa'] = $this->generateQRCode($google2fa, true);
|
||||
Framework::message(lang('New secret generated, you need to save it to disable the old one!'));
|
||||
break;
|
||||
case 'disable':
|
||||
if (Credentials::delete(0, $GLOBALS['egw_info']['user']['account_id'], Credentials::TWOFA))
|
||||
{
|
||||
Framework::refresh_opener(lang('Secret deleted, two factor authentication disabled.'), 'preferences');
|
||||
Framework::window_close();
|
||||
}
|
||||
else
|
||||
{
|
||||
Framework::message(lang('Failed to delete secret!'), 'error');
|
||||
}
|
||||
break;
|
||||
default: // no action, save secret
|
||||
if (!$google2fa->verifyKey($secret_key, $content['2fa']['code']))
|
||||
{
|
||||
$tmpl->set_validation_error('code', lang('Code is invalid'), '2fa');
|
||||
break 2;
|
||||
}
|
||||
if (($content['2fa']['cred_id'] = Credentials::write(0,
|
||||
$GLOBALS['egw_info']['user']['account_lid'],
|
||||
$secret_key, Credentials::TWOFA,
|
||||
$GLOBALS['egw_info']['user']['account_id'],
|
||||
$content['2fa']['cred_id'])))
|
||||
{
|
||||
Framework::refresh_opener(lang('Two Factor Auth enabled.'), 'preferences');
|
||||
Framework::window_close();
|
||||
}
|
||||
else
|
||||
{
|
||||
Framework::message(lang('Failed to store secret!'), 'error');
|
||||
}
|
||||
break;
|
||||
}
|
||||
unset($content['2fa']['action']);
|
||||
break;
|
||||
|
||||
case 'tokens':
|
||||
if (is_array($content) && $content['nm']['selected'])
|
||||
{
|
||||
try {
|
||||
switch($content['nm']['action'])
|
||||
{
|
||||
case 'delete':
|
||||
$token_repo = new AccessTokenRepository();
|
||||
$token_repo->revokeAccessToken(['access_token_id' => $content['nm']['selected']]);
|
||||
$refresh_token_repo = new RefreshTokenRepository();
|
||||
$refresh_token_repo->revokeRefreshToken(['access_token_id' => $content['nm']['selected']]);
|
||||
$msg = (count($content['nm']['selected']) > 1 ?
|
||||
count($content['nm']['selected']).' ' : '').
|
||||
lang('Access Token revoked.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch(\Exception $e) {
|
||||
$msg = lang('Error').': '.$e->getMessage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Framework::message($e->getMessage(), 'error');
|
||||
}
|
||||
|
||||
// display tokens, if we have openid installed (currently no run-rights needed!)
|
||||
if ($GLOBALS['egw_info']['apps']['openid'] && class_exists(AccessTokenRepository::class))
|
||||
{
|
||||
$content['nm'] = [
|
||||
'get_rows' => 'preferences.'.__CLASS__.'.getTokens',
|
||||
'no_cat' => true,
|
||||
'no_filter' => true,
|
||||
'no_filter2' => true,
|
||||
'filter_no_lang' => true,
|
||||
'order' => 'access_token_updated',
|
||||
'sort' => 'DESC',
|
||||
'row_id' => 'access_token_id',
|
||||
'default_cols' => '!client_id',
|
||||
'actions' => self::tokenActions(),
|
||||
];
|
||||
$sel_options += [
|
||||
'client_status' => ['Disabled', 'Active'],
|
||||
'access_token_revoked' => ['Active', 'Revoked'],
|
||||
'access_token_scopes' => (new ScopeRepository())->selOptions(),
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
$readonlys['tabs']['tokens'] = true;
|
||||
}
|
||||
|
||||
$tmpl->exec('preferences.preferences_password.change', $content, $sel_options, $readonlys, [
|
||||
'2fa' => $content['2fa']+[
|
||||
'secret_key' => $secret_key,
|
||||
],
|
||||
], 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query tokens for nextmatch widget
|
||||
*
|
||||
* @param array $query with keys 'start', 'search', 'order', 'sort', 'col_filter'
|
||||
* For other keys like 'filter', 'cat_id' you have to reimplement this method in a derived class.
|
||||
* @param array &$rows returned rows/competitions
|
||||
* @param array &$readonlys eg. to disable buttons based on acl, not use here, maybe in a derived class
|
||||
* @return int number of rows found
|
||||
*/
|
||||
public function getTokens(array $query, array &$rows, array &$readonlys)
|
||||
{
|
||||
if (!class_exists(AccessTokenRepository::class)) return;
|
||||
|
||||
$token_repo = new AccessTokenRepository();
|
||||
if (($ret = $token_repo->get_rows($query, $rows, $readonlys)))
|
||||
{
|
||||
foreach($rows as $key => &$row)
|
||||
{
|
||||
if (!is_int($key)) continue;
|
||||
|
||||
// boolean does NOT work as key for select-box
|
||||
$row['access_token_revoked'] = (string)(int)$row['access_token_revoked'];
|
||||
$row['client_status'] = (string)(int)$row['client_status'];
|
||||
|
||||
// dont send token itself to UI
|
||||
unset($row['access_token_identifier']);
|
||||
|
||||
// format user-agent as "OS Version\nBrowser Version" prefering auth-code over access-token
|
||||
// as for implicit grant auth-code contains real user-agent, access-token container the server
|
||||
if (!empty($row['auth_code_user_agent']))
|
||||
{
|
||||
$row['user_agent'] = Api\Header\UserAgent::osBrowser($row['auth_code_user_agent']);
|
||||
$row['user_ip'] = $row['auth_code_ip'];
|
||||
$row['user_agent_tooltip'] = Api\Header\UserAgent::osBrowser($row['access_token_user_agent']);
|
||||
$row['user_ip_tooltip'] = $row['access_token_ip'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$row['user_agent'] = Api\Header\UserAgent::osBrowser($row['access_token_user_agent']);
|
||||
$row['user_ip'] = $row['access_token_ip'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actions for tokens
|
||||
*/
|
||||
protected function tokenActions()
|
||||
{
|
||||
return [
|
||||
'delete' => array(
|
||||
'caption' => 'Revoke',
|
||||
'allowOnMultiple' => true,
|
||||
'confirm' => 'Revoke this token',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate QRCode and optional new secret
|
||||
*
|
||||
* @param Google2FA $google2fa
|
||||
* @param boolean|null $generate =null null: generate new qrCode/secret, if none exists
|
||||
* true: allways generate new qrCode (to reset existing one)
|
||||
* false: use existing secret, but generate qrCode
|
||||
* @return array with keys "qrc" and "cred_id"
|
||||
*/
|
||||
protected function generateQRCode(Google2FA $google2fa, $generate=null)
|
||||
{
|
||||
$creds = Credentials::read(0, Credentials::TWOFA, $GLOBALS['egw_info']['user']['account_id']);
|
||||
|
||||
if (!$generate && $creds && strlen($creds['2fa_password']) >= 16)
|
||||
{
|
||||
$secret_key = $creds['2fa_password'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$secret_key = $google2fa->generateSecretKey();//16, $GLOBALS['egw_info']['user']['account_lid']);
|
||||
}
|
||||
$qrc = '';
|
||||
if (isset($generate) || empty($creds))
|
||||
{
|
||||
$image = $google2fa->getQRCodeInline(
|
||||
!empty($GLOBALS['egw_info']['server']['site_title']) ?
|
||||
$GLOBALS['egw_info']['server']['site_title'] : 'EGroupware',
|
||||
$GLOBALS['egw_info']['user']['account_email'],
|
||||
$secret_key
|
||||
);
|
||||
$qrc = 'data:image/'.(substr($image, 0, 5) === '<?xml' ? 'svg+xml' : 'png').
|
||||
';base64,'.base64_encode($image);
|
||||
}
|
||||
return [
|
||||
'qrc' => $qrc,
|
||||
'hide_qrc' => empty($qrc),
|
||||
'cred_id' => !empty($creds) ? $creds['2fa_cred_id'] : null,
|
||||
'secret_key' => $secret_key,
|
||||
'status' => !empty($creds) ? lang('Two Factor Auth is already setup.') : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,4 +81,27 @@ tr.prefRow:hover .prefHelp {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
#preferences_settings_country_chzn {width:49% !important;}
|
||||
#preferences_settings_country_chzn {width:49% !important;}
|
||||
|
||||
/**
|
||||
* 2FA setup
|
||||
*/
|
||||
.securityHeader {
|
||||
margin-top: 1em;
|
||||
font-size: 120%;
|
||||
}
|
||||
img.qrCode {
|
||||
position: relative;
|
||||
left: -14px;
|
||||
}
|
||||
.toptApp {
|
||||
display: list-item !important;
|
||||
list-style-type: disc;
|
||||
list-style-position: inside;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.toptStatus {
|
||||
margin-top: 1em;
|
||||
font-style: italic;
|
||||
font-size: 120%;
|
||||
}
|
@ -2,41 +2,96 @@
|
||||
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd">
|
||||
<!-- $Id$ -->
|
||||
<overlay>
|
||||
<template id="preferences.password" template="" lang="" group="0" version="14.2">
|
||||
<grid resize_ratio="0.25" >
|
||||
<columns>
|
||||
<column width="35%"/>
|
||||
<column width="65%"/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row class="dialogHeader">
|
||||
<description value="Change password" class="et2_fullWidth"/>
|
||||
</row>
|
||||
<row>
|
||||
<description value="Enter your old password"/>
|
||||
<passwd id="o_passwd_2" class="et2_fullWidth" needed="true"/>
|
||||
</row>
|
||||
<row>
|
||||
<description value="Enter your new password"/>
|
||||
<passwd id="n_passwd" class="et2_fullWidth" needed="true"/>
|
||||
</row>
|
||||
<row>
|
||||
<description value="Re-enter your password"/>
|
||||
<passwd id="n_passwd_2" class="et2_fullWidth" needed="true"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
<!-- the empty resizable grid make sure that the toolbar stays always at the bottom after window gets resized -->
|
||||
<grid resize_ratio="0.75">
|
||||
<template id="preferences.password.change" template="" lang="" group="0" version="14.2">
|
||||
<grid width="100%">
|
||||
<columns>
|
||||
<column width="200"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row></row>
|
||||
<row>
|
||||
<description value="Enter your new password"/>
|
||||
<passwd id="n_passwd" class="et2_required"/>
|
||||
</row>
|
||||
<row>
|
||||
<description value="Re-enter your password"/>
|
||||
<passwd id="n_passwd_2" class="et2_required"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
</template>
|
||||
<template id="preferences.password.2fa" template="" lang="" group="0" version="19.1">
|
||||
<grid id="2fa" width="100%">
|
||||
<columns>
|
||||
<column width="200"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row disabled="!@hide_qrc" valign="top">
|
||||
<vbox>
|
||||
<button id="action[show]" label="Show QRCode" statustext="Show QRCode to enable on an additional device."/>
|
||||
<button id="action[reset]" label="Reset QRCode"
|
||||
statustext="Generate new QRCode to disable existing one, after enabling the new one!"/>
|
||||
<button id="action[disable]" label="Disable Two Factor Auth"
|
||||
onclick="et2_dialog.confirm(widget,'Are you sure?','Disable Two Factor Auth')"
|
||||
statustext="Disabling allows to again log in without a second factor."/>
|
||||
</vbox>
|
||||
<description id="status" class="toptStatus"/>
|
||||
</row>
|
||||
<row disabled="@hide_qrc" valign="top">
|
||||
<image src="qrc" class="qrCode"/>
|
||||
<vbox>
|
||||
<description value="Setup Two Factor Authentication" class="toptStatus" height="50px"/>
|
||||
<description/>
|
||||
<description value="Scan QRCode with a Time-based One-time Password (TOTP) App:"/>
|
||||
<description value="Google Authenticator for Android" statustext="click to install"
|
||||
extra_link_target="_blank" class="toptApp" href="$cont[gauth_android]"/>
|
||||
<description value="Google Authenticator for iOS" statustext="click to install"
|
||||
extra_link_target="_blank" class="toptApp" href="$cont[gauth_ios]"/>
|
||||
<description value="Or other compatible apps" class="toptApp"/>
|
||||
</vbox>
|
||||
</row>
|
||||
<row disabled="@hide_qrc">
|
||||
<textbox id="code" blur="XXX XXX" class="et2_required"/>
|
||||
<description value="Enter code to verify correct setup"/>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
</template>
|
||||
<template id="preferences.password" template="" lang="" group="0" version="19.1">
|
||||
<grid>
|
||||
<columns>
|
||||
<column width="215"/>
|
||||
<column/>
|
||||
</columns>
|
||||
<rows>
|
||||
<row>
|
||||
<description value="Security & Password" class="securityHeader"/>
|
||||
</row>
|
||||
<row>
|
||||
<description value="Current password" for="password"/>
|
||||
<hbox>
|
||||
<passwd id="password" needed="true" autocomplete="off"/>
|
||||
<description value=" "/>
|
||||
<description value="You need to enter your password to make any security changes!"/>
|
||||
</hbox>
|
||||
</row>
|
||||
</rows>
|
||||
</grid>
|
||||
<tabbox id="tabs" tab_height="400" width="100%">
|
||||
<tabs>
|
||||
<tab id="change_password" label="Change password"/>
|
||||
<tab id="two_factor_auth" label="Two factor auth"/>
|
||||
<tab id="tokens" label="Revoke Acccess Tokens"/>
|
||||
</tabs>
|
||||
<tabpanels>
|
||||
<template id="preferences.password.change"/>
|
||||
<template id="preferences.password.2fa"/>
|
||||
<template id="openid.access_tokens"/>
|
||||
</tabpanels>
|
||||
</tabbox>
|
||||
<hbox class="dialogFooterToolbar">
|
||||
<button label="Change" id="button[change]" image="check" background_image="true"/>
|
||||
<button label="Save" id="button[save]"/>
|
||||
<button label="Cancel" id="button[cancel]" onclick="window.close();"/>
|
||||
</hbox>
|
||||
</template>
|
||||
|
@ -103,6 +103,28 @@ textarea.prefValue {
|
||||
#preferences_settings_country_chzn {
|
||||
width: 49% !important;
|
||||
}
|
||||
/**
|
||||
* 2FA setup
|
||||
*/
|
||||
.securityHeader {
|
||||
margin-top: 1em;
|
||||
font-size: 120%;
|
||||
}
|
||||
img.qrCode {
|
||||
position: relative;
|
||||
left: -14px;
|
||||
}
|
||||
.toptApp {
|
||||
display: list-item !important;
|
||||
list-style-type: disc;
|
||||
list-style-position: inside;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.toptStatus {
|
||||
margin-top: 1em;
|
||||
font-style: italic;
|
||||
font-size: 120%;
|
||||
}
|
||||
/* #############################################################################
|
||||
// iframe
|
||||
// Rahmen + padding**/
|
||||
|
Loading…
Reference in New Issue
Block a user