summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFrederic Guillot <fred@kanboard.net>2015-03-31 22:48:14 -0400
committerFrederic Guillot <fred@kanboard.net>2015-03-31 22:48:14 -0400
commitabeeba71672a711dab98194bb8ae751ee95e3385 (patch)
tree26838682b1ab13611b9697d1b8900122409dbf47
parent5d393ed9962ebe18a162cb09b08eaea9359df2cc (diff)
Add two factor authentication
-rw-r--r--README.markdown1
-rw-r--r--app/Controller/Base.php21
-rw-r--r--app/Controller/Twofactor.php137
-rw-r--r--app/Locale/da_DK/translations.php11
-rw-r--r--app/Locale/de_DE/translations.php11
-rw-r--r--app/Locale/es_ES/translations.php11
-rw-r--r--app/Locale/fi_FI/translations.php11
-rw-r--r--app/Locale/fr_FR/translations.php11
-rw-r--r--app/Locale/hu_HU/translations.php11
-rw-r--r--app/Locale/it_IT/translations.php11
-rw-r--r--app/Locale/ja_JP/translations.php11
-rw-r--r--app/Locale/nl_NL/translations.php11
-rw-r--r--app/Locale/pl_PL/translations.php11
-rw-r--r--app/Locale/pt_BR/translations.php11
-rw-r--r--app/Locale/ru_RU/translations.php11
-rw-r--r--app/Locale/sr_Latn_RS/translations.php11
-rw-r--r--app/Locale/sv_SE/translations.php11
-rw-r--r--app/Locale/th_TH/translations.php11
-rw-r--r--app/Locale/tr_TR/translations.php11
-rw-r--r--app/Locale/zh_CN/translations.php11
-rw-r--r--app/Model/User.php3
-rw-r--r--app/Model/UserSession.php27
-rw-r--r--app/Schema/Mysql.php8
-rw-r--r--app/Schema/Postgres.php8
-rw-r--r--app/Schema/Sqlite.php8
-rw-r--r--app/Template/twofactor/check.php10
-rw-r--r--app/Template/twofactor/index.php37
-rw-r--r--app/Template/user/index.php4
-rw-r--r--app/Template/user/sidebar.php45
-rw-r--r--composer.json3
-rw-r--r--composer.lock105
-rw-r--r--docs/2fa.markdown33
32 files changed, 615 insertions, 22 deletions
diff --git a/README.markdown b/README.markdown
index 98601ebf..1691e96f 100644
--- a/README.markdown
+++ b/README.markdown
@@ -84,6 +84,7 @@ Documentation
#### Working with users
- [User management](docs/user-management.markdown)
+- [Two factor authentication](docs/2fa.markdown)
#### Settings
diff --git a/app/Controller/Base.php b/app/Controller/Base.php
index 6420e0ee..f498c3ce 100644
--- a/app/Controller/Base.php
+++ b/app/Controller/Base.php
@@ -176,6 +176,7 @@ abstract class Base
if (! $this->acl->isPublicAction($controller, $action)) {
$this->handleAuthentication();
+ $this->handle2FA($controller, $action);
$this->handleAuthorization($controller, $action);
$this->session['has_subtask_inprogress'] = $this->subtask->hasSubtaskInProgress($this->userSession->getId());
@@ -200,6 +201,26 @@ abstract class Base
}
/**
+ * Check 2FA
+ *
+ * @access public
+ */
+ public function handle2FA($controller, $action)
+ {
+ $controllers = array('twofactor', 'user');
+ $actions = array('code', 'check', 'logout');
+
+ if ($this->userSession->has2FA() && ! $this->userSession->check2FA() && ! in_array($controller, $controllers) && ! in_array($action, $actions)) {
+
+ if ($this->request->isAjax()) {
+ $this->response->text('Not Authorized', 401);
+ }
+
+ $this->response->redirect($this->helper->url('twofactor', 'code', array('user_id' => $user['id'])));
+ }
+ }
+
+ /**
* Check page access and authorization
*
* @access public
diff --git a/app/Controller/Twofactor.php b/app/Controller/Twofactor.php
new file mode 100644
index 00000000..7711666b
--- /dev/null
+++ b/app/Controller/Twofactor.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Controller;
+
+use Otp\Otp;
+use Otp\GoogleAuthenticator;
+use Base32\Base32;
+
+/**
+ * Two Factor Auth controller
+ *
+ * @package controller
+ * @author Frederic Guillot
+ */
+class Twofactor extends User
+{
+ /**
+ * Only the current user can access to 2FA settings
+ *
+ * @access private
+ */
+ private function checkCurrentUser(array $user)
+ {
+ if ($user['id'] != $this->userSession->getId()) {
+ $this->forbidden();
+ }
+ }
+
+ /**
+ * Index
+ *
+ * @access public
+ */
+ public function index()
+ {
+ $user = $this->getUser();
+ $this->checkCurrentUser($user);
+
+ $label = $user['email'] ?: $user['username'];
+
+ $this->response->html($this->layout('twofactor/index', array(
+ 'user' => $user,
+ 'qrcode_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getQrCodeUrl('totp', $label, $user['twofactor_secret']) : '',
+ 'key_url' => $user['twofactor_activated'] == 1 ? GoogleAuthenticator::getKeyUri('totp', $label, $user['twofactor_secret']) : '',
+ )));
+ }
+
+ /**
+ * Enable/disable 2FA
+ *
+ * @access public
+ */
+ public function save()
+ {
+ $user = $this->getUser();
+ $this->checkCurrentUser($user);
+
+ $values = $this->request->getValues();
+
+ if (isset($values['twofactor_activated']) && $values['twofactor_activated'] == 1) {
+ $this->user->update(array(
+ 'id' => $user['id'],
+ 'twofactor_activated' => 1,
+ 'twofactor_secret' => GoogleAuthenticator::generateRandom(),
+ ));
+ }
+ else {
+ $this->user->update(array(
+ 'id' => $user['id'],
+ 'twofactor_activated' => 0,
+ 'twofactor_secret' => '',
+ ));
+ }
+
+ $this->session->flash(t('User updated successfully.'));
+ $this->response->redirect($this->helper->url('twofactor', 'index', array('user_id' => $user['id'])));
+ }
+
+ /**
+ * Test 2FA
+ *
+ * @access public
+ */
+ public function test()
+ {
+ $user = $this->getUser();
+ $this->checkCurrentUser($user);
+
+ $otp = new Otp;
+ $values = $this->request->getValues();
+
+ if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) {
+ $this->session->flash(t('The two factor authentication code is valid.'));
+ }
+ else {
+ $this->session->flashError(t('The two factor authentication code is not valid.'));
+ }
+
+ $this->response->redirect($this->helper->url('twofactor', 'index', array('user_id' => $user['id'])));
+ }
+
+ /**
+ * Check 2FA
+ *
+ * @access public
+ */
+ public function check()
+ {
+ $user = $this->getUser();
+ $this->checkCurrentUser($user);
+
+ $otp = new Otp;
+ $values = $this->request->getValues();
+
+ if (! empty($values['code']) && $otp->checkTotp(Base32::decode($user['twofactor_secret']), $values['code'])) {
+ $this->session['2fa_validated'] = true;
+ $this->session->flash(t('The two factor authentication code is valid.'));
+ $this->response->redirect($this->helper->url('app', 'index'));
+ }
+ else {
+ $this->session->flashError(t('The two factor authentication code is not valid.'));
+ $this->response->redirect($this->helper->url('twofactor', 'code'));
+ }
+ }
+
+ /**
+ * Ask the 2FA code
+ *
+ * @access public
+ */
+ public function code()
+ {
+ $this->response->html($this->template->layout('twofactor/check', array(
+ 'title' => t('Check two factor authentication code'),
+ )));
+ }
+}
diff --git a/app/Locale/da_DK/translations.php b/app/Locale/da_DK/translations.php
index 6d08a885..51397129 100644
--- a/app/Locale/da_DK/translations.php
+++ b/app/Locale/da_DK/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/de_DE/translations.php b/app/Locale/de_DE/translations.php
index 5bc1663c..82319766 100644
--- a/app/Locale/de_DE/translations.php
+++ b/app/Locale/de_DE/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/es_ES/translations.php b/app/Locale/es_ES/translations.php
index 80de8774..4fa07781 100644
--- a/app/Locale/es_ES/translations.php
+++ b/app/Locale/es_ES/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/fi_FI/translations.php b/app/Locale/fi_FI/translations.php
index 842086bb..239d5a51 100644
--- a/app/Locale/fi_FI/translations.php
+++ b/app/Locale/fi_FI/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/fr_FR/translations.php b/app/Locale/fr_FR/translations.php
index ef53801e..7cca5aa6 100644
--- a/app/Locale/fr_FR/translations.php
+++ b/app/Locale/fr_FR/translations.php
@@ -836,4 +836,15 @@ return array(
'Help on Hipchat integration' => 'Aide sur l\'intégration avec Hipchat',
'Enable Gravatar images' => 'Activer les images Gravatar',
'Information' => 'Informations',
+ 'Check two factor authentication code' => 'Vérification du code pour l\'authentification à deux-facteurs',
+ 'The two factor authentication code is not valid.' => 'Le code pour l\'authentification à deux-facteurs n\'est pas valide.',
+ 'The two factor authentication code is valid.' => 'Le code pour l\'authentification à deux-facteurs est valide.',
+ 'Code' => 'Code',
+ 'Two factor authentication' => 'Authentification à deux-facteurs',
+ 'Enable/disable two factor authentication' => 'Activer/désactiver l\'authentification à deux-facteurs',
+ 'This QR Ccde contains the key URI: ' => 'Ce code QR contient l\'url de la clé : ',
+ 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => 'Sauvegardez cette clé secrete dans votre logiciel TOTP (par exemple Google Authenticator ou FreeOTP).',
+ 'Check my code' => 'Vérifier mon code',
+ 'Secret key: ' => 'Clé secrète : ',
+ 'Test your device' => 'Testez votre appareil',
);
diff --git a/app/Locale/hu_HU/translations.php b/app/Locale/hu_HU/translations.php
index cb23c082..af5c8cb7 100644
--- a/app/Locale/hu_HU/translations.php
+++ b/app/Locale/hu_HU/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/it_IT/translations.php b/app/Locale/it_IT/translations.php
index b0d38c8e..b4cdf9f0 100644
--- a/app/Locale/it_IT/translations.php
+++ b/app/Locale/it_IT/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/ja_JP/translations.php b/app/Locale/ja_JP/translations.php
index 2573fbf9..ff563073 100644
--- a/app/Locale/ja_JP/translations.php
+++ b/app/Locale/ja_JP/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/nl_NL/translations.php b/app/Locale/nl_NL/translations.php
index 4cc4a5fa..eba573d2 100644
--- a/app/Locale/nl_NL/translations.php
+++ b/app/Locale/nl_NL/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/pl_PL/translations.php b/app/Locale/pl_PL/translations.php
index e02d1f2b..bad64238 100644
--- a/app/Locale/pl_PL/translations.php
+++ b/app/Locale/pl_PL/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/pt_BR/translations.php b/app/Locale/pt_BR/translations.php
index 2483e142..787aa2d0 100644
--- a/app/Locale/pt_BR/translations.php
+++ b/app/Locale/pt_BR/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/ru_RU/translations.php b/app/Locale/ru_RU/translations.php
index 86cc1531..bcf216cc 100644
--- a/app/Locale/ru_RU/translations.php
+++ b/app/Locale/ru_RU/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/sr_Latn_RS/translations.php b/app/Locale/sr_Latn_RS/translations.php
index e6bdd7c2..0229a030 100644
--- a/app/Locale/sr_Latn_RS/translations.php
+++ b/app/Locale/sr_Latn_RS/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/sv_SE/translations.php b/app/Locale/sv_SE/translations.php
index 4dc75556..3e52818b 100644
--- a/app/Locale/sv_SE/translations.php
+++ b/app/Locale/sv_SE/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/th_TH/translations.php b/app/Locale/th_TH/translations.php
index 1000cf3f..400c0c26 100644
--- a/app/Locale/th_TH/translations.php
+++ b/app/Locale/th_TH/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/tr_TR/translations.php b/app/Locale/tr_TR/translations.php
index 62d404eb..0f8a2802 100644
--- a/app/Locale/tr_TR/translations.php
+++ b/app/Locale/tr_TR/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Locale/zh_CN/translations.php b/app/Locale/zh_CN/translations.php
index 4cb288ad..5f3eacf0 100644
--- a/app/Locale/zh_CN/translations.php
+++ b/app/Locale/zh_CN/translations.php
@@ -834,4 +834,15 @@ return array(
// 'Help on Hipchat integration' => '',
// 'Enable Gravatar images' => '',
// 'Information' => '',
+ // 'Check two factor authentication code' => '',
+ // 'The two factor authentication code is not valid.' => '',
+ // 'The two factor authentication code is valid.' => '',
+ // 'Code' => '',
+ // 'Two factor authentication' => '',
+ // 'Enable/disable two factor authentication' => '',
+ // 'This QR Ccde contains the key URI: ' => '',
+ // 'Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).' => '',
+ // 'Check my code' => '',
+ // 'Secret key: ' => '',
+ // 'Test your device' => '',
);
diff --git a/app/Model/User.php b/app/Model/User.php
index 7586f3c4..6c348caa 100644
--- a/app/Model/User.php
+++ b/app/Model/User.php
@@ -60,7 +60,8 @@ class User extends Base
'is_ldap_user',
'notifications_enabled',
'google_id',
- 'github_id'
+ 'github_id',
+ 'twofactor_activated'
);
}
diff --git a/app/Model/UserSession.php b/app/Model/UserSession.php
index 6d9a2ebc..efb02722 100644
--- a/app/Model/UserSession.php
+++ b/app/Model/UserSession.php
@@ -28,15 +28,42 @@ class UserSession extends Base
unset($user['password']);
}
+ if (isset($user['twofactor_secret'])) {
+ unset($user['twofactor_secret']);
+ }
+
$user['id'] = (int) $user['id'];
$user['default_project_id'] = (int) $user['default_project_id'];
$user['is_admin'] = (bool) $user['is_admin'];
$user['is_ldap_user'] = (bool) $user['is_ldap_user'];
+ $user['twofactor_activated'] = (bool) $user['twofactor_activated'];
$this->session['user'] = $user;
}
/**
+ * Return true if the user has validated the 2FA key
+ *
+ * @access public
+ * @return bool
+ */
+ public function check2FA()
+ {
+ return isset($this->session['2fa_validated']) && $this->session['2fa_validated'] === true;
+ }
+
+ /**
+ * Return true if the user has 2FA enabled
+ *
+ * @access public
+ * @return bool
+ */
+ public function has2FA()
+ {
+ return isset($this->session['user']['twofactor_activated']) && $this->session['user']['twofactor_activated'] === true;
+ }
+
+ /**
* Return true if the logged user is admin
*
* @access public
diff --git a/app/Schema/Mysql.php b/app/Schema/Mysql.php
index 4ea7b041..2dcfd3b7 100644
--- a/app/Schema/Mysql.php
+++ b/app/Schema/Mysql.php
@@ -6,7 +6,13 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 60;
+const VERSION = 61;
+
+function version_61($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_activated TINYINT(1) DEFAULT 0');
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_secret CHAR(16)');
+}
function version_60($pdo)
{
diff --git a/app/Schema/Postgres.php b/app/Schema/Postgres.php
index 93d8e869..6c6ea001 100644
--- a/app/Schema/Postgres.php
+++ b/app/Schema/Postgres.php
@@ -6,7 +6,13 @@ use PDO;
use Core\Security;
use Model\Link;
-const VERSION = 40;
+const VERSION = 42;
+
+function version_42($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_activated BOOLEAN DEFAULT \'0\'');
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_secret CHAR(16)');
+}
function version_41($pdo)
{
diff --git a/app/Schema/Sqlite.php b/app/Schema/Sqlite.php
index 3683acb6..ef6523e4 100644
--- a/app/Schema/Sqlite.php
+++ b/app/Schema/Sqlite.php
@@ -6,7 +6,13 @@ use Core\Security;
use PDO;
use Model\Link;
-const VERSION = 59;
+const VERSION = 60;
+
+function version_60($pdo)
+{
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_activated INTEGER DEFAULT 0');
+ $pdo->exec('ALTER TABLE users ADD COLUMN twofactor_secret TEXT');
+}
function version_59($pdo)
{
diff --git a/app/Template/twofactor/check.php b/app/Template/twofactor/check.php
new file mode 100644
index 00000000..af64bc76
--- /dev/null
+++ b/app/Template/twofactor/check.php
@@ -0,0 +1,10 @@
+<form method="post" action="<?= $this->u('twofactor', 'check', array('user_id' => $this->userSession->getId())) ?>" autocomplete="off">
+
+ <?= $this->formCsrf() ?>
+ <?= $this->formLabel(t('Code'), 'code') ?>
+ <?= $this->formText('code', array(), array(), array('placeholder="123456"'), 'form-numeric') ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Check my code') ?>" class="btn btn-blue"/>
+ </div>
+</form> \ No newline at end of file
diff --git a/app/Template/twofactor/index.php b/app/Template/twofactor/index.php
new file mode 100644
index 00000000..bc9a12e9
--- /dev/null
+++ b/app/Template/twofactor/index.php
@@ -0,0 +1,37 @@
+<div class="page-header">
+ <h2><?= t('Two factor authentication') ?></h2>
+</div>
+
+<form method="post" action="<?= $this->u('twofactor', 'save', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->formCsrf() ?>
+ <?= $this->formCheckbox('twofactor_activated', t('Enable/disable two factor authentication'), 1, isset($user['twofactor_activated']) && $user['twofactor_activated'] == 1) ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Save') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+
+<?php if ($user['twofactor_activated'] == 1): ?>
+<div class="listing">
+ <p><?= t('Secret key: ') ?><strong><?= $this->e($user['twofactor_secret']) ?></strong> (base32)</p>
+ <p><br/><img src="<?= $qrcode_url ?>"/><br/><br/></p>
+ <p>
+ <?= t('This QR Ccde contains the key URI: ') ?><strong><?= $this->e($key_url) ?></strong>
+ <br/><br/>
+ <?= t('Save the secret key in your TOTP software (by example Google Authenticator or FreeOTP).') ?>
+ </p>
+</div>
+
+<h3><?= t('Test your device') ?></h3>
+<form method="post" action="<?= $this->u('twofactor', 'test', array('user_id' => $user['id'])) ?>" autocomplete="off">
+
+ <?= $this->formCsrf() ?>
+ <?= $this->formLabel(t('Code'), 'code') ?>
+ <?= $this->formText('code', array(), array(), array('placeholder="123456"'), 'form-numeric') ?>
+
+ <div class="form-actions">
+ <input type="submit" value="<?= t('Check my code') ?>" class="btn btn-blue"/>
+ </div>
+</form>
+<?php endif ?>
diff --git a/app/Template/user/index.php b/app/Template/user/index.php
index 41e205ba..d6b0fecf 100644
--- a/app/Template/user/index.php
+++ b/app/Template/user/index.php
@@ -17,6 +17,7 @@
<th><?= $paginator->order(t('Name'), 'name') ?></th>
<th><?= $paginator->order(t('Email'), 'email') ?></th>
<th><?= $paginator->order(t('Administrator'), 'is_admin') ?></th>
+ <th><?= $paginator->order(t('Two factor authentication'), 'twofactor_activated') ?></th>
<th><?= $paginator->order(t('Default project'), 'default_project_id') ?></th>
<th><?= $paginator->order(t('Notifications'), 'notifications_enabled') ?></th>
<th><?= t('External accounts') ?></th>
@@ -40,6 +41,9 @@
<?= $user['is_admin'] ? t('Yes') : t('No') ?>
</td>
<td>
+ <?= $user['twofactor_activated'] ? t('Yes') : t('No') ?>
+ </td>
+ <td>
<?= (isset($user['default_project_id']) && isset($projects[$user['default_project_id']])) ? $this->e($projects[$user['default_project_id']]) : t('None'); ?>
</td>
<td>
diff --git a/app/Template/user/sidebar.php b/app/Template/user/sidebar.php
index 1af10c1d..f794c609 100644
--- a/app/Template/user/sidebar.php
+++ b/app/Template/user/sidebar.php
@@ -1,10 +1,32 @@
<div class="sidebar">
- <h2><?= t('Actions') ?></h2>
+ <h2><?= t('Information') ?></h2>
<ul>
<li>
<?= $this->a(t('Summary'), 'user', 'show', array('user_id' => $user['id'])) ?>
</li>
+ <?php if ($this->userSession->isAdmin()): ?>
+ <li>
+ <?= $this->a(t('User dashboard'), 'app', 'dashboard', array('user_id' => $user['id'])) ?>
+ </li>
+ <li>
+ <?= $this->a(t('User calendar'), 'user', 'calendar', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
+ <?php if ($this->userSession->isAdmin() || $this->userSession->isCurrentUser($user['id'])): ?>
+ <li>
+ <?= $this->a(t('Time tracking'), 'user', 'timesheet', array('user_id' => $user['id'])) ?>
+ </li>
+ <li>
+ <?= $this->a(t('Last logins'), 'user', 'last', array('user_id' => $user['id'])) ?>
+ </li>
+ <li>
+ <?= $this->a(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
+ </ul>
+ <h2><?= t('Actions') ?></h2>
+ <ul>
<?php if ($this->userSession->isAdmin() || $this->userSession->isCurrentUser($user['id'])): ?>
<li>
<?= $this->a(t('Edit profile'), 'user', 'edit', array('user_id' => $user['id'])) ?>
@@ -16,31 +38,22 @@
</li>
<?php endif ?>
+ <?php if ($this->userSession->isCurrentUser($user['id'])): ?>
+ <li>
+ <?= $this->a(t('Two factor authentication'), 'twofactor', 'index', array('user_id' => $user['id'])) ?>
+ </li>
+ <?php endif ?>
+
<li>
<?= $this->a(t('Email notifications'), 'user', 'notifications', array('user_id' => $user['id'])) ?>
</li>
<li>
<?= $this->a(t('External accounts'), 'user', 'external', array('user_id' => $user['id'])) ?>
</li>
- <li>
- <?= $this->a(t('Last logins'), 'user', 'last', array('user_id' => $user['id'])) ?>
- </li>
- <li>
- <?= $this->a(t('Persistent connections'), 'user', 'sessions', array('user_id' => $user['id'])) ?>
- </li>
- <li>
- <?= $this->a(t('Time tracking'), 'user', 'timesheet', array('user_id' => $user['id'])) ?>
- </li>
<?php endif ?>
<?php if ($this->userSession->isAdmin()): ?>
<li>
- <?= $this->a(t('User dashboard'), 'app', 'dashboard', array('user_id' => $user['id'])) ?>
- </li>
- <li>
- <?= $this->a(t('User calendar'), 'user', 'calendar', array('user_id' => $user['id'])) ?>
- </li>
- <li>
<?= $this->a(t('Hourly rates'), 'hourlyrate', 'index', array('user_id' => $user['id'])) ?>
</li>
<li>
diff --git a/composer.json b/composer.json
index 8e78ab39..3c8c74a4 100644
--- a/composer.json
+++ b/composer.json
@@ -11,7 +11,8 @@
"pimple/pimple" : "~3.0",
"symfony/console" : "@stable",
"symfony/event-dispatcher" : "~2.6",
- "fguillot/simpleLogger" : "0.0.1"
+ "fguillot/simpleLogger" : "0.0.1",
+ "christian-riesen/otp": "1.4"
},
"autoload" : {
"psr-0" : {
diff --git a/composer.lock b/composer.lock
index 3331ecd1..360ed751 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,9 +4,111 @@
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "hash": "9c0c8b06745d54478604d80815b59b9b",
+ "hash": "466ee3928d7d3b0bbee15de3b4c76676",
"packages": [
{
+ "name": "christian-riesen/base32",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ChristianRiesen/base32.git",
+ "reference": "6943e7b010ab224139fd4248ccf84865aa7b0f91"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ChristianRiesen/base32/zipball/6943e7b010ab224139fd4248ccf84865aa7b0f91",
+ "reference": "6943e7b010ab224139fd4248ccf84865aa7b0f91",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*",
+ "satooshi/php-coveralls": "0.*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Base32\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Riesen",
+ "email": "chris.riesen@gmail.com",
+ "homepage": "http://christianriesen.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "Base32 encoder/decoder according to RFC 4648",
+ "homepage": "https://github.com/ChristianRiesen/base32",
+ "keywords": [
+ "base32",
+ "decode",
+ "encode",
+ "rfc4648"
+ ],
+ "time": "2015-02-12 09:08:33"
+ },
+ {
+ "name": "christian-riesen/otp",
+ "version": "1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ChristianRiesen/otp.git",
+ "reference": "a209b8bbd975d96d6b5287f8658562061adef1f8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ChristianRiesen/otp/zipball/a209b8bbd975d96d6b5287f8658562061adef1f8",
+ "reference": "a209b8bbd975d96d6b5287f8658562061adef1f8",
+ "shasum": ""
+ },
+ "require": {
+ "christian-riesen/base32": ">=1.0",
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Otp": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christian Riesen",
+ "email": "chris.riesen@gmail.com",
+ "homepage": "http://christianriesen.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "One Time Passwords, hotp and totp according to RFC4226 and RFC6238",
+ "homepage": "https://github.com/ChristianRiesen/otp",
+ "keywords": [
+ "googleauthenticator",
+ "hotp",
+ "otp",
+ "rfc4226",
+ "rfc6238",
+ "totp"
+ ],
+ "time": "2015-02-12 09:11:49"
+ },
+ {
"name": "erusev/parsedown",
"version": "1.5.1",
"source": {
@@ -571,6 +673,7 @@
},
"prefer-stable": false,
"platform": {
+ "php": ">=5.3",
"ext-mbstring": "*"
},
"platform-dev": []
diff --git a/docs/2fa.markdown b/docs/2fa.markdown
new file mode 100644
index 00000000..627c636a
--- /dev/null
+++ b/docs/2fa.markdown
@@ -0,0 +1,33 @@
+Two factor authentication
+=========================
+
+Each user can enable the [two factor authentication](http://en.wikipedia.org/wiki/Two_factor_authentication).
+After a successful login, a one-time code (6 characters) is asked to the user to allow the access to Kanboard.
+
+This code have to be provided by a compatible software generally installed on your smartphone.
+
+Kanboard use the [Time-based One-time Password Algorithm](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) defined in the [RFC 6238](http://tools.ietf.org/html/rfc6238).
+
+There are many software compatible with the standard TOTP system.
+By example, you can use these free and open source applications:
+
+- [Google Authenticator](https://github.com/google/google-authenticator/) (Android, iOS, Blackberry)
+- [FreeOTP](https://fedorahosted.org/freeotp/) (Android, iOS)
+- [OATH Toolkit](http://www.nongnu.org/oath-toolkit/) (Command line utility on Unix/Linux)
+
+This system can work offline and you don't necessary need to have a mobile phone.
+
+Setup
+-----
+
+1. Go to your user profile
+2. On the left, click on **Two factor authentication** and check the box
+3. A secret key is generated for you
+
+![2FA](http://kanboard.net/screenshots/documentation/2fa.png)
+
+- You have to save the secret key in your TOTP software. If you use a smartphone, the easiest solution is to scan the QR code with FreeOTP or Google Authenticator.
+- Each time you will open a new session, a new code will be asked
+- Don't forget to test your device before closing your session
+
+A new secret key is generated each time you enable/disable this feature. \ No newline at end of file