Add support for HOTP and TOTP authentication

This supports Google Authenticator and similar applications.

Issue #6197

Signed-off-by: Michal Čihař <michal@cihar.com>
This commit is contained in:
Michal Čihař 2017-10-31 08:58:33 +01:00
parent a646d4314e
commit 869131f59c
8 changed files with 201 additions and 3 deletions

View File

@ -57,7 +57,9 @@
"symfony/polyfill-mbstring": "^1.3"
},
"conflict": {
"tecnickcom/tcpdf": "<6.2"
"tecnickcom/tcpdf": "<6.2",
"pragmarx/google2fa": "<2.0",
"bacon/bacon-qr-code": "<1.0"
},
"suggest": {
"ext-openssl": "Cookie encryption",
@ -68,7 +70,9 @@
"ext-zip": "For zip import and export",
"ext-gd2": "For image transformations",
"ext-mbstring": "For best performance",
"tecnickcom/tcpdf": "For PDF support"
"tecnickcom/tcpdf": "For PDF support",
"pragmarx/google2fa": "For 2FA authentication",
"bacon/bacon-qr-code": "For 2FA authentication"
},
"require-dev": {
"phpunit/phpunit": "~4.1",

View File

@ -0,0 +1,154 @@
<?php
/* vim: set expandtab sw=4 ts=4 sts=4: */
/**
* Second authentication factor handling
*
* @package PhpMyAdmin
*/
namespace PhpMyAdmin\Plugins\SecondFactor;
use PhpMyAdmin\Message;
use PhpMyAdmin\Template;
use PhpMyAdmin\Plugins\SecondFactorPlugin;
use PragmaRX\Google2FA\Google2FA;
/**
* HOTP and TOTP based second factor
*
* Also known as Google, Authy, or OTP
*/
class Application extends SecondFactorPlugin
{
/**
* @var string
*/
public static $id = 'application';
protected $_provided = false;
protected $_google2fa;
/**
* Creates object
*
* @param string $user User name
* @param array $config Second factor configuration
*/
public function __construct($user, $config)
{
parent::__construct($user, $config);
$this->_google2fa = new Google2FA();
$this->_google2fa->setWindow(8);
}
/**
* Get any property of this class
*
* @param string $property name of the property
*
* @return mixed|void if property exist, value of the relevant property
*/
public function __get($property)
{
switch ($property) {
case 'google2fa':
return $this->_google2fa;
case 'config':
return $this->_config;
}
}
/**
* Checks authentication, returns true on success
*
* @return boolean
*/
public function check()
{
$this->_provided = false;
if (!isset($_POST['2fa_code']) || !isset($this->_config['secret'])) {
return false;
}
$this->_provided = true;
return $this->_google2fa->verifyKey(
$this->_config['secret'], $_POST['2fa_code']
);
}
/**
* Renders user interface to enter second factor
*
* @return string HTML code
*/
public function render()
{
if ($this->_provided) {
Message::rawError(
__('Two-factor authentication failed.')
)->display();
}
return Template::get('login/second/application')->render();
}
/**
* Renders user interface to configure second factor
*
* @return string HTML code
*/
public function setup()
{
if ($this->_provided) {
Message::rawError(
__('Two-factor authentication failed.')
)->display();
}
$inlineUrl = $this->_google2fa->getQRCodeInline(
'phpMyAdmin',
$this->_user,
$this->_config['secret']
);
return Template::get('login/second/application_configure')->render([
'image' => $inlineUrl,
]);
}
/**
* Performs backend configuration
*
* @return boolean
*/
public function configure()
{
if (! isset($_SESSION['2fa_application_key'])) {
$_SESSION['2fa_application_key'] = $this->_google2fa->generateSecretKey();
}
$this->_config['secret'] = $_SESSION['2fa_application_key'];
$result = $this->check();
if ($result) {
unset($_SESSION['2fa_application_key']);
}
return $result;
}
/**
* Get user visible name
*
* @return string
*/
public static function getName()
{
return __('Authentication application');
}
/**
* Get user visible description
*
* @return string
*/
public static function getDescription()
{
return __('Provides authentication using HOTP and TOTP applications such as FreeOTP, Google Authenticator or Authy.');
}
}

View File

@ -91,6 +91,8 @@ class SecondFactor
return $this->_available;
case 'writable':
return $this->_writable;
case 'config':
return $this->_config;
}
}

View File

@ -228,7 +228,7 @@ if [ ! -d libraries/tcpdf ] ; then
# suggested package. Let's require it and then revert
# composer.json to original state.
cp composer.json composer.json.backup
composer require --update-no-dev tecnickcom/tcpdf
composer require --update-no-dev tecnickcom/tcpdf pragmarx/google2fa bacon/bacon-qr-code
mv composer.json.backup composer.json
echo "* Cleanup of composer packages"
rm -rf \

View File

@ -0,0 +1,4 @@
<p>
<label>{% trans "Authentication code:" %} <input type="text" name="2fa_code" /></label>
</p>
<p>{% trans "Open the two-factor authentication app on your device to view your authentication code and verify your identity." %}</p>

View File

@ -0,0 +1,10 @@
{{ Url_getHiddenInputs() }}
<p>
{% trans "Please scan following QR code into the two-factor authentication app on your device and enter authentication code it generates." %}
</p>
<p>
<img src="{{ image }}" />
</p>
<p>
<label>{% trans "Authentication code:" %} <input type="text" name="2fa_code" /></label>
</p>

View File

@ -2,3 +2,5 @@
./test/install-runkit
composer install --no-interaction
# Install optional deps
composer require tecnickcom/tcpdf pragmarx/google2fa bacon/bacon-qr-code

View File

@ -87,4 +87,26 @@ class SecondFactorTest extends PmaTestCase
$object = new SecondFactor('user');
$this->assertFalse($object->configure('simple'));
}
public function testApplication()
{
$object = new SecondFactor('user');
if (! in_array('application', $object->available)) {
$this->markTestSkipped('google2fa not available');
}
/* Without providing code this should fail */
$this->assertFalse($object->configure('application'));
/* Invalid code */
$_POST['2fa_code'] = 'invalid';
$this->assertFalse($object->configure('application'));
/* Generate valid code */
$google2fa = $object->backend->google2fa;
$_POST['2fa_code'] = $google2fa->oathHotp(
$object->backend->config['secret'],
$google2fa->getTimestamp()
);
$this->assertTrue($object->configure('application'));
}
}