mirror of
https://github.com/processwire/processwire.git
synced 2025-08-14 10:45:54 +02:00
Upgrade the two-factor authentication system (Tfa) so that it now supports the ability to fingerprint and remember a user’s browser and other aspects, so that the user doesn't have to re-enter their TFA code on every login (optional).
This commit is contained in:
@@ -19,8 +19,39 @@ require_once(__DIR__ . '/boot.php');
|
|||||||
*
|
*
|
||||||
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
|
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
|
||||||
* https://processwire.com
|
* https://processwire.com
|
||||||
*
|
*
|
||||||
|
* Default API vars (A-Z)
|
||||||
|
* ======================
|
||||||
|
* @property AdminTheme|AdminThemeFramework|null $adminTheme
|
||||||
|
* @property WireCache $cache
|
||||||
|
* @property Config $config
|
||||||
|
* @property WireDatabasePDO $database
|
||||||
|
* @property WireDateTime $datetime
|
||||||
|
* @property Fieldgroups $fieldgroups
|
||||||
|
* @property Fields $fields
|
||||||
|
* @property Fieldtypes $fieldtypes
|
||||||
|
* @property WireFileTools $files
|
||||||
* @property Fuel $fuel
|
* @property Fuel $fuel
|
||||||
|
* @property WireHooks $hooks
|
||||||
|
* @property WireInput $input
|
||||||
|
* @property Languages $languages (present only if LanguageSupport installed)
|
||||||
|
* @property WireLog $log
|
||||||
|
* @property WireMailTools $mail
|
||||||
|
* @property Modules $modules
|
||||||
|
* @property Notices $notices
|
||||||
|
* @property Page $page
|
||||||
|
* @property Pages $pages
|
||||||
|
* @property Permissions $permissions
|
||||||
|
* @property Process|ProcessPageView $process
|
||||||
|
* @property WireProfilerInterface $profiler
|
||||||
|
* @property Roles $roles
|
||||||
|
* @property Sanitizer $sanitizer
|
||||||
|
* @property Session $session
|
||||||
|
* @property Templates $templates
|
||||||
|
* @property Paths $urls
|
||||||
|
* @property User $user
|
||||||
|
* @property Users $users
|
||||||
|
* @property ProcessWire $wire
|
||||||
*
|
*
|
||||||
* @method init()
|
* @method init()
|
||||||
* @method ready()
|
* @method ready()
|
||||||
@@ -164,9 +195,6 @@ class ProcessWire extends Wire {
|
|||||||
/**
|
/**
|
||||||
* Fuel manages ProcessWire API variables
|
* Fuel manages ProcessWire API variables
|
||||||
*
|
*
|
||||||
* This will replace the static $fuel from the Wire class in PW 3.0.
|
|
||||||
* Currently it is just here as a placeholder.
|
|
||||||
*
|
|
||||||
* @var Fuel|null
|
* @var Fuel|null
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@@ -446,9 +474,9 @@ class ProcessWire extends Wire {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$notices = new Notices();
|
$notices = new Notices();
|
||||||
|
$this->wire('notices', $notices, true); // first so any API var can send notices
|
||||||
$this->wire('urls', $config->urls); // shortcut API var
|
$this->wire('urls', $config->urls); // shortcut API var
|
||||||
$this->wire('log', new WireLog(), true);
|
$this->wire('log', new WireLog(), true);
|
||||||
$this->wire('notices', $notices, true);
|
|
||||||
$this->wire('sanitizer', new Sanitizer());
|
$this->wire('sanitizer', new Sanitizer());
|
||||||
$this->wire('datetime', new WireDateTime());
|
$this->wire('datetime', new WireDateTime());
|
||||||
$this->wire('files', new WireFileTools());
|
$this->wire('files', new WireFileTools());
|
||||||
@@ -760,11 +788,20 @@ class ProcessWire extends Wire {
|
|||||||
$this->wire($key, $value, $lock);
|
$this->wire($key, $value, $lock);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API var directly
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @return mixed
|
||||||
|
*
|
||||||
|
*/
|
||||||
public function __get($key) {
|
public function __get($key) {
|
||||||
if($key === 'fuel') return $this->fuel;
|
if($key === 'fuel') return $this->fuel;
|
||||||
if($key === 'shutdown') return $this->shutdown;
|
if($key === 'shutdown') return $this->shutdown;
|
||||||
if($key === 'instanceID') return $this->instanceID;
|
if($key === 'instanceID') return $this->instanceID;
|
||||||
|
$value = $this->fuel->get($key);
|
||||||
|
if($value !== null) return $value;
|
||||||
return parent::__get($key);
|
return parent::__get($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
*
|
*
|
||||||
* This class is for “Tfa” modules to extend. See the TfaEmail and TfaTotp modules as examples.
|
* This class is for “Tfa” modules to extend. See the TfaEmail and TfaTotp modules as examples.
|
||||||
*
|
*
|
||||||
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
|
* ProcessWire 3.x, Copyright 2020 by Ryan Cramer
|
||||||
* https://processwire.com
|
* https://processwire.com
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
@@ -36,6 +36,8 @@
|
|||||||
* @property int $codeExpire Codes expire after this many seconds (default=180)
|
* @property int $codeExpire Codes expire after this many seconds (default=180)
|
||||||
* @property int $codeType Type of TFA code to use, see codeType constants (default=0, which is Tfa::codeTypeDigits)
|
* @property int $codeType Type of TFA code to use, see codeType constants (default=0, which is Tfa::codeTypeDigits)
|
||||||
* @property string $startUrl URL we are operating from (default='./')
|
* @property string $startUrl URL we are operating from (default='./')
|
||||||
|
* @property int $rememberDays Number of days to "remember this browser", 0 to disable option, or -1 for no limit? (default=0)
|
||||||
|
* @property array $rememberFingerprints Fingerprints to remember: agent,agentVL,accept,scheme,host,ip,fwip (default=agentVL,accept,scheme,host)
|
||||||
* @property array $formAttrs Form <form> element attributes
|
* @property array $formAttrs Form <form> element attributes
|
||||||
* @property array $inputAttrs Code <input> element attributes
|
* @property array $inputAttrs Code <input> element attributes
|
||||||
* @property string $inputLabel Label for code <input> element
|
* @property string $inputLabel Label for code <input> element
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
* @property string $submitLabel Label for submit button
|
* @property string $submitLabel Label for submit button
|
||||||
* @property bool $showCancel Show a cancel link under authentication code form? (default=true)
|
* @property bool $showCancel Show a cancel link under authentication code form? (default=true)
|
||||||
* @property string $cancelLabel Label to use for Cancel link (default='Cancel', translatable)
|
* @property string $cancelLabel Label to use for Cancel link (default='Cancel', translatable)
|
||||||
|
* @property string $rememberLabel Label for "remember this browser" option
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* @method bool start($name, $pass)
|
* @method bool start($name, $pass)
|
||||||
@@ -86,11 +89,12 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
if(!$this->wire('fields')->get($this->userFieldName)) $this->install();
|
|
||||||
if($this->className() != 'Tfa') $this->initHooks();
|
|
||||||
$this->set('codeExpire', 180);
|
$this->set('codeExpire', 180);
|
||||||
$this->set('startUrl', './');
|
$this->set('startUrl', './');
|
||||||
$this->set('formAttrs', array('id' => 'ProcessLoginForm'));
|
$this->set('rememberDays', 0);
|
||||||
|
$this->set('rememberLabel', $this->_('Remember this computer?'));
|
||||||
|
$this->set('rememberFingerprints', array('agentVL', 'accept', 'scheme', 'host'));
|
||||||
|
$this->set('formAttrs', array('id' => 'ProcessLoginForm', 'class' => 'pw-tfa'));
|
||||||
$this->set('inputAttrs', array('id' => 'login_name', 'autofocus' => 'autofocus'));
|
$this->set('inputAttrs', array('id' => 'login_name', 'autofocus' => 'autofocus'));
|
||||||
$this->set('inputLabel', $this->_('Authentication Code'));
|
$this->set('inputLabel', $this->_('Authentication Code'));
|
||||||
$this->set('submitAttrs', array('id' => 'Inputfield_login_submit'));
|
$this->set('submitAttrs', array('id' => 'Inputfield_login_submit'));
|
||||||
@@ -100,6 +104,33 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when assigned to ProcessWire instance
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function wired() {
|
||||||
|
if(!$this->wire()->fields->get($this->userFieldName)) $this->install();
|
||||||
|
if($this->className() != 'Tfa') $this->initHooks();
|
||||||
|
parent::wired();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access the RememberTfa instance
|
||||||
|
*
|
||||||
|
* #pw-internal
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param array $settings
|
||||||
|
* @return RememberTfa
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function remember(User $user, array $settings) {
|
||||||
|
$remember = $this->wire(new RememberTfa($this, $user, $settings));
|
||||||
|
$remember->setDays($this->rememberDays);
|
||||||
|
$remember->setFingerprints($this->rememberFingerprints);
|
||||||
|
return $remember;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the start URL and optionally append query string
|
* Get the start URL and optionally append query string
|
||||||
*
|
*
|
||||||
@@ -132,8 +163,8 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
*
|
*
|
||||||
* On successful authentication of user, this method performs a redirect to the next step.
|
* On successful authentication of user, this method performs a redirect to the next step.
|
||||||
* If user does not exist, they are not allowed to login, or fails to authenticate, this method
|
* If user does not exist, they are not allowed to login, or fails to authenticate, this method
|
||||||
* returns a boolean false. If user authenticates but simply does not have 2FA enabled,
|
* returns a boolean false. If user does not have 2FA enabled, or is remembered from a previous
|
||||||
* then this method returns true.
|
* TFA login, then this method returns true, but user still needs to be authenticated.
|
||||||
*
|
*
|
||||||
* If preferred, you can ignore the return value, as this method will perform redirects whenever
|
* If preferred, you can ignore the return value, as this method will perform redirects whenever
|
||||||
* it needs to move on to the next 2FA step.
|
* it needs to move on to the next 2FA step.
|
||||||
@@ -153,7 +184,7 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
$this->sessionReset();
|
$this->sessionReset();
|
||||||
|
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
$user = $this->wire('users')->get("name=" . $sanitizer->selectorValue($name));
|
$user = $this->wire()->users->get('name=' . $sanitizer->selectorValue($name));
|
||||||
|
|
||||||
// unknown user
|
// unknown user
|
||||||
if(!$user || !$user->id) return false;
|
if(!$user || !$user->id) return false;
|
||||||
@@ -162,14 +193,24 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
if(!$session->allowLogin($user->name, $user)) return false;
|
if(!$session->allowLogin($user->name, $user)) return false;
|
||||||
|
|
||||||
// check if user exists but does not have 2FA enabled
|
// check if user exists but does not have 2FA enabled
|
||||||
$tfaModule = $user->get($this->userFieldName);
|
$tfaModule = $this->getModule($user); /** @var Tfa $tfaModule */
|
||||||
if(!$tfaModule) return true;
|
if(!$tfaModule) return true;
|
||||||
|
|
||||||
$settings = $tfaModule->getUserSettings($user);
|
$settings = $tfaModule->getUserSettings($user);
|
||||||
if(!$tfaModule->enabledForUser($user, $settings)) return true;
|
if(!$tfaModule->enabledForUser($user, $settings)) return true;
|
||||||
|
|
||||||
// check if user name and pass authenticate
|
// check if user name and pass authenticate
|
||||||
if(!$session->authenticate($user, $pass)) return false;
|
if(!$session->authenticate($user, $pass)) return false;
|
||||||
|
|
||||||
|
if($tfaModule->rememberDays && $tfaModule->remember($user, $settings)->remembered()) {
|
||||||
|
if($this->wire()->config->debug) {
|
||||||
|
$this->message(
|
||||||
|
$this->_('Code was not required because the browser was recognized from a previous login.'),
|
||||||
|
Notice::noGroup
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// at this point user has successfully authenticated with given name and pass
|
// at this point user has successfully authenticated with given name and pass
|
||||||
if($tfaModule->startUser($user, $settings)) {
|
if($tfaModule->startUser($user, $settings)) {
|
||||||
@@ -255,6 +296,48 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
return $this->wire('input')->get($this->keyName) === $this->getSessionKey();
|
return $this->wire('input')->get($this->keyName) === $this->getSessionKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Programmatically enable this TFA module for given user
|
||||||
|
*
|
||||||
|
* This can only be used if the Tfa module supports it (i.e. TfaEMail does but TfaTotp cannot).
|
||||||
|
* Returns false when not supported by the module. When a module supports it, it should include
|
||||||
|
* a parent::enableForUser() call that includes the $settings argument.
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param array $settings For internal use, should only be specified on a parent::setEnabledForUser() call.
|
||||||
|
* @return bool
|
||||||
|
* @since 3.0.159
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function enableForUser(User $user, array $settings = array()) {
|
||||||
|
$moduleName = $this->className();
|
||||||
|
if($moduleName === 'Tfa') throw new WireException('This method may only be called on a Tfa module');
|
||||||
|
if(empty($settings)) return false; // module lacks auto-configure support for this
|
||||||
|
$user->setAndSave($this->userFieldName, $moduleName);
|
||||||
|
$settings['enabled'] = true;
|
||||||
|
$this->saveUserSettings($user, $settings);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Programmatically disable this TFA module for given user
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @return bool
|
||||||
|
* @throws WireException
|
||||||
|
* @since 3.0.159
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function disableForUser(User $user) {
|
||||||
|
$moduleName = $this->className();
|
||||||
|
if($moduleName === 'Tfa') throw new WireException('This method may only be called on a Tfa module');
|
||||||
|
if($user->get($this->userFieldName) != $moduleName) return false;
|
||||||
|
$user->setAndSave($this->userFieldName, '');
|
||||||
|
$settings = array('enabled' => false);
|
||||||
|
$this->saveUserSettings($user, $settings);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is TFA enabled for given user?
|
* Is TFA enabled for given user?
|
||||||
*
|
*
|
||||||
@@ -271,7 +354,7 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
$enabled = empty($settings['enabled']) ? false : true;
|
$enabled = empty($settings['enabled']) ? false : true;
|
||||||
return $enabled;
|
return $enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when TFA has successfully completed and user is now logged in
|
* Returns true when TFA has successfully completed and user is now logged in
|
||||||
*
|
*
|
||||||
@@ -299,16 +382,30 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public function getModule(User $user = null) {
|
public function getModule(User $user = null) {
|
||||||
if($user) return $user->get($this->userFieldName);
|
|
||||||
|
$module = null;
|
||||||
$moduleName = $this->sessionGet('type');
|
$moduleName = $this->sessionGet('type');
|
||||||
if($moduleName) {
|
|
||||||
if($moduleName === $this->className()) return $this;
|
if($user) {
|
||||||
return $this->wire('modules')->getModule($moduleName);
|
$module = $user->get($this->userFieldName);
|
||||||
|
} else if($moduleName && $moduleName === $this->className()) {
|
||||||
|
$module = $this;
|
||||||
|
} else if($moduleName) {
|
||||||
|
$module = $this->wire()->modules->getModule($moduleName);
|
||||||
} else {
|
} else {
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
if($user) return $user->get($this->userFieldName);
|
if($user) $module = $user->get($this->userFieldName);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
if($module && !$module instanceof Tfa) $module = null;
|
||||||
|
|
||||||
|
if($module) {
|
||||||
|
/** @var Tfa $module */
|
||||||
|
$module->rememberDays = $this->rememberDays;
|
||||||
|
$module->rememberFingerprints = $this->rememberFingerprints;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $module;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -364,6 +461,15 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
$f->attr('required', 'required');
|
$f->attr('required', 'required');
|
||||||
$f->collapsed = Inputfield::collapsedNever;
|
$f->collapsed = Inputfield::collapsedNever;
|
||||||
$form->add($f);
|
$form->add($f);
|
||||||
|
|
||||||
|
if($this->rememberDays) {
|
||||||
|
$f = $modules->get('InputfieldCheckbox');
|
||||||
|
$f->attr('id+name', 'tfa_remember');
|
||||||
|
$f->attr('value', $this->rememberDays);
|
||||||
|
$f->label = $this->rememberLabel;
|
||||||
|
$f->collapsed = Inputfield::collapsedNever;
|
||||||
|
$form->add($f);
|
||||||
|
}
|
||||||
|
|
||||||
/** @var InputfieldSubmit $f */
|
/** @var InputfieldSubmit $f */
|
||||||
$f = $modules->get('InputfieldSubmit');
|
$f = $modules->get('InputfieldSubmit');
|
||||||
@@ -424,7 +530,10 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
|
|
||||||
/** @var Session $session */
|
/** @var Session $session */
|
||||||
$session = $this->wire('session');
|
$session = $this->wire('session');
|
||||||
|
|
||||||
|
/** @var Config $config */
|
||||||
|
$config = $this->wire('config');
|
||||||
|
|
||||||
/** @var string|null $key */
|
/** @var string|null $key */
|
||||||
$key = $input->get($this->keyName);
|
$key = $input->get($this->keyName);
|
||||||
|
|
||||||
@@ -465,21 +574,41 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
// code submitted: validate code and set to blank if not valid format
|
// code submitted: validate code and set to blank if not valid format
|
||||||
$form->processInput($input->post);
|
$form->processInput($input->post);
|
||||||
$code = $form->getChildByName('tfa_code')->val();
|
$code = $form->getChildByName('tfa_code')->val();
|
||||||
|
$codeHash = sha1($code . $user->id . substr($config->userAuthSalt, 0, 9));
|
||||||
|
|
||||||
// at this point, a code has been submitted
|
// at this point, a code has been submitted
|
||||||
$this->sessionSet('tries', ++$numTries);
|
$this->sessionSet('tries', ++$numTries);
|
||||||
|
|
||||||
|
|
||||||
// validate code
|
// validate code
|
||||||
$settings = $this->getUserSettings($user);
|
$settings = $this->getUserSettings($user);
|
||||||
$valid = $this->isValidUserCode($user, $code, $settings);
|
$valid = $this->isValidUserCode($user, $code, $settings);
|
||||||
|
|
||||||
|
if($valid === true && isset($settings['last_code'])) {
|
||||||
|
if($codeHash === $settings['last_code']) {
|
||||||
|
// do not allow same code to be reused, just in case traffic is being intercepted
|
||||||
|
// and chosen Tfa module does not already handle this case
|
||||||
|
$valid = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if($valid === true) {
|
if($valid === true) {
|
||||||
// code is validated, so do a forced login since user is already authenticated
|
// code is validated, so do a forced login since user is already authenticated
|
||||||
$this->sessionReset();
|
$this->sessionReset();
|
||||||
$user = $session->forceLogin($user);
|
$user = $session->forceLogin($user);
|
||||||
if($user && $user->id && $user->id == $userID) {
|
if($user && $user->id && "$user->id" === "$userID") {
|
||||||
// code successfully validated and user is now logged in
|
// code successfully validated and user is now logged in
|
||||||
|
$settings = $this->getUserSettings($user); // get fresh
|
||||||
|
$settings['last_code'] = $codeHash;
|
||||||
|
if($this->rememberDays && $input->post('tfa_remember') == $this->rememberDays) {
|
||||||
|
if($this->remember($user, $settings)->enable()) {
|
||||||
|
$this->message(
|
||||||
|
sprintf($this->_('This computer/browser is now remembered for up to %d days.'), $this->rememberDays) . ' ' .
|
||||||
|
$this->_('Changes to browser and/or location may require a new code.'),
|
||||||
|
Notice::noGroup);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->saveUserSettings($user, $settings);
|
||||||
|
}
|
||||||
return $user;
|
return $user;
|
||||||
} else {
|
} else {
|
||||||
// not likely for login to fail here, since they were already authenticated before
|
// not likely for login to fail here, since they were already authenticated before
|
||||||
@@ -611,7 +740,7 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
protected function getDefaultUserSettings(User $user) {
|
protected function getDefaultUserSettings(User $user) {
|
||||||
if($user) {}
|
if($user) {}
|
||||||
return array(
|
return array(
|
||||||
'enabled' => false // whether user has this auth method enabled
|
'enabled' => false, // whether user has this auth method enabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -780,6 +909,13 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
if(!$this->enabledForUser($user, $settings)) {
|
if(!$this->enabledForUser($user, $settings)) {
|
||||||
$this->getUserSettingsInputfields($user, $fieldset, $settings);
|
$this->getUserSettingsInputfields($user, $fieldset, $settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($this->wire()->input->post('_tfa_clear_remember')) {
|
||||||
|
unset($settings['remember']);
|
||||||
|
$changes['remember'] = 'remember';
|
||||||
|
$this->remember($user, $settings)->disableAll();
|
||||||
|
$this->message($this->_('Cleared remembered browsers'));
|
||||||
|
}
|
||||||
|
|
||||||
foreach($fieldset->getAll() as $f) {
|
foreach($fieldset->getAll() as $f) {
|
||||||
$name = $f->attr('name');
|
$name = $f->attr('name');
|
||||||
@@ -896,6 +1032,15 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
$this->_('Two factor authentication enabled!') . ' ' .
|
$this->_('Two factor authentication enabled!') . ' ' .
|
||||||
$this->_('To disable or change settings, select the “None” option above and save.');
|
$this->_('To disable or change settings, select the “None” option above and save.');
|
||||||
$fieldset->collapsed = Inputfield::collapsedYes;
|
$fieldset->collapsed = Inputfield::collapsedYes;
|
||||||
|
|
||||||
|
if(!empty($settings['remember'])) {
|
||||||
|
/** @var InputfieldCheckbox $f */
|
||||||
|
$f = $modules->get('InputfieldCheckbox');
|
||||||
|
$f->attr('name', '_tfa_clear_remember');
|
||||||
|
$f->label = $this->_('Clear remembered browsers that skip entering authentication code');
|
||||||
|
$fieldset->add($f);
|
||||||
|
}
|
||||||
|
|
||||||
$this->wire('session')->removeFor('_user', 'requireTfa'); // set by ProcessLogin
|
$this->wire('session')->removeFor('_user', 'requireTfa'); // set by ProcessLogin
|
||||||
} else {
|
} else {
|
||||||
/** @var InputfieldFieldset $fieldset */
|
/** @var InputfieldFieldset $fieldset */
|
||||||
@@ -915,7 +1060,7 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
foreach($fieldset->getAll() as $f) {
|
foreach($fieldset->getAll() as $f) {
|
||||||
$name = $f->attr('name');
|
$name = $f->attr('name');
|
||||||
if(isset($settings[$name])) $f->val($settings[$name]);
|
if(isset($settings[$name])) $f->val($settings[$name]);
|
||||||
$f->attr('name', "_tfa_$name");
|
if(strpos($name, '_tfa_') !== 0) $f->attr('name', "_tfa_$name");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1012,4 +1157,466 @@ class Tfa extends WireData implements Module, ConfigurableModule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the “remember me” feature for Tfa class
|
||||||
|
*
|
||||||
|
* Accessed from $tfaInstance->remember($user, $settings)->method().
|
||||||
|
* This class is kept in Tfa.php because it cannot be instantiated without
|
||||||
|
* a Tfa instance.
|
||||||
|
*
|
||||||
|
* @method array getFingerprintArray($getLabels = false)
|
||||||
|
*
|
||||||
|
* #pw-internal
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class RememberTfa extends Wire {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows debug info in warning messages, only for development
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const debug = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max browsers to remember for any user
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const maxItems = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Tfa
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected $tfa;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var User|null
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected $user = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected $settings = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected $remember = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Days to remember
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected $days = 90;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Means by which to fingerprint user (extras on top of random remembered cookie)
|
||||||
|
*
|
||||||
|
* Options: agent, agentVL, accept, scheme, host, ip, fwip
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected $fingerprints = array(
|
||||||
|
'agentVL',
|
||||||
|
'accept',
|
||||||
|
'scheme',
|
||||||
|
'host',
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct
|
||||||
|
*
|
||||||
|
* @param User $user
|
||||||
|
* @param Tfa $tfa
|
||||||
|
* @param array $settings
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function __construct(Tfa $tfa, User $user, array $settings) {
|
||||||
|
$this->tfa = $tfa;
|
||||||
|
$tfa->wire($this);
|
||||||
|
$this->user = $user;
|
||||||
|
$this->settings = $settings;
|
||||||
|
if(isset($settings['remember'])) $this->remember = $settings['remember'];
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set days to remember between logins
|
||||||
|
*
|
||||||
|
* @param int $days
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function setDays($days) {
|
||||||
|
$this->days = (int) $days;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fingerprints to use for newly created "remember" items
|
||||||
|
*
|
||||||
|
* @param array $fingerprints
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function setFingerprints(array $fingerprints) {
|
||||||
|
$this->fingerprints = $fingerprints;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save Tfa 'remember' settings
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function saveRemember() {
|
||||||
|
if(count($this->remember)) {
|
||||||
|
$this->settings['remember'] = $this->remember;
|
||||||
|
} else {
|
||||||
|
unset($this->settings['remember']);
|
||||||
|
}
|
||||||
|
return $this->tfa->saveUserSettings($this->user, $this->settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set combination of user/browser/host/page as remembered and allowed to skip TFA
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function enable() {
|
||||||
|
|
||||||
|
if(!$this->days) return false;
|
||||||
|
|
||||||
|
$rand = new WireRandom();
|
||||||
|
$this->wire($rand);
|
||||||
|
$cookieValue = $rand->alphanumeric(0, array('minLength' => 40, 'maxLength' => 256));
|
||||||
|
$qty = count($this->remember);
|
||||||
|
|
||||||
|
if($qty > self::maxItems) {
|
||||||
|
$this->remember = array_slice($this->remember, $qty - self::maxItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
$name = $rand->alpha(0, array('minLength' => 3, 'maxLength' => 7));
|
||||||
|
} while(isset($this->remember[$name]) || $this->getCookie($name) !== null);
|
||||||
|
|
||||||
|
$this->remember[$name] = array(
|
||||||
|
'fingerprint' => $this->getFingerprintString(),
|
||||||
|
'created' => time(),
|
||||||
|
'expires' => strtotime("+$this->days DAYS"),
|
||||||
|
'value' => $this->serverValue($cookieValue),
|
||||||
|
'page' => $this->wire()->page->id,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->debugNote("Enabled new remember: $name");
|
||||||
|
$this->debugNote($this->remember[$name]);
|
||||||
|
|
||||||
|
$result = $this->saveRemember();
|
||||||
|
if($result) $this->setCookie($name, $cookieValue);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is current user/browser/host/URL one that is remembered and TFA can be skipped?
|
||||||
|
*
|
||||||
|
* @param bool $getName Return remembered cookie name rather than true? (default=false)
|
||||||
|
* @return bool|string
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function remembered($getName = false) {
|
||||||
|
|
||||||
|
if(!$this->days) return false;
|
||||||
|
|
||||||
|
$page = $this->wire()->page;
|
||||||
|
$fingerprint = $this->getFingerprintString();
|
||||||
|
$valid = false;
|
||||||
|
$validName = '';
|
||||||
|
$disableNames = array();
|
||||||
|
|
||||||
|
foreach($this->remember as $name => $item) {
|
||||||
|
|
||||||
|
// skip any that do not match current page
|
||||||
|
if("$item[page]" !== "$page->id") {
|
||||||
|
$this->debugNote("Skipping $name because page: $item[page] != $page->id");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($item['expires']) && time() >= $item['expires']) {
|
||||||
|
$this->debugNote("Skipping $name because it has expired (expires=$item[expires])");
|
||||||
|
$disableNames[] = $name;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cookieValue = $this->getCookie($name);
|
||||||
|
|
||||||
|
// skip any where cookie value isn't present
|
||||||
|
if(empty($cookieValue)) {
|
||||||
|
// if cookie not present on this browser skip it because likely for another browser the user has
|
||||||
|
$this->debugNote("Skipping $name because cookie not present");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip and remove any that do not match current browser fingerprint
|
||||||
|
if(!$this->fingerprintStringMatches($item['fingerprint'])) {
|
||||||
|
$fingerprintTypes = $this->getFingerprintTypes($item['fingerprint']);
|
||||||
|
if(!isset($fingerprintTypes['ip']) && !isset($fingerprintTypes['fwip'])) {
|
||||||
|
// if IP isn't part of fingerprint then it is okay to remove this entry because browser can no longer match
|
||||||
|
$disableNames[] = $name;
|
||||||
|
}
|
||||||
|
$this->debugNote("Skipping $name because fingerprint: $item[fingerprint] != $fingerprint");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cookie found, now validate it
|
||||||
|
$valid = $item['value'] === $this->serverValue($cookieValue);
|
||||||
|
|
||||||
|
if($valid) {
|
||||||
|
// cookie is valid, now refresh it, resetting its expiration
|
||||||
|
$this->debugNote("Valid remember: $name");
|
||||||
|
$this->setCookie($name, $cookieValue);
|
||||||
|
$validName = $name;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// clear because cookie value populated but is not correct
|
||||||
|
$this->debugNote("Skipping $name because cookie does not authenticate with server value");
|
||||||
|
$disableNames[] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(count($disableNames)) $this->disable($disableNames);
|
||||||
|
|
||||||
|
return ($getName && $valid ? $validName : $valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable one or more cookie/remembered client by name(s)
|
||||||
|
*
|
||||||
|
* @param array|string $names
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function disable($names) {
|
||||||
|
if(!is_array($names)) $names = array($names);
|
||||||
|
$qty = 0;
|
||||||
|
foreach($names as $name) {
|
||||||
|
$found = isset($this->remember[$name]);
|
||||||
|
if($found) unset($this->remember[$name]);
|
||||||
|
if($this->clearCookie($name)) $found = true;
|
||||||
|
if($found) $qty++;
|
||||||
|
if($found) $this->debugNote("Disabling: $name");
|
||||||
|
}
|
||||||
|
if($qty) $this->saveRemember();
|
||||||
|
return $qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable all stored "remember me" data for user
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function disableAll() {
|
||||||
|
// remove cookies
|
||||||
|
foreach($this->remember as $name => $item) {
|
||||||
|
$this->clearCookie($name);
|
||||||
|
}
|
||||||
|
// remove from user settings
|
||||||
|
$this->remember = array();
|
||||||
|
$this->debugNote("Disabled all");
|
||||||
|
return $this->saveRemember();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a "remember me" cookie value
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return string|null
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function getCookie($name) {
|
||||||
|
$name = $this->cookieName($name);
|
||||||
|
return $this->wire()->input->cookie->get($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the "remember me" cookie
|
||||||
|
*
|
||||||
|
* @param string $cookieName
|
||||||
|
* @param string $cookieValue
|
||||||
|
* @return WireInputData
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function setCookie($cookieName, $cookieValue) {
|
||||||
|
$cookieOptions = array(
|
||||||
|
'age' => ($this->days > 0 ? $this->days * 86400 : 31536000),
|
||||||
|
'httponly' => true,
|
||||||
|
'domain' => '',
|
||||||
|
);
|
||||||
|
if($this->config->https) $options['secure'] = true;
|
||||||
|
$cookieName = $this->cookieName($cookieName);
|
||||||
|
$this->debugNote("Setting cookie: $cookieName=$cookieValue");
|
||||||
|
return $this->wire()->input->cookie->set($cookieName, $cookieValue, $cookieOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cookie prefix
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function cookiePrefix() {
|
||||||
|
$config = $this->wire()->config;
|
||||||
|
$cookiePrefix = $config->https ? $config->sessionNameSecure : $config->sessionName;
|
||||||
|
if(empty($cookiePrefix)) $cookiePrefix = 'wire';
|
||||||
|
return $cookiePrefix . '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given name return cookie name
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function cookieName($name) {
|
||||||
|
$prefix = $this->cookiePrefix();
|
||||||
|
if(strpos($name, $prefix) !== 0) $name = $prefix . $name;
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cookie
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function clearCookie($name) {
|
||||||
|
$name = $this->cookiePrefix() . $name;
|
||||||
|
$cookies = $this->wire()->input->cookie;
|
||||||
|
if($cookies->get($name) === null) return false;
|
||||||
|
$cookies->set($name, null, array()); // remove
|
||||||
|
$this->debugNote("Clearing cookie: $name");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a cookie value return equivalent expected server value
|
||||||
|
*
|
||||||
|
* @param string $cookieValue
|
||||||
|
* @param User|null $user
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function serverValue($cookieValue, User $user = null) {
|
||||||
|
if($user === null) $user = $this->user;
|
||||||
|
return sha1(
|
||||||
|
$user->id . $user->name . $user->email .
|
||||||
|
substr(((string) $user->pass), 0, 15) .
|
||||||
|
substr($this->wire()->config->userAuthSalt, 0, 10) .
|
||||||
|
$cookieValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fingerprint of current browser, host and URL
|
||||||
|
*
|
||||||
|
* Note that this is not guaranted unique, so is only a secondary security measure to
|
||||||
|
* ensure that remember-me record is married to an agent, scheme, and http host.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function ___getFingerprintArray() {
|
||||||
|
|
||||||
|
$agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'noagent';
|
||||||
|
|
||||||
|
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||||
|
$fwip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||||
|
} else if(isset($_SERVER['HTTP_CLIENT_IP'])) {
|
||||||
|
$fwip = $_SERVER['HTTP_CLIENT_IP'];
|
||||||
|
} else {
|
||||||
|
$fwip = 'nofwip';
|
||||||
|
}
|
||||||
|
|
||||||
|
$fingerprints = array(
|
||||||
|
'agent' => $agent,
|
||||||
|
'agentVL' => preg_replace('![^a-zA-Z]!', '', $agent), // versionless agent
|
||||||
|
'accept' => (isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : 'noaccept'),
|
||||||
|
'scheme' => ($this->config->https ? 'HTTPS' : 'http'),
|
||||||
|
'host' => $this->config->httpHost,
|
||||||
|
'ip' => (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'noip'),
|
||||||
|
'fwip' => $fwip,
|
||||||
|
);
|
||||||
|
|
||||||
|
$fingerprint = array();
|
||||||
|
|
||||||
|
foreach($this->fingerprints as $type) {
|
||||||
|
$fingerprint[$type] = $fingerprints[$type];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->debugNote($fingerprint);
|
||||||
|
|
||||||
|
return $fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fingerprint string
|
||||||
|
*
|
||||||
|
* @param array $types Fingerprints to use, or omit when creating new
|
||||||
|
* @return string
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public function getFingerprintString(array $types = null) {
|
||||||
|
if($types === null) $types = $this->fingerprints;
|
||||||
|
return implode(',', $types) . ':' . sha1(implode("\n", $this->getFingerprintArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does given fingerprint match one determined from current request?
|
||||||
|
*
|
||||||
|
* @param string $fpstr Fingerprint to compare
|
||||||
|
* @return bool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function fingerprintStringMatches($fpstr) {
|
||||||
|
$types = $this->getFingerprintTypes($fpstr);
|
||||||
|
$fpnow = $types ? $this->getFingerprintString($types) : '';
|
||||||
|
return ($fpstr && $fpnow && $fpstr === $fpnow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the types used in given fingerprint string
|
||||||
|
*
|
||||||
|
* @param string $fpstr
|
||||||
|
* @return array|bool
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function getFingerprintTypes($fpstr) {
|
||||||
|
if(strpos($fpstr, ':') === false) return false;
|
||||||
|
list($types,) = explode(':', $fpstr, 2);
|
||||||
|
$a = explode(',', $types);
|
||||||
|
$types = array();
|
||||||
|
foreach($a as $type) $types[$type] = $type;
|
||||||
|
return $types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display debug note
|
||||||
|
*
|
||||||
|
* @param string|array $note
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
protected function debugNote($note) {
|
||||||
|
if(self::debug) $this->warning($note);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -818,17 +818,7 @@ class WireRandom extends Wire {
|
|||||||
} else {
|
} else {
|
||||||
// for password use, slower
|
// for password use, slower
|
||||||
$rawLength = (int) ($requiredLength * 3 / 4 + 1);
|
$rawLength = (int) ($requiredLength * 3 / 4 + 1);
|
||||||
|
|
||||||
// mcrypt_create_iv
|
|
||||||
if((!$valid || $test) && function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
|
|
||||||
// @operator added for PHP 7.1 which throws deprecated notice on this function call
|
|
||||||
$buffer = @mcrypt_create_iv($rawLength, MCRYPT_DEV_URANDOM);
|
|
||||||
if($buffer) $valid = true;
|
|
||||||
if($test) $tests['mcrypt_create_iv'] = $buffer;
|
|
||||||
} else if($test) {
|
|
||||||
$tests['mcrypt_create_iv'] = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// PHP7 random_bytes
|
// PHP7 random_bytes
|
||||||
if((!$valid || $test) && function_exists('random_bytes')) {
|
if((!$valid || $test) && function_exists('random_bytes')) {
|
||||||
try {
|
try {
|
||||||
@@ -842,6 +832,16 @@ class WireRandom extends Wire {
|
|||||||
$tests['random_bytes'] = '';
|
$tests['random_bytes'] = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mcrypt_create_iv
|
||||||
|
if((!$valid || $test) && function_exists('mcrypt_create_iv') && !defined('PHALANGER')) {
|
||||||
|
// @operator added for PHP 7.1 which throws deprecated notice on this function call
|
||||||
|
$buffer = @mcrypt_create_iv($rawLength, MCRYPT_DEV_URANDOM);
|
||||||
|
if($buffer) $valid = true;
|
||||||
|
if($test) $tests['mcrypt_create_iv'] = $buffer;
|
||||||
|
} else if($test) {
|
||||||
|
$tests['mcrypt_create_iv'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
// openssl_random_pseudo_bytes
|
// openssl_random_pseudo_bytes
|
||||||
if((!$valid || $test) && function_exists('openssl_random_pseudo_bytes')) {
|
if((!$valid || $test) && function_exists('openssl_random_pseudo_bytes')) {
|
||||||
$good = false;
|
$good = false;
|
||||||
|
@@ -16,3 +16,19 @@ ul.Inputfields .InputfieldSubmit {
|
|||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#wrap_tfa_remember {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#wrap_tfa_remember .InputfieldHeader {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#wrap_tfa_remember .InputfieldContent {
|
||||||
|
padding: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProcessLogin #ProcessLoginForm.tfa {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,8 @@
|
|||||||
* @property bool|int $allowEmail Whether or not email login is allowed (0|false=off, 1|true=Yes, 2=Yes or name also allowed)
|
* @property bool|int $allowEmail Whether or not email login is allowed (0|false=off, 1|true=Yes, 2=Yes or name also allowed)
|
||||||
* @property string $emailField Field name used for email login (when enabled).
|
* @property string $emailField Field name used for email login (when enabled).
|
||||||
* @property array $tfaRecRoleIDs Role IDs where admin prompts/recommends them to enable TFA.
|
* @property array $tfaRecRoleIDs Role IDs where admin prompts/recommends them to enable TFA.
|
||||||
|
* @property int $tfaRememberDays Allow user to remember their browser and bypass TFA for this many days (-1=no limit, 0=disabled)
|
||||||
|
* @property array $tfaRememberFingerprints Means by which to fingerprint user’s browser
|
||||||
*
|
*
|
||||||
* @method void beforeLogin() #pw-hooker
|
* @method void beforeLogin() #pw-hooker
|
||||||
* @method void afterLogin() #pw-hooker
|
* @method void afterLogin() #pw-hooker
|
||||||
@@ -154,6 +156,8 @@ class ProcessLogin extends Process implements ConfigurableModule {
|
|||||||
*/
|
*/
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->set('tfaRecRoleIDs', array());
|
$this->set('tfaRecRoleIDs', array());
|
||||||
|
$this->set('tfaRememberDays', 90);
|
||||||
|
$this->set('tfaRememberFingerprints', array('agentVL', 'accept', 'scheme', 'host'));
|
||||||
$this->set('allowEmail', false);
|
$this->set('allowEmail', false);
|
||||||
$this->set('emailField', 'email');
|
$this->set('emailField', 'email');
|
||||||
$this->customMarkup['forgot-icon'] = wireIconMarkup('question-circle', 'fw');
|
$this->customMarkup['forgot-icon'] = wireIconMarkup('question-circle', 'fw');
|
||||||
@@ -336,6 +340,8 @@ class ProcessLogin extends Process implements ConfigurableModule {
|
|||||||
if(count($tfas)) {
|
if(count($tfas)) {
|
||||||
$tfa = new Tfa();
|
$tfa = new Tfa();
|
||||||
$this->wire($tfa);
|
$this->wire($tfa);
|
||||||
|
$tfa->rememberDays = $this->tfaRememberDays;
|
||||||
|
$tfa->rememberFingerprints = $this->tfaRememberFingerprints;
|
||||||
}
|
}
|
||||||
|
|
||||||
if($tfa && $tfa->active()) {
|
if($tfa && $tfa->active()) {
|
||||||
@@ -735,7 +741,7 @@ class ProcessLogin extends Process implements ConfigurableModule {
|
|||||||
}
|
}
|
||||||
$home = $this->pages->get('/');
|
$home = $this->pages->get('/');
|
||||||
$icon = $this->markup('home-icon');
|
$icon = $this->markup('home-icon');
|
||||||
$label = $home->getFormatted('title');
|
$label = $this->wire()->sanitizer->entities($home->getUnformatted('title'));
|
||||||
$links['home'] = str_replace(
|
$links['home'] = str_replace(
|
||||||
array('{url}', '{out}'),
|
array('{url}', '{out}'),
|
||||||
array($home->url, "$icon $label"),
|
array($home->url, "$icon $label"),
|
||||||
@@ -953,6 +959,40 @@ class ProcessLogin extends Process implements ConfigurableModule {
|
|||||||
}
|
}
|
||||||
$f->attr('value', $this->get('tfaRecRoleIDs'));
|
$f->attr('value', $this->get('tfaRecRoleIDs'));
|
||||||
$fieldset->add($f);
|
$fieldset->add($f);
|
||||||
|
|
||||||
|
/** @var InputfieldInteger $f */
|
||||||
|
$f = $modules->get('InputfieldInteger');
|
||||||
|
$f->attr('name', 'tfaRememberDays');
|
||||||
|
$f->label = $this->_('Allow users the option to skip code entry when their browser/location is remembered?');
|
||||||
|
$f->description = $this->_('Enter the number of days that browser/location will be remembered or 0 to disable.');
|
||||||
|
$f->attr('value', (int) $this->tfaRememberDays);
|
||||||
|
$fieldset->add($f);
|
||||||
|
|
||||||
|
$fingerprints = array(
|
||||||
|
'agent' => $this->_('User agent (browser, platform, and versions of each)'),
|
||||||
|
'agentVL' => $this->_('Non-versioned user agent (browser and platform, but no versions—less likely to change often)'),
|
||||||
|
'accept' => $this->_('Accept header (content types user’s browser accepts)'),
|
||||||
|
'scheme' => $this->_('Current request scheme whether HTTP or HTTPS'),
|
||||||
|
'host' => $this->_('Server hostname (value of $config->httpHost)'),
|
||||||
|
'ip' => $this->_('User’s IP address (REMOTE_ADDR)'),
|
||||||
|
'fwip' => $this->_('User’s forwarded or client IP address (HTTP_X_FORWARDED_FOR or HTTP_CLIENT_IP)'),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @var InputfieldCheckboxes $f */
|
||||||
|
$f = $modules->get('InputfieldCheckboxes');
|
||||||
|
$f->attr('name', 'tfaRememberFingerprints');
|
||||||
|
$f->label = $this->_('Do not allow user to skip code entry when any of these properties change');
|
||||||
|
$f->description =
|
||||||
|
$this->_('Changes to password, name, email, or a random cookie in the user’s browser, will always require code entry at login.') . ' ' .
|
||||||
|
$this->_('In addition, changes to any checked items below will also require code entry at login.') . ' ' .
|
||||||
|
$this->_('These properties form a fingerprint of the user’s browser beyond the random cookie that we set.');
|
||||||
|
$f->notes = $this->_('This setting only applies when the option to remember browser/location is enabled.');
|
||||||
|
foreach($fingerprints as $name => $label) {
|
||||||
|
$f->addOption($name, $label);
|
||||||
|
}
|
||||||
|
$f->showIf = 'tfaRememberDays!=0';
|
||||||
|
$f->attr('value', $this->tfaRememberFingerprints);
|
||||||
|
$fieldset->add($f);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$fieldset->description = $this->_('To configure this you must first install one or more Tfa modules and then return here.');
|
$fieldset->description = $this->_('To configure this you must first install one or more Tfa modules and then return here.');
|
||||||
|
Reference in New Issue
Block a user