new user security popup incl. 2FA and token revokation

This commit is contained in:
Ralf Becker 2019-06-05 13:10:25 +02:00
parent e87655394d
commit 44a0079b9d
14 changed files with 873 additions and 69 deletions

View File

@ -1164,9 +1164,9 @@ abstract class Framework extends Framework\Extra
$this->_add_topmenu_item(array( $this->_add_topmenu_item(array(
'id' => 'password', 'id' => 'password',
'name' => 'preferences', 'name' => 'preferences',
'title' => lang('Password'), 'title' => lang('Security & Password'),
'url' => "javascript:egw.open_link('". '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 /* disable help until content is reworked

View File

@ -67,6 +67,9 @@ class Login
$tmpl->set_block('login_form','change_password'); $tmpl->set_block('login_form','change_password');
$tmpl->set_var('change_password', ''); $tmpl->set_var('change_password', '');
$tmpl->set_var('lang_password',lang('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" // display login-message depending on $_GET[cd] and what's in database/header for "login_message"
$cd_msg = self::check_logoutcode($_GET['cd']); $cd_msg = self::check_logoutcode($_GET['cd']);

View File

@ -49,6 +49,27 @@ class UserAgent
return self::$ua_mobile; 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' * user-agent: 'firefox', 'msie', 'edge', 'safari' (incl. iPhone), 'chrome', 'opera', 'konqueror', 'mozilla'
* *

View File

@ -63,9 +63,13 @@ class Credentials
*/ */
const SMIME = 16; 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 * Password in cleartext
@ -113,6 +117,7 @@ class Credentials
self::SMTP => 'acc_smtp_', self::SMTP => 'acc_smtp_',
self::ADMIN => 'acc_imap_admin_', self::ADMIN => 'acc_imap_admin_',
self::SMIME => 'acc_smime_', self::SMIME => 'acc_smime_',
self::TWOFA => '2fa_',
); );
/** /**

View File

@ -50,20 +50,24 @@
<td align="right">{lang_password}:&nbsp;</td> <td align="right">{lang_password}:&nbsp;</td>
<td><input name="passwd" tabindex="5" value="{passwd}" type="password" size="30" /></td> <td><input name="passwd" tabindex="5" value="{passwd}" type="password" size="30" /></td>
</tr> </tr>
<tr>
<td align="right">{lang_2fa}:&nbsp;</td>
<td><input name="2fa_code" tabindex="6" size="30" title="{lang_2fa_help}"/></td>
</tr>
<!-- BEGIN change_password --> <!-- BEGIN change_password -->
<tr> <tr>
<td align="right">{lang_new_password}:&nbsp;</td> <td align="right">{lang_new_password}:&nbsp;</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>
<tr> <tr>
<td align="right">{lang_repeat_password}:&nbsp;</td> <td align="right">{lang_repeat_password}:&nbsp;</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> </tr>
<!-- END change_password --> <!-- END change_password -->
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td> <td>
<input tabindex="8" type="submit" value=" {lang_login} " name="submitit" /> <input tabindex="9" type="submit" value=" {lang_login} " name="submitit" />
</td> </td>
</tr> </tr>
<!-- BEGIN registration --> <!-- BEGIN registration -->

View File

@ -87,7 +87,8 @@
"egroupware/activesync": "self.version", "egroupware/activesync": "self.version",
"egroupware/adodb-php": "self.version", "egroupware/adodb-php": "self.version",
"bower-asset/diff2html": "^2.7", "bower-asset/diff2html": "^2.7",
"tinymce/tinymce": "^5.0" "tinymce/tinymce": "^5.0",
"pragmarx/google2fa-qrcode": "^1.0"
}, },
"require-dev": { "require-dev": {
}, },

422
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "ebdf3fe200cf9fe22536ceb0054a5fba", "content-hash": "61dbe936707f756396f1c438b9bfa02b",
"packages": [ "packages": [
{ {
"name": "adldap2/adldap2", "name": "adldap2/adldap2",
@ -56,6 +56,55 @@
], ],
"time": "2016-07-14T18:11:24+00:00" "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", "name": "bower-asset/cropper",
"version": "v2.3.4", "version": "v2.3.4",
@ -456,6 +505,48 @@
], ],
"time": "2018-08-27T06:10:37+00:00" "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", "name": "egroupware/activesync",
"version": "dev-master", "version": "dev-master",
@ -1015,6 +1106,113 @@
"homepage": "http://www.oomphinc.com/", "homepage": "http://www.oomphinc.com/",
"time": "2017-03-31T16:57:39+00:00" "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", "name": "pear-pear.horde.org/Horde_Compress",
"version": "2.2.2", "version": "2.2.2",
@ -2358,6 +2556,228 @@
"homepage": "http://pear.php.net/package/XML_Util", "homepage": "http://pear.php.net/package/XML_Util",
"time": "2017-06-28T19:21:19+00:00" "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", "name": "tinymce/tinymce",
"version": "5.0.3", "version": "5.0.3",

View File

@ -252,7 +252,7 @@ else
} }
} }
$GLOBALS['sessionid'] = $GLOBALS['egw']->session->create($login, $passwd, $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) if (!$GLOBALS['sessionid'] && $GLOBALS['egw']->session->cd_reason == Api\Session::CD_FORCE_PASSWORD_CHANGE)
{ {

View File

@ -40,6 +40,12 @@
<input name="passwd" tabindex="5" value="{passwd}" type="password" size="30" placeholder="{lang_password}"/> <input name="passwd" tabindex="5" value="{passwd}" type="password" size="30" placeholder="{lang_password}"/>
</td> </td>
</tr> </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 --> <!-- BEGIN remember_me_selection -->
<tr> <tr>
<td> <td>
@ -68,19 +74,19 @@
<tr> <tr>
<td> <td>
<span class="field_icons password"></span> <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> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<span class="field_icons password"></span> <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> </td>
</tr> </tr>
<!-- END change_password --> <!-- END change_password -->
<tr> <tr>
<td> <td>
<input tabindex="8" type="submit" value=" {lang_login} " name="submitit" /> <input tabindex="9" type="submit" value=" {lang_login} " name="submitit" />
</td> </td>
</tr> </tr>

View File

@ -43,6 +43,12 @@
<input name="passwd" tabindex="5" value="{passwd}" type="password" size="30" placeholder="{lang_password}"/> <input name="passwd" tabindex="5" value="{passwd}" type="password" size="30" placeholder="{lang_password}"/>
</td> </td>
</tr> </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 --> <!-- BEGIN remember_me_selection -->
<tr> <tr>
<td> <td>

View File

@ -6,22 +6,27 @@
* @link http://www.egroupware.org * @link http://www.egroupware.org
* @author Joseph Engo <jengo@phpgroupware.org> * @author Joseph Engo <jengo@phpgroupware.org>
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @version $Id$
*/ */
use EGroupware\Api; use EGroupware\Api;
use EGroupware\Api\Framework; use EGroupware\Api\Framework;
use EGroupware\Api\Etemplate; 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 class preferences_password
{ {
var $public_functions = array( var $public_functions = array(
'change' => True '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 * Change password, two factor auth or revoke tokens
* process change password form
* *
* @param type $content * @param type $content
*/ */
@ -29,34 +34,267 @@ class preferences_password
{ {
if ($GLOBALS['egw']->acl->check('nopasswordchange', 1)) 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'); $GLOBALS['egw_info']['flags']['app_header'] = lang('Change your password');
$tmpl = new Etemplate('preferences.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.') : '',
];
} }
/** /**

View File

@ -82,3 +82,26 @@ tr.prefRow:hover .prefHelp {
} }
#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%;
}

View File

@ -2,41 +2,96 @@
<!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd"> <!DOCTYPE overlay PUBLIC "-//EGroupware GmbH//eTemplate 2//EN" "http://www.egroupware.org/etemplate2.dtd">
<!-- $Id$ --> <!-- $Id$ -->
<overlay> <overlay>
<template id="preferences.password" template="" lang="" group="0" version="14.2"> <template id="preferences.password.change" template="" lang="" group="0" version="14.2">
<grid resize_ratio="0.25" > <grid width="100%">
<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">
<columns> <columns>
<column width="200"/>
<column/> <column/>
</columns> </columns>
<rows> <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> </rows>
</grid> </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 &amp; 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"> <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();"/> <button label="Cancel" id="button[cancel]" onclick="window.close();"/>
</hbox> </hbox>
</template> </template>

View File

@ -103,6 +103,28 @@ textarea.prefValue {
#preferences_settings_country_chzn { #preferences_settings_country_chzn {
width: 49% !important; 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 // iframe
// Rahmen + padding**/ // Rahmen + padding**/