From e62779e26781c849bdc24f40e94330bec97f8069 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Tue, 5 Jan 2016 20:31:15 -0500 Subject: Improve 2FA --- app/Auth/TotpAuth.php | 28 +++++-- app/Controller/Twofactor.php | 88 +++++++++++++++------- .../PostAuthenticationProviderInterface.php | 15 ++++ app/Core/Security/Token.php | 10 +-- app/Template/twofactor/index.php | 40 ++-------- app/Template/twofactor/show.php | 31 ++++++++ composer.json | 1 + composer.lock | 53 ++++++++++++- tests/units/Auth/TotpAuthTest.php | 5 +- 9 files changed, 191 insertions(+), 80 deletions(-) create mode 100644 app/Template/twofactor/show.php diff --git a/app/Auth/TotpAuth.php b/app/Auth/TotpAuth.php index f41fabd8..e1cc712c 100644 --- a/app/Auth/TotpAuth.php +++ b/app/Auth/TotpAuth.php @@ -40,7 +40,7 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface */ public function getName() { - return 'Time-based One-time Password Algorithm'; + return t('Time-based One-time Password Algorithm'); } /** @@ -55,6 +55,16 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface return $otp->checkTotp(Base32::decode($this->secret), $this->code); } + /** + * Called before to prompt the user + * + * @access public + */ + public function beforeCode() + { + + } + /** * Set validation code * @@ -66,6 +76,18 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface $this->code = $code; } + /** + * Generate secret + * + * @access public + * @return string + */ + public function generateSecret() + { + $this->secret = GoogleAuthenticator::generateRandom(); + return $this->secret; + } + /** * Set secret token * @@ -85,10 +107,6 @@ class TotpAuth extends Base implements PostAuthenticationProviderInterface */ public function getSecret() { - if (empty($this->secret)) { - $this->secret = GoogleAuthenticator::generateRandom(); - } - return $this->secret; } diff --git a/app/Controller/Twofactor.php b/app/Controller/Twofactor.php index aeb13acc..8dbfcf66 100644 --- a/app/Controller/Twofactor.php +++ b/app/Controller/Twofactor.php @@ -23,7 +23,7 @@ class Twofactor extends User } /** - * Index + * Show form to disable/enable 2FA * * @access public */ @@ -31,54 +31,45 @@ class Twofactor extends User { $user = $this->getUser(); $this->checkCurrentUser($user); - - $provider = $this->authenticationManager->getPostAuthenticationProvider(); - $label = $user['email'] ?: $user['username']; - - $provider->setSecret($user['twofactor_secret']); + unset($this->sessionStorage->twoFactorSecret); $this->response->html($this->layout('twofactor/index', array( 'user' => $user, - 'qrcode_url' => $user['twofactor_activated'] == 1 ? $provider->getQrCodeUrl($label) : '', - 'key_url' => $user['twofactor_activated'] == 1 ? $provider->getKeyUrl($label) : '', + 'provider' => $this->authenticationManager->getPostAuthenticationProvider()->getName(), ))); } /** - * Enable/disable 2FA + * Show page with secret and test form * * @access public */ - public function save() + public function show() { $user = $this->getUser(); $this->checkCurrentUser($user); - $values = $this->request->getValues(); + $label = $user['email'] ?: $user['username']; + $provider = $this->authenticationManager->getPostAuthenticationProvider(); - if (isset($values['twofactor_activated']) && $values['twofactor_activated'] == 1) { - $this->user->update(array( - 'id' => $user['id'], - 'twofactor_activated' => 1, - 'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(), - )); + if (! isset($this->sessionStorage->twoFactorSecret)) { + $provider->generateSecret(); + $provider->beforeCode(); + $this->sessionStorage->twoFactorSecret = $provider->getSecret(); } else { - $this->user->update(array( - 'id' => $user['id'], - 'twofactor_activated' => 0, - 'twofactor_secret' => '', - )); + $provider->setSecret($this->sessionStorage->twoFactorSecret); } - // Allow the user to test or disable the feature - $this->userSession->disablePostAuthentication(); - - $this->flash->success(t('User updated successfully.')); - $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); + $this->response->html($this->layout('twofactor/show', array( + 'user' => $user, + 'secret' => $this->sessionStorage->twoFactorSecret, + 'qrcode_url' => $provider->getQrCodeUrl($label), + 'key_url' => $provider->getKeyUrl($label), + ))); } /** - * Test code + * Test code and save secret * * @access public */ @@ -91,14 +82,47 @@ class Twofactor extends User $provider = $this->authenticationManager->getPostAuthenticationProvider(); $provider->setCode(empty($values['code']) ? '' : $values['code']); - $provider->setSecret($user['twofactor_secret']); + $provider->setSecret($this->sessionStorage->twoFactorSecret); if ($provider->authenticate()) { $this->flash->success(t('The two factor authentication code is valid.')); + + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 1, + 'twofactor_secret' => $this->authenticationManager->getPostAuthenticationProvider()->getSecret(), + )); + + unset($this->sessionStorage->twoFactorSecret); + $this->userSession->disablePostAuthentication(); + + $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); } else { $this->flash->failure(t('The two factor authentication code is not valid.')); + $this->response->redirect($this->helper->url->to('twofactor', 'show', array('user_id' => $user['id']))); } + } + /** + * Disable 2FA for the current user + * + * @access public + */ + public function deactivate() + { + $user = $this->getUser(); + $this->checkCurrentUser($user); + + $this->user->update(array( + 'id' => $user['id'], + 'twofactor_activated' => 0, + 'twofactor_secret' => '', + )); + + // Allow the user to test or disable the feature + $this->userSession->disablePostAuthentication(); + + $this->flash->success(t('User updated successfully.')); $this->response->redirect($this->helper->url->to('twofactor', 'index', array('user_id' => $user['id']))); } @@ -135,6 +159,12 @@ class Twofactor extends User */ public function code() { + if (! isset($this->sessionStorage->twoFactorBeforeCodeCalled)) { + $provider = $this->authenticationManager->getPostAuthenticationProvider(); + $provider->beforeCode(); + $this->sessionStorage->twoFactorBeforeCodeCalled = true; + } + $this->response->html($this->template->layout('twofactor/check', array( 'title' => t('Check two factor authentication code'), ))); diff --git a/app/Core/Security/PostAuthenticationProviderInterface.php b/app/Core/Security/PostAuthenticationProviderInterface.php index 88fc2fe5..3f628bb0 100644 --- a/app/Core/Security/PostAuthenticationProviderInterface.php +++ b/app/Core/Security/PostAuthenticationProviderInterface.php @@ -10,6 +10,13 @@ namespace Kanboard\Core\Security; */ interface PostAuthenticationProviderInterface extends AuthenticationProviderInterface { + /** + * Called only one time before to prompt the user for pin code + * + * @access public + */ + public function beforeCode(); + /** * Set user pin-code * @@ -18,6 +25,14 @@ interface PostAuthenticationProviderInterface extends AuthenticationProviderInte */ public function setCode($code); + /** + * Generate secret if necessary + * + * @access public + * @return string + */ + public function generateSecret(); + /** * Set secret token (fetched from user profile) * diff --git a/app/Core/Security/Token.php b/app/Core/Security/Token.php index 9fd2d02b..cbd784a8 100644 --- a/app/Core/Security/Token.php +++ b/app/Core/Security/Token.php @@ -21,15 +21,7 @@ class Token extends Base */ public static function getToken() { - if (function_exists('random_bytes')) { - return bin2hex(random_bytes(30)); - } elseif (function_exists('openssl_random_pseudo_bytes')) { - return bin2hex(openssl_random_pseudo_bytes(30)); - } elseif (ini_get('open_basedir') === '' && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { - return hash('sha256', file_get_contents('/dev/urandom', false, null, 0, 30)); - } - - return hash('sha256', uniqid(mt_rand(), true)); + return bin2hex(random_bytes(30)); } /** diff --git a/app/Template/twofactor/index.php b/app/Template/twofactor/index.php index 4c4ca088..b9ee4b49 100644 --- a/app/Template/twofactor/index.php +++ b/app/Template/twofactor/index.php @@ -2,42 +2,14 @@

-
- + form->csrf() ?> - form->checkbox('twofactor_activated', t('Enable/disable two factor authentication'), 1, isset($user['twofactor_activated']) && $user['twofactor_activated'] == 1) ?> - +

e($provider) ?>

- -
-
- - -
-

e($user['twofactor_secret']) ?> (base32)

- - -




- - -

- - e($key_url) ?> -

+ + + + - -

-
- -

-
- - form->csrf() ?> - form->label(t('Code'), 'code') ?> - form->text('code', array(), array(), array('placeholder="123456"'), 'form-numeric') ?> - -
-
- diff --git a/app/Template/twofactor/show.php b/app/Template/twofactor/show.php new file mode 100644 index 00000000..dd72965a --- /dev/null +++ b/app/Template/twofactor/show.php @@ -0,0 +1,31 @@ + + + +
+ +

e($secret) ?>

+ + + +




+ + + +

e($key_url) ?>

+ +
+ + +

+
+ + form->csrf() ?> + form->label(t('Code'), 'code') ?> + form->text('code', array(), array(), array('placeholder="123456"', 'autofocus'), 'form-numeric') ?> + +
+ +
+
\ No newline at end of file diff --git a/composer.json b/composer.json index b2eed1f1..6907f0ce 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "fguillot/simpleLogger" : "1.0.0", "fguillot/simple-validator" : "1.0.0", "league/html-to-markdown" : "~4.0", + "paragonie/random_compat": "@stable", "pimple/pimple" : "~3.0", "ramsey/array_column": "@stable", "swiftmailer/swiftmailer" : "~5.4", diff --git a/composer.lock b/composer.lock index d2549d4f..047d2415 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "061fd1874ea29264f1aeee3d9394f441", - "content-hash": "85eaeb7f843881473efd22773a1e97d4", + "hash": "6fdfe5fd711d763418007b9f7b2bf5d4", + "content-hash": "c90bee5db71739e8c759b5c8f5032699", "packages": [ { "name": "christian-riesen/base32", @@ -459,6 +459,54 @@ ], "time": "2015-11-20 16:20:25" }, + { + "name": "paragonie/random_compat", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "d762ee5b099a29044603cd4649851e81aa66cb47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/d762ee5b099a29044603cd4649851e81aa66cb47", + "reference": "d762ee5b099a29044603cd4649851e81aa66cb47", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "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", + "pseudorandom", + "random" + ], + "time": "2015-12-10 14:48:13" + }, { "name": "pimple/pimple", "version": "v3.0.2", @@ -876,6 +924,7 @@ "minimum-stability": "stable", "stability-flags": { "fguillot/picodb": 20, + "paragonie/random_compat": 0, "ramsey/array_column": 0 }, "prefer-stable": false, diff --git a/tests/units/Auth/TotpAuthTest.php b/tests/units/Auth/TotpAuthTest.php index fcb7ea31..c8dcfb28 100644 --- a/tests/units/Auth/TotpAuthTest.php +++ b/tests/units/Auth/TotpAuthTest.php @@ -15,6 +15,9 @@ class TotpAuthTest extends Base public function testGetSecret() { $provider = new TotpAuth($this->container); + $this->assertEmpty($provider->getSecret()); + + $provider->generateSecret(); $secret = $provider->getSecret(); $this->assertNotEmpty($secret); @@ -48,7 +51,7 @@ class TotpAuthTest extends Base { $provider = new TotpAuth($this->container); - $secret = $provider->getSecret(); + $secret = $provider->generateSecret(); $this->assertNotEmpty($secret); $provider->setCode('1234'); -- cgit v1.2.3