diff --git a/composer.json b/composer.json index fd98c5ba3e..3e49d49291 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/libraries/classes/Plugins/SecondFactor/Application.php b/libraries/classes/Plugins/SecondFactor/Application.php new file mode 100644 index 0000000000..aa262969d6 --- /dev/null +++ b/libraries/classes/Plugins/SecondFactor/Application.php @@ -0,0 +1,154 @@ +_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.'); + } +} + diff --git a/libraries/classes/SecondFactor.php b/libraries/classes/SecondFactor.php index 7f47de5dc4..83bdf4f1b2 100644 --- a/libraries/classes/SecondFactor.php +++ b/libraries/classes/SecondFactor.php @@ -91,6 +91,8 @@ class SecondFactor return $this->_available; case 'writable': return $this->_writable; + case 'config': + return $this->_config; } } diff --git a/scripts/create-release.sh b/scripts/create-release.sh index de1c7e0626..7747c2f5f8 100755 --- a/scripts/create-release.sh +++ b/scripts/create-release.sh @@ -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 \ diff --git a/templates/login/second/application.twig b/templates/login/second/application.twig new file mode 100644 index 0000000000..1a919dc73b --- /dev/null +++ b/templates/login/second/application.twig @@ -0,0 +1,4 @@ +
+ +
+{% trans "Open the two-factor authentication app on your device to view your authentication code and verify your identity." %}
diff --git a/templates/login/second/application_configure.twig b/templates/login/second/application_configure.twig new file mode 100644 index 0000000000..40a67ad552 --- /dev/null +++ b/templates/login/second/application_configure.twig @@ -0,0 +1,10 @@ +{{ Url_getHiddenInputs() }} ++{% trans "Please scan following QR code into the two-factor authentication app on your device and enter authentication code it generates." %} +
+
+
+
+ +
diff --git a/test/ci-install-test b/test/ci-install-test index 4917bc7ed8..a32fbf74a3 100755 --- a/test/ci-install-test +++ b/test/ci-install-test @@ -2,3 +2,5 @@ ./test/install-runkit composer install --no-interaction +# Install optional deps +composer require tecnickcom/tcpdf pragmarx/google2fa bacon/bacon-qr-code diff --git a/test/classes/SecondFactorTest.php b/test/classes/SecondFactorTest.php index 6c197c6977..1baef7da87 100644 --- a/test/classes/SecondFactorTest.php +++ b/test/classes/SecondFactorTest.php @@ -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')); + } }