| Current Path : /var/www/consult-e-syn/public_html/plugins/loginguard/u2f/ |
| Current File : /var/www/consult-e-syn/public_html/plugins/loginguard/u2f/u2f.php |
<?php
/**
* @package AkeebaLoginGuard
* @copyright Copyright (c)2016-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
use Akeeba\LoginGuard\Admin\Model\Tfa;
use FOF30\Container\Container;
use Joomla\CMS\Environment\Browser;
use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\User\User;
// Prevent direct access
defined('_JEXEC') or die;
/**
* Akeeba LoginGuard Plugin for Two Step Verification method "Time-based One Time Password"
*
* Requires a 6-digit code generated by Google Authenticator or any compatible application. These codes change
* automatically every 30 seconds.
*/
class PlgLoginguardU2f extends CMSPlugin
{
/**
* The TFA method name handled by this plugin
*
* @var string
*/
private $tfaMethodName = 'u2f';
/**
* Should I report myself as enabled?
*
* @var bool
*/
private $enabled = true;
/**
* U2F Server Library instance
*
* @var \u2flib_server\U2F
*/
protected $u2f = null;
/**
* Constructor. Loads the language files as well.
*
* @param object &$subject The object to observe
* @param array $config An optional associative array of configuration settings.
* Recognized key values include 'name', 'group', 'params', 'language'
* (this list is not meant to be comprehensive).
*/
public function __construct($subject, array $config = array())
{
parent::__construct($subject, $config);
// Load the language file
$this->loadLanguage();
// Try to load the U2F server library
if (!class_exists('u2flib_server\\U2F'))
{
require_once __DIR__ . '/classes/u2f.php';
}
// Make sure I can load the server library
$this->enabled = class_exists('u2flib_server\\U2F');
if (!$this->enabled)
{
return;
}
// Check if we're on a supported browser
$this->enabled = $this->isSupportedBrowser();
if (!$this->enabled)
{
return;
}
// Check OpenSSL version
$this->enabled = $this->isOpenSSL10OrNewer();
if (!$this->enabled)
{
return;
}
// Try to create a server library object
$jURI = Uri::getInstance();
$appId = $jURI->toString(array('scheme', 'host', 'port'));
try
{
$this->u2f = new u2flib_server\U2F($appId);
}
catch (\Exception $e)
{
$this->enabled = false;
return;
}
// Finally, detect old Google Chrome versions and activate U2F support manually.
$this->loadOldChromeJavascript();
}
/**
* Gets the identity of this TFA method
*
* @return array
*/
public function onLoginGuardTfaGetMethod()
{
if (!$this->enabled)
{
return array();
}
$helpURL = $this->params->get('helpurl', 'https://github.com/akeeba/loginguard/wiki/U2F');
return array(
// Internal code of this TFA method
'name' => $this->tfaMethodName,
// User-facing name for this TFA method
'display' => JText::_('PLG_LOGINGUARD_U2F_LBL_DISPLAYEDAS'),
// Short description of this TFA method displayed to the user
'shortinfo' => JText::_('PLG_LOGINGUARD_U2F_LBL_SHORTINFO'),
// URL to the logo image for this method
'image' => 'media/plg_loginguard_u2f/images/u2f.svg',
// Are we allowed to disable it?
'canDisable' => true,
// Are we allowed to have multiple instances of it per user?
'allowMultiple' => true,
// URL for help content
'help_url' => $helpURL,
// Allow authentication against all entries of this TFA method. Otherwise authentication takes place against a SPECIFIC entry at a time.
'allowEntryBatching' => 1,
);
}
/**
* Returns the information which allows LoginGuard to render the TFA setup page. This is the page which allows the
* user to add or modify a TFA method for their user account. If the record does not correspond to your plugin
* return an empty array.
*
* @param stdClass $record The #__loginguard_tfa record currently selected by the user.
*
* @return array
*/
public function onLoginGuardTfaGetSetup($record)
{
// Make sure we are enabled
if (!$this->enabled)
{
return array();
}
// Make sure we are actually meant to handle this method
if ($record->method != $this->tfaMethodName)
{
return array();
}
// Load the options from the record (if any)
$options = $this->_decodeRecordOptions($record);
$currentRecordRegistrations = isset($options['registrations']) ? $options['registrations'] : array();
$registrations = $this->getRegistrationsFor($record->user_id);
// Get some values assuming that we are NOT setting up U2F (the key is already registered)
$submitOnClick = '';
$preMessage = JText::_('PLG_LOGINGUARD_U2F_LBL_CONFIGURED');
$u2fRegData = json_encode($this->u2f->getRegisterData($registrations));
$type = 'input';
$html = '';
$helpURL = $this->params->get('helpurl', 'https://github.com/akeeba/loginguard/wiki/U2F');
/**
* If there are no security keys set up yet I need to show a different message and take a different action when
* my user clicks the submit button.
*/
if (empty($currentRecordRegistrations))
{
// Load Javascript
HTMLHelper::_('script', 'plg_loginguard_u2f/u2f-api.min.js', [
'version' => 'auto',
'relative' => true,
'detectDebug' => true,
'framework' => true,
'pathOnly' => false,
'detectBrowser' => true,
], [
'defer' => false,
'async' => false,
]);
HTMLHelper::_('script', 'plg_loginguard_u2f/u2f.min.js', [
'version' => 'auto',
'relative' => true,
'detectDebug' => true,
'framework' => true,
'pathOnly' => false,
'detectBrowser' => true,
], [
'defer' => false,
'async' => false,
]);
$js = <<< JS
;; // Defense against broken scripts
window.jQuery(document).ready(function() {
akeeba.LoginGuard.u2f.regData = $u2fRegData;
});
JS;
Factory::getDocument()->addScriptDeclaration($js);
$layoutPath = PluginHelper::getLayoutPath('loginguard', 'u2f', 'register');
ob_start();
include $layoutPath;
$html = ob_get_clean();
$type = 'custom';
// Load JS translations
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_OTHER');
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_CANNOTPROCESS');
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_CLIENTCONFIGNOTSUPPORTED');
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_INELIGIBLE');
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_TIMEOUT');
// Save the U2F request to the session
$session = Factory::getSession();
$session->set('u2f.request', $u2fRegData, 'com_loginguard');
// Special button handling
$submitOnClick = "akeeba.LoginGuard.u2f.setUp(); return false;";
// Message to display
$preMessage = JText::_('PLG_LOGINGUARD_U2F_LBL_INSTRUCTIONS');
}
return array(
// Default title if you are setting up this TFA method for the first time
'default_title' => JText::_('PLG_LOGINGUARD_U2F_LBL_DISPLAYEDAS'),
// Custom HTML to display above the TFA setup form
'pre_message' => $preMessage,
// Heading for displayed tabular data. Typically used to display a list of fixed TFA codes, TOTP setup parameters etc
'table_heading' => '',
// Any tabular data to display (label => custom HTML). See above
'tabular_data' => array(),
// Hidden fields to include in the form (name => value)
'hidden_data' => array(
'u2fregdata' => $u2fRegData,
),
// How to render the TFA setup code field. "input" (HTML input element) or "custom" (custom HTML)
'field_type' => $type,
// The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
'input_type' => 'hidden',
// Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed YubiKey ID etc.
'input_value' => '',
// Placeholder text for the HTML input box. Leave empty if you don't need it.
'placeholder' => '',
// Label to show above the HTML input box. Leave empty if you don't need it.
'label' => '',
// Custom HTML. Only used when field_type = custom.
'html' => $html,
// Should I show the submit button (apply the TFA setup)? Only applies in the Add page.
'show_submit' => false,
// onclick handler for the submit button (apply the TFA setup)?
'submit_onclick' => $submitOnClick,
// Custom HTML to display below the TFA setup form
'post_message' => '',
// URL for help content
'help_url' => $helpURL,
);
}
/**
* Parse the input from the TFA setup page and return the configuration information to be saved to the database. If
* the information is invalid throw a RuntimeException to signal the need to display the editor page again. The
* message of the exception will be displayed to the user. If the record does not correspond to your plugin return
* an empty array.
*
* @param stdClass $record The #__loginguard_tfa record currently selected by the user.
* @param JInput $input The user input you are going to take into account.
*
* @return array The configuration data to save to the database
*
* @throws RuntimeException In case the validation fails
*/
public function onLoginGuardTfaSaveSetup($record, JInput $input)
{
// Make sure we are enabled
if (!$this->enabled)
{
return array();
}
// Make sure we are actually meant to handle this method
if ($record->method != $this->tfaMethodName)
{
return array();
}
// Load the options from the record (if any)
$options = $this->_decodeRecordOptions($record);
if (!isset($options['registrations']))
{
$options['registrations'] = array();
}
// load the registration request from the session
$session = Factory::getSession();
$u2fRegData = $session->get('u2f.request', null, 'com_loginguard');
$session->set('u2f.request', null, 'com_loginguard');
$registrationRequest = json_decode($u2fRegData);
// Load the registration response from the input
$code = $input->get('code', null, 'raw');
$registerResponse = json_decode($code);
// If there was no registration request BUT there is a registration response throw an error
if (empty($registrationRequest) && !(empty($code) || empty($registerResponse)))
{
throw new RuntimeException(JText::_('JERROR_ALERTNOAUTHOR'), 403);
}
// If there is no registration request (and there isn't a registration response) we are just saving the title.
if (empty($registrationRequest))
{
return $options;
}
// In any other case try to authorize the registration
try
{
$registration = $this->u2f->doRegister($registrationRequest[0], $registerResponse);
}
catch (\u2flib_server\Error $err)
{
throw new RuntimeException($err->getMessage(), 403);
}
// The code is valid. Unset the request data from the session and update the options
$options['registrations'][] = $registration;
// Return the configuration to be serialized
return $options;
}
/**
* Returns the information which allows LoginGuard to render the captive TFA page. This is the page which appears
* right after you log in and asks you to validate your login with TFA.
*
* @param stdClass $record The #__loginguard_tfa record currently selected by the user.
*
* @return array
*/
public function onLoginGuardTfaCaptive($record)
{
// Make sure we are enabled
if (!$this->enabled)
{
return [];
}
// Make sure we are actually meant to handle this method
if ($record->method != $this->tfaMethodName)
{
return [];
}
// Get the media version
$mediaVersion = Container::getInstance('com_loginguard')->mediaVersion;
// We are going to load a JS file and use custom on-load JS to intercept the loginguard-captive-button-submit button
HTMLHelper::_('script', 'plg_loginguard_u2f/u2f-api.min.js', [
'version' => $mediaVersion,
'relative' => true,
'detectDebug' => true,
'framework' => true,
'pathOnly' => false,
'detectBrowser' => true,
], [
'defer' => false,
'async' => false,
]);
HTMLHelper::_('script', 'plg_loginguard_u2f/u2f.min.js', [
'version' => $mediaVersion,
'relative' => true,
'detectDebug' => true,
'framework' => true,
'pathOnly' => false,
'detectBrowser' => true,
], [
'defer' => false,
'async' => false,
]);
// Load JS translations
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_OTHER');
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_CANNOTPROCESS');
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_CLIENTCONFIGNOTSUPPORTED');
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_INELIGIBLE_SIGN');
JText::script('PLG_LOGINGUARD_U2F_ERR_JS_TIMEOUT');
// Load the options from the record (if any), or from the entire method if the allowEntryBatching flag is set.
$registrations = $this->getRegistrations($record);
/**
* The following code looks stupid. An explanation is in order.
*
* What we normally want to do is save the authentication data returned by getAuthenticateData into the session.
* This is what is sent to the U2F key through the Javascript API and signed. The signature is posted back to
* the form as the "code" which is read by onLoginGuardTfaValidate. That method will read the authentication
* data from the session and pass it along with the key registration data (from the database) and the
* authentication response (the "code" submitted in the form) to the U2F library for validation.
*
* Validation will work as long as the challenge recorded in the encrypted AUTHENTICATION RESPONSE matches, upon
* decryption, the challenge recorded in the AUTHENTICATION DATA.
*
* I observed that for whatever stupid reason the browser was sometimes sending TWO requests to the server's
* captive login page but only rendered the FIRST. This meant that the authentication data sent to the key had
* already been overwritten in the session by the "invisible" second request. As a result the challenge would
* not match and we'd get a validation error.
*
* The code below will attempt to read the authentication data from the session first. If it exists it will NOT
* try to replace it (technically it replaces it with a copy of the same data - same difference!). If nothing
* exists in the session, however, it WILL store the (random seeded) result of the getAuthenticateData method.
* Therefore the first request to the captive login page will store a new set of authentication data whereas the
* second, "invisible", request will just reuse the same data as the first request, fixing the observed issue in
* a way that doesn't compromise security.
*
* In case you are wondering, yes, the data is removed from the session in the onLoginGuardTfaValidate method.
* In fact it's the first thing we do after reading it, preventing constant reuse of the same set of challenges.
*
* That was fun to debug - for "poke your eyes with a rusty fork" values of fun.
*/
$session = Factory::getSession();
$u2fAuthData = $this->u2f->getAuthenticateData($registrations);
$u2fAuthData = $session->get('u2f.authentication', base64_encode(serialize($u2fAuthData)), 'com_loginguard');
$u2fAuthData = unserialize(base64_decode($u2fAuthData));
$u2fAuthDataJSON = json_encode($u2fAuthData);
$session->set('u2f.authentication', base64_encode(serialize($u2fAuthData)), 'com_loginguard');
$js = <<< JS
;; // Defense against broken scripts
function akeebaLoginGuardU2FOnClick()
{
window.jQuery('#loginguard-u2f-button').hide();
akeeba.LoginGuard.u2f.validate();
return false;
}
window.jQuery(document).ready(function($) {
akeeba.LoginGuard.u2f.authData = $u2fAuthDataJSON;
$('#loginguard-captive-button-submit').click(function() {
akeebaLoginGuardU2FOnClick();
});
setTimeout(function() {
akeebaLoginGuardU2FOnClick();
}, 250);
});
JS;
Factory::getDocument()->addScriptDeclaration($js);
$layoutPath = PluginHelper::getLayoutPath('loginguard', 'u2f', 'validate');
ob_start();
include $layoutPath;
$html = ob_get_clean();
$helpURL = $this->params->get('helpurl', 'https://github.com/akeeba/loginguard/wiki/U2F');
return [
// Custom HTML to display above the TFA form
'pre_message' => JText::_('PLG_LOGINGUARD_U2F_LBL_INSTRUCTIONS'),
// How to render the TFA code field. "input" (HTML input element) or "custom" (custom HTML)
'field_type' => 'custom',
// The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
'input_type' => '',
// Placeholder text for the HTML input box. Leave empty if you don't need it.
'placeholder' => '',
// Label to show above the HTML input box. Leave empty if you don't need it.
'label' => '',
// Custom HTML. Only used when field_type = custom.
'html' => $html,
// Custom HTML to display below the TFA form
'post_message' => '',
// Should I hide the submit button? Useful if you need to render your own buttons or use a method which is meant to auto-submit upon doing a certain action.
'hide_submit' => true,
// URL for help content
'help_url' => $helpURL,
// Allow authentication against all entries of this TFA method. Otherwise authentication takes place against a SPECIFIC entry at a time.
'allowEntryBatching' => 1,
];
}
/**
* Validates the Two Factor Authentication code submitted by the user in the captive Two Step Verification page. If
* the record does not correspond to your plugin return FALSE.
*
* @param Tfa $record The TFA method's record you're validating against
* @param User $user The user record
* @param string $code The submitted code
*
* @return bool
*/
public function onLoginGuardTfaValidate(Tfa $record, User $user, $code)
{
// Make sure we are enabled
if (!$this->enabled)
{
return false;
}
// Make sure we are actually meant to handle this method
if ($record->method != $this->tfaMethodName)
{
return false;
}
// Double check the TFA method is for the correct user
if ($user->id != $record->user_id)
{
return false;
}
// Load the options from the record (if any), or from the entire method if the allowEntryBatching flag is set.
$registrations = $this->getRegistrations($record);
// Get the authentication response
$authenticateResponse = json_decode($code);
if (empty($authenticateResponse))
{
// Invalid authentication signature response in request
return false;
}
$session = Factory::getSession();
$authenticationRequest = $session->get('u2f.authentication', null, 'com_loginguard');
$session->set('u2f.authentication', null, 'com_loginguard');
if (empty($authenticationRequest))
{
// No authentication request in session; do not proceed
return false;
}
$authenticationRequest = unserialize(base64_decode($authenticationRequest));
if (empty($authenticationRequest))
{
// Invalid authentication request in session; do not proceed
return false;
}
// Validate the U2F signature
try
{
$registration = $this->u2f->doAuthenticate($authenticationRequest, $registrations, $authenticateResponse);
}
catch (Exception $e)
{
return false;
}
// The $registration contains the updated registration for the used security key. But WHICH one?
$id = $record->id;
/**
* Save the updated registration to the database.
*
* Why? Every time the security key signs a verification request it increases its internal counter monotonical-
* ly. Every subsequent signing request will have a counter larger than the previous one. If the library sees a
* counter that's lower than the last recorded one we know that we have a cloned security key and we have to
* reject it. This protection only works if we "remember" the last counter encountered, i.e. if we save the
* updated registration after validation.
*/
$update = (object)array(
'id' => $id,
'options' => json_encode(array('registrations' => array($registration))),
);
$container = Container::getInstance('com_loginguard');
$container->platform->runPlugins('onLoginGuardBeforeSaveRecord', [&$update]);
$db = Factory::getDbo();
$db->updateObject('#__loginguard_tfa', $update, array('id'));
return true;
}
/**
* Decodes the options from a #__loginguard_tfa record into an options object.
*
* @param stdClass|string $record The record object or just the JSON-encoded options
*
* @return array
*/
private function _decodeRecordOptions($record)
{
$options = array(
'registrations' => array(),
);
$recordOptions = null;
if (is_object($record))
{
$recordOptions = $record->options;
}
elseif (is_string($record))
{
$recordOptions = $record;
}
if (!empty($recordOptions))
{
/**
* The end result is:
* $recordOptions is an array with one key, 'registrations'
* $recordOptions['registrations'] is a simple (numerically indexed) array. Its contents are objects.
* That's exactly what I wanted.
*/
$temp = [];
foreach ($recordOptions['registrations'] as $k => $opt)
{
$temp[$k] = (object)$opt;
}
$recordOptions = ['registrations' => $temp];
$options = array_merge($options, $recordOptions);
}
return $options;
}
/**
* Checks if we have OpenSSL 1.0 or newer
*
* @return bool
*/
private function isOpenSSL10OrNewer()
{
// No OpenSSL? No joy.
if (!defined('OPENSSL_VERSION_TEXT'))
{
return false;
}
$parts = explode(' ', OPENSSL_VERSION_TEXT);
// Not actually OpenSSL? No joy.
if (strtoupper($parts[0]) != 'OPENSSL')
{
return false;
}
// We can't directly use version compare as it doesn't follow PHP version semantics
$version = $parts[1];
$parts = explode('.', $version, 4);
$version = $parts[0] . '.' . $parts[1] . '.' . (int)$parts[2];
return version_compare($version, '1.0.0', 'ge');
}
/**
* Old Google Chrome versions (38, 39 and 40) required a special extension to enable support for U2F. This extension
* would only load by default the U2F support on Google sites. Third party sites needed to load the extension's JS
* file directly. This method detects Google Chrome 38, 39 and 40 and loads the extension Javascript to activate U2F
* support. On newer versions of Google Chrome and browsers made by other vendors this method takes no action. U2F
* is supported transparently either natively or through an add-on.
*
* NB: If you're still using a browser from late 2013 to late 2014 then being able to use a U2F security key is
* probably the least of your worries. I wouldn't run such an old browser even at gunpoint!
*
* @see http://stackoverflow.com/questions/27158182/u2f-support-without-the-u2f-chrome-extension
*
* @return void
*/
private function loadOldChromeJavascript()
{
if (!class_exists('JBrowser'))
{
JLoader::import('joomla.environment.browser');
}
$jBrowser = Browser::getInstance();
$browserMake = $jBrowser->getBrowser() == 'chrome';
$browserVersion = $jBrowser->getVersion();
$isOldChrome = ($browserMake) && version_compare($browserVersion, '38.0', 'ge') && version_compare($browserVersion, '41.0', 'lt');
if ($isOldChrome)
{
Factory::getDocument()->addScript('chrome-extension://pfboblefjcgdjicmnffhdgionmgcdmne/u2f-api.js');
}
}
/**
* Get all security key registrations for the specified user
*
* @param int $user_id The user ID to look for. Leave empty for the current user.
*
* @return array
*/
private function getRegistrationsFor($user_id = null)
{
if (empty($user_id))
{
$user_id = Factory::getUser()->id;
}
$return = array();
$container = Container::getInstance('com_loginguard');
/** @var Tfa $tfaModel */
$tfaModel = $container->factory->model('Tfa')->tmpInstance();
$results = $tfaModel->user_id($user_id)->method('u2f')->get(true);
if ($results->count() < 1)
{
return $return;
}
foreach ($results as $result)
{
$options = $this->_decodeRecordOptions($result);
if (!isset($options['registrations']) || empty($options['registrations']))
{
continue;
}
$return[$result->id] = $options['registrations'][0];
}
return $return;
}
/**
* Get the security key registrations for a given record. If the allowEntryBatching flag is 0 (No) we only return
* the key registrations for the given record. If the allowEntryBatching flag is 1 (Yes) we return the combined key
* registrations for all security key records of the user ID found in the $record object.
*
* @param stdClass $record The LoginGuard record
*
* @return array Security key registrations for use by the U2F library
*/
private function getRegistrations($record)
{
$options = $this->_decodeRecordOptions($record);
$registrations = array();
try
{
$container = Container::getInstance('com_loginguard');
/** @var Tfa $tfaModel */
$tfaModel = $container->factory->model('Tfa')->tmpInstance();
$records = $tfaModel->user_id($record->user_id)->method($record->method)->get(true);
}
catch (Exception $e)
{
$records = array();
}
// Loop all records, stop if at least one matches
$container = Container::getInstance('com_loginguard');
/** @var Tfa $aRecord */
foreach ($records as $aRecord)
{
$recordOptions = $this->_decodeRecordOptions($aRecord);
$recordRegistrations = isset($recordOptions['registrations']) ? $recordOptions['registrations'] : array();
$registrations = array_merge($registrations, $recordRegistrations);
}
return $registrations;
}
/**
* Deactivate on unsupported browsers.
*
* Based on our research, the only browsers where U2F is supported are Google Chrome and Opera 15+. This applies
* to all operating systems except for iOS (see below).
*
* Note that even though FF offers experimental U2F support it doesn't work properly. We've found that only
* security keys registered by Firefox will work on it. Moreover, registering a security key in the frontend
* makes it impossible to use it in the backend (and vice versa). As a result we're now disabling the U2F plugin
* on Firefox.
*
* Finally, iOS devices do not support security keys on any browser. Security keys are USB devices which need a
* full USB host implementation and OS support to interact with them. iOS does not provide such a feature.
* Please note that "security keys" refers to U2F mode *only*. YubiKey OTP mode still works because the device
* works as a USB keyboard or an NFC data tag in that mode. iOS supports USB keyboards if you use the Apple
* USB-C/Lightning to USB-A dongle. Moreover, since iOS 11, it is possible to use a third party NFC reader app
* to get a YubiKey OTP code over NFC, copy it to clipboard and then paste it in the browser of your choice.
*
* @return bool
*/
private function isSupportedBrowser()
{
$agent = '';
if (isset($_SERVER['HTTP_USER_AGENT']))
{
$agent = trim($_SERVER['HTTP_USER_AGENT']);
}
/**
* If I have no idea which browser you're using I'm just going to let you try and use U2F. May the Force be with
* you.
*/
if (empty($agent))
{
return true;
}
// If you're on iOS I won't let you try and use U2F as it's not supported by your Operating System.
if (preg_match('/(iPhone|iPod|iPad|iOS)/i', $agent))
{
return false;
}
// Getting your browser make and model. Hang on.
$jBrowser = Browser::getInstance();
$browserMake = $jBrowser->getBrowser();
$browserVersion = $jBrowser->getVersion();
// Are you on Chrome 38+? Awesome, you can use U2F on Windows, Linux, macOS and Android!
if (($browserMake == 'chrome') && version_compare($browserVersion, '38.0', 'ge'))
{
return true;
}
// Are you using Opera 41 or later? Awesome, you can use U2F on Windows, Linux and macOS!
if (($browserMake == 'opera') && version_compare($browserVersion, '41.0', 'ge'))
{
return true;
}
return false;
}
}