From 3ab9b358e52ca582e59030009fab160e71fa1fd3 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 28 Sep 2018 11:14:19 -0400 Subject: [PATCH] Update ProcessLogin to support a configuration option to specify roles that should be prompted to enable two-factor authentication --- wire/core/Tfa.php | 19 +++- wire/core/admin.php | 38 +++++-- .../Process/ProcessLogin/ProcessLogin.module | 101 +++++++++++++++++- 3 files changed, 143 insertions(+), 15 deletions(-) diff --git a/wire/core/Tfa.php b/wire/core/Tfa.php index 6ac093b6..86af4730 100644 --- a/wire/core/Tfa.php +++ b/wire/core/Tfa.php @@ -323,7 +323,9 @@ class Tfa extends WireData implements Module, ConfigurableModule { $f->attr('id', 'Inputfield_login_submit'); $form->add($f); - $form->appendMarkup = "

" . $this->_('Cancel') . "

"; + $form->appendMarkup = + "

" . $this->_('Cancel') . "

" . + ""; $this->authCodeForm = $form; return $form; @@ -809,17 +811,20 @@ class Tfa extends WireData implements Module, ConfigurableModule { /** @var Modules $modules */ $modules = $event->wire('modules'); + /** @var Sanitizer $sanitizer */ + $sanitizer = $this->wire('sanitizer'); $user = $this->getUser(); if($user->isGuest()) { $inputfield->val(0); return; } - + + $tfaTitle = $modules->getModuleInfoProperty($this, 'title'); $settings = $this->getUserSettings($user); $enabled = $this->enabledForUser($user, $settings); $fieldset = $modules->get('InputfieldFieldset'); - $fieldset->label = $modules->getModuleInfoProperty($this, 'title'); + $fieldset->label = $tfaTitle; $fieldset->showIf = "$this->userFieldName=" . $this->className(); if($enabled) { @@ -829,11 +834,17 @@ class Tfa extends WireData implements Module, ConfigurableModule { $this->_('Two factor authentication enabled!') . ' ' . $this->_('To disable or change settings, select the “None” option above and save.'); $fieldset->collapsed = Inputfield::collapsedYes; + $this->wire('session')->removeFor('_user', 'requireTfa'); // set by ProcessLogin } else { /** @var InputfieldFieldset $fieldset */ $this->getUserSettingsInputfields($user, $fieldset, $settings); if(!$this->wire('input')->requestMethod('POST')) { - $this->warning($this->_('Please configure your two-factor authentication settings')); + $this->warning( + '' . $sanitizer->entities1($this->_('Please configure')) . ' ' . + wireIconMarkup('angle-right') . ' ' . + "" . $sanitizer->entities1($tfaTitle) . "", + Notice::allowMarkup + ); } } diff --git a/wire/core/admin.php b/wire/core/admin.php index e10d0ca0..57dd53e6 100644 --- a/wire/core/admin.php +++ b/wire/core/admin.php @@ -6,7 +6,7 @@ * This file is designed for inclusion by /site/templates/admin.php template and all the variables * it references are from your template namespace. * - * Copyright 2016 by Ryan Cramer + * Copyright 2018 by Ryan Cramer * * @var Config $config * @var User $user @@ -66,6 +66,25 @@ function _checkForHttpHostError(Config $config) { ); } +/** + * Check if two factor authentication is being required and display warning with link to configure + * + * @param Session $session + * + */ +function _checkForTwoFactorAuth(Session $session) { + $tfaUrl = $session->getFor('_user', 'requireTfa'); // contains URL to configure TFA + if(!$tfaUrl || strpos($tfaUrl, $session->wire('page')->url()) === 0) return; + $sanitizer = $session->wire('sanitizer'); + $session->wire('user')->warning( + '' . $sanitizer->entities1(__('Action required')) . ' ' . + wireIconMarkup('angle-right') . ' ' . + "" . $sanitizer->entities1(__('Enable two-factor authentication')) . " ", + Notice::allowMarkup + ); +} + + // notify superuser if there is an http host error if($user->isSuperuser()) _checkForHttpHostError($config); @@ -81,17 +100,20 @@ $breadcrumbs = $wire->wire('breadcrumbs', new Breadcrumbs()); foreach($page->parents() as $p) { if($p->id > 1) $breadcrumbs->add(new Breadcrumb($p->url, $p->get("title|name"))); } + $controller = null; $content = ''; - +$ajax = $config->ajax; +$modal = $input->get('modal') ? true : false; +$demo = $config->demo; // enable modules to output their own ajax responses if they choose to -if($config->ajax) ob_start(); +if($ajax) ob_start(); if($page->process && $page->process != 'ProcessPageView') { try { - if($config->demo && !in_array($page->process, array('ProcessLogin'))) { + if($demo && !in_array($page->process, array('ProcessLogin'))) { if(count($_POST)) $wire->error("Features that use POST variables are disabled in this demo"); foreach($_POST as $k => $v) unset($_POST[$k]); foreach($_FILES as $k => $v) unset($_FILES[$k]); @@ -109,9 +131,12 @@ if($page->process && $page->process != 'ProcessPageView') { /** @noinspection PhpIncludeInspection */ include_once($initFile); } - if($input->get('modal')) $session->addHookBefore('redirect', null, '_hookSessionRedirectModal'); + if($modal) $session->addHookBefore('redirect', null, '_hookSessionRedirectModal'); $content = $controller->execute(); $process = $controller->wire('process'); + + if(!$ajax && !$modal && !$demo && $user->isLoggedin()) _checkForTwoFactorAuth($session); + if($process) {} // ignore } catch(Wire404Exception $e) { $wire->error($e->getMessage()); @@ -154,7 +179,7 @@ if($page->process && $page->process != 'ProcessPageView') { $content = '

' . __('This page has no process assigned.') . '

'; } -if($config->ajax) { +if($ajax) { // enable modules to output their own ajax responses if they choose to if(!$content) $content = ob_get_contents(); ob_end_clean(); @@ -175,5 +200,6 @@ if($controller && $controller->isAjax()) { /** @noinspection PhpIncludeInspection */ require($adminThemeFile); $session->removeNotices(); + if($content) {} // ignore } diff --git a/wire/modules/Process/ProcessLogin/ProcessLogin.module b/wire/modules/Process/ProcessLogin/ProcessLogin.module index cd64eed9..756d547f 100644 --- a/wire/modules/Process/ProcessLogin/ProcessLogin.module +++ b/wire/modules/Process/ProcessLogin/ProcessLogin.module @@ -11,7 +11,8 @@ * ProcessWire 3.x, Copyright 2018 by Ryan Cramer * https://processwire.com * - * @property bool $allowForgot Whether the ProcessForgotPassword module is installed. + * @property bool $allowForgot Whether the ProcessForgotPassword module is installed. + * @property array $tfaRecRoleIDs Role IDs where admin prompts/recommends them to enable TFA. * * @method void beforeLogin() #pw-hooker * @method void afterLogin() #pw-hooker @@ -24,16 +25,17 @@ * @method void login($name, $pass) #pw-hooker * @method void loginFailed($name) #pw-hooker * @method void loginSuccess(User $user) #pw-hooker + * * */ -class ProcessLogin extends Process { +class ProcessLogin extends Process implements ConfigurableModule { public static function getModuleInfo() { return array( 'title' => 'Login', 'summary' => 'Login to ProcessWire', - 'version' => 105, + 'version' => 106, 'permanent' => true, 'permission' => 'page-view', ); @@ -93,6 +95,23 @@ class ProcessLogin extends Process { */ protected $logoutURL = ''; + /** + * Did user login with two factor authentication? + * + * @var bool + * + */ + protected $tfaLoginSuccess = false; + + /** + * Construct + * + */ + public function __construct() { + $this->set('tfaRecRoleIDs', array()); + parent::__construct(); + } + /** * Build the login form * @@ -178,6 +197,7 @@ class ProcessLogin extends Process { if($tfa && $tfa->active()) { // two factor authentication if($tfa->success()) { + $this->tfaLoginSuccess = true; $this->loginSuccess($this->wire('user')); $this->afterLoginRedirect('./'); } else { @@ -601,19 +621,90 @@ class ProcessLogin extends Process { * */ protected function ___loginSuccess(User $user) { + /** @var Session $session */ $session = $this->wire('session'); - $this->wire('session')->message($user->name . ' - ' . $this->_("Successful login")); + $session->message($user->name . ' - ' . $this->_("Successful login")); + if($this->isAdmin) { $copyVars = $session->getFor($this, 'copyVars'); if(!is_array($copyVars)) $copyVars = array(); foreach($copyVars as $key => $value) { $session->set($key, $value); } + $session->remove('error'); $session->removeFor($this, 'copyVars'); - $this->afterLogin(); } + + if(count($this->tfaRecRoleIDs) && !$this->tfaLoginSuccess) { + // determine if Tfa module is installed and user has role requiring Tfa + $requireTfa = false; + if(count($this->wire('modules')->findByPrefix('Tfa'))) { + foreach($this->tfaRecRoleIDs as $roleID) { + $role = $this->wire('roles')->get((int) $roleID); + if($role && $user->hasRole($role)) { + $requireTfa = true; + break; + } + } + } + if($requireTfa) { + $url = $this->wire('config')->urls('admin') . 'profile/#wrap_Inputfield_tfa_type'; + $session->setFor('_user', 'requireTfa', $url); + } + } + + if($this->isAdmin) $this->afterLogin(); + } + + /** + * Configure module settings + * + * @param InputfieldWrapper $inputfields + * + */ + public function getModuleConfigInputfields(InputfieldWrapper $inputfields) { + + /** @var Modules $modules */ + $modules = $this->wire('modules'); + + /** @var InputfieldFieldset $fieldset */ + $fieldset = $modules->get('InputfieldFieldset'); + $fieldset->label = $this->_('Two-factor authentication'); + $fieldset->icon = 'user-secret'; + $inputfields->add($fieldset); + $tfaModules = $modules->findByPrefix('Tfa'); + + if(count($tfaModules)) { + $items = array(); + foreach($tfaModules as $name) { + $items[] = "[$name](" . $modules->getModuleEditUrl($name) . ")"; + } + $fieldset->description = $this->_('Found the following Tfa modules:') . ' ' . implode(', ', $items); + /** @var InputfieldCheckboxes $f */ + $f = $modules->get('InputfieldCheckboxes'); + $f->attr('name', 'tfaRecRoleIDs'); + $f->icon = 'gears'; + $f->label = $this->_('Strongly suggest two-factor authentication for these roles'); + $f->description = + $this->_('After logging in to the admin, ProcessWire will prompt users in the roles you select here to use two-factor authentication for their accounts.'); + foreach($this->wire('roles') as $role) { + if($role->name == 'guest') continue; + $f->addOption($role->id, $role->name); + } + $f->attr('value', $this->get('tfaRecRoleIDs')); + $fieldset->add($f); + + } else { + $fieldset->description = $this->_('To configure this you must first install one or more Tfa modules and then return here.'); + } + + $fieldset->appendMarkup = + "

" . + $this->_('Tfa modules in the ProcessWire modules directory') . ' ' . + wireIconMarkup('external-link') . "

"; + } }