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:
parent
a646d4314e
commit
869131f59c
@ -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",
|
||||
|
||||
154
libraries/classes/Plugins/SecondFactor/Application.php
Normal file
154
libraries/classes/Plugins/SecondFactor/Application.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +91,8 @@ class SecondFactor
|
||||
return $this->_available;
|
||||
case 'writable':
|
||||
return $this->_writable;
|
||||
case 'config':
|
||||
return $this->_config;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 \
|
||||
|
||||
4
templates/login/second/application.twig
Normal file
4
templates/login/second/application.twig
Normal 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>
|
||||
10
templates/login/second/application_configure.twig
Normal file
10
templates/login/second/application_configure.twig
Normal 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>
|
||||
@ -2,3 +2,5 @@
|
||||
|
||||
./test/install-runkit
|
||||
composer install --no-interaction
|
||||
# Install optional deps
|
||||
composer require tecnickcom/tcpdf pragmarx/google2fa bacon/bacon-qr-code
|
||||
|
||||
@ -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'));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user