diff options
| author | Frederic Guillot <fred@kanboard.net> | 2015-03-31 22:48:14 -0400 | 
|---|---|---|
| committer | Frederic Guillot <fred@kanboard.net> | 2015-03-31 22:48:14 -0400 | 
| commit | abeeba71672a711dab98194bb8ae751ee95e3385 (patch) | |
| tree | 26838682b1ab13611b9697d1b8900122409dbf47 | |
| parent | 5d393ed9962ebe18a162cb09b08eaea9359df2cc (diff) | |
Add two factor authentication
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 + + + +- 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  | 
