| Current Path : /var/www/consult-e-syn/public_html/plugins/loginguard/pushbullet/ |
| Current File : /var/www/consult-e-syn/public_html/plugins/loginguard/pushbullet/pushbullet.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 FOF30\Encrypt\Totp;
use Joomla\CMS\Factory;
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 "Authentication Code by PushBullet"
*
* Requires entering a 6-digit code sent to the user through PushBullet. These codes change automatically every 30 seconds.
*/
class PlgLoginguardPushbullet extends CMSPlugin
{
/**
* The PushBullet access token for the PushBullet account which owns the PushBullet OAuth Client defined by the
* clientId and secret below.
*
* @var string
*/
public $accessToken;
/**
* PushBullet OAuth2 Client ID
*
* @var string
*/
public $clientId;
/**
* PushBullet OAuth2 Secret ID
*
* @var string
*/
private $secret;
/**
* The TFA method name handled by this plugin
*
* @var string
*/
private $tfaMethodName = 'pushbullet';
/**
* The component's container object
*
* @var Container
* @since 2.0.0
*/
private $container = 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);
if (!class_exists('LoginGuardPushbulletApi', true))
{
require_once __DIR__ . '/classes/pushbullet.php';
}
// Get a reference to the component's container
$this->container = Container::getInstance('com_loginguard');
// Load the PushBullet API parameters
/** @var \Joomla\Registry\Registry $params */
$params = $this->params;
$this->accessToken = $params->get('access_token', null);
$this->clientId = $params->get('client_id', null);
$this->secret = $params->get('secret', null);
// Load the language files
$this->loadLanguage();
}
/**
* Gets the identity of this TFA method
*
* @return array|false
*/
public function onLoginGuardTfaGetMethod()
{
// This plugin is disabled if you haven't configured it yet
if (empty($this->accessToken) || empty($this->clientId) || empty($this->secret))
{
return false;
}
$helpURL = $this->params->get('helpurl', 'https://github.com/akeeba/loginguard/wiki/Pushbullet');
return array(
// Internal code of this TFA method
'name' => $this->tfaMethodName,
// User-facing name for this TFA method
'display' => JText::_('PLG_LOGINGUARD_PUSHBULLET_LBL_DISPLAYEDAS'),
// Short description of this TFA method displayed to the user
'shortinfo' => JText::_('PLG_LOGINGUARD_PUSHBULLET_LBL_SHORTINFO'),
// URL to the logo image for this method
'image' => 'media/plg_loginguard_pushbullet/images/pushbullet.png',
// Are we allowed to disable it?
'canDisable' => true,
// Are we allowed to have multiple instances of it per user?
'allowMultiple' => false,
// URL for help content
'help_url' => $helpURL,
);
}
/**
* 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)
{
$helpURL = $this->params->get('helpurl', 'https://github.com/akeeba/loginguard/wiki/Pushbullet');
// 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);
$key = isset($options['key']) ? $options['key'] : '';
$token = isset($options['token']) ? $options['token'] : '';
// If there's a key or toekn in the session use that instead.
$session = Factory::getSession();
$key = $session->get('pushbullet.key', $key, 'com_loginguard');
$token = $session->get('pushbullet.token', $token, 'com_loginguard');
// Initialize objects
$totp = new Totp();
// If there's still no key in the options, generate one and save it in the session
if (empty($key))
{
$key = $totp->generateSecret();
$session->set('pushbullet.key', $key, 'com_loginguard');
}
$session->set('pushbullet.user_id', $record->user_id, 'com_loginguard');
// If there is no token we need to show the OAuth2 button
if (empty($token))
{
$layoutPath = PluginHelper::getLayoutPath('loginguard', 'pushbullet', 'oauth2');
ob_start();
include $layoutPath;
$html = ob_get_clean();
return array(
// Default title if you are setting up this TFA method for the first time
'default_title' => JText::_('PLG_LOGINGUARD_PUSHBULLET_LBL_DISPLAYEDAS'),
// Custom HTML to display above the TFA setup form
'pre_message' => JText::_('PLG_LOGINGUARD_PUSHBULLET_LBL_SETUP_INSTRUCTIONS'),
// 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(),
// How to render the TFA setup 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' => '',
// 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' => '',
// Custom HTML to display below the TFA setup form
'post_message' => '',
// URL for help content
'help_url' => $helpURL,
);
}
// We have a token and a key. Send a push message with a new code and ask the user to enter it.
$this->sendCode($key, $token);
return array(
// Default title if you are setting up this TFA method for the first time
'default_title' => JText::_('PLG_LOGINGUARD_PUSHBULLET_LBL_DISPLAYEDAS'),
// Custom HTML to display above the TFA setup form
'pre_message' => '',
// 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(
'key' => $key,
),
// How to render the TFA setup code field. "input" (HTML input element) or "custom" (custom HTML)
'field_type' => 'input',
// The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
'input_type' => 'text',
// 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' => JText::_('PLG_LOGINGUARD_PUSHBULLET_LBL_SETUP_PLACEHOLDER'),
// Label to show above the HTML input box. Leave empty if you don't need it.
'label' => JText::_('PLG_LOGINGUARD_PUSHBULLET_LBL_SETUP_LABEL'),
// Custom HTML. Only used when field_type = custom.
'html' => '',
// Should I show the submit button (apply the TFA setup)? Only applies in the Add page.
'show_submit' => true,
// onclick handler for the submit button (apply the TFA setup)?
'submit_onclick' => '',
// 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 actually meant to handle this method
if ($record->method != $this->tfaMethodName)
{
return array();
}
$session = Factory::getSession();
// Load the options from the record (if any)
$options = $this->_decodeRecordOptions($record);
$key = isset($options['key']) ? $options['key'] : '';
$token = isset($options['token']) ? $options['token'] : '';
// If there is no key in the options fetch one from the session
if (empty($key))
{
$key = $session->get('pushbullet.key', null, 'com_loginguard');
}
// If there is no key in the options fetch one from the session
if (empty($token))
{
$token = $session->get('pushbullet.token', null, 'com_loginguard');
}
// If there is still no key in the options throw an error
if (empty($key))
{
throw new RuntimeException(JText::_('JERROR_ALERTNOAUTHOR'), 403);
}
// If there is still no token in the options throw an error
if (empty($token))
{
throw new RuntimeException(JText::_('JERROR_ALERTNOAUTHOR'), 403);
}
/**
* If the code is empty but the key already existed in $options someone is simply changing the title / default
* method status. We can allow this and stop checking anything else now.
*/
$code = $input->getInt('code');
if (empty($code) && !empty($optionsKey))
{
return $options;
}
// In any other case validate the submitted code
$totp = new Totp();
$isValid = $totp->checkCode($key, $code);
if (!$isValid)
{
throw new RuntimeException(JText::_('PLG_LOGINGUARD_PUSHBULLET_ERR_INVALID_CODE'), 500);
}
// The code is valid. Unset the key from the session.
$session->set('totp.key', null, 'com_loginguard');
// Return the configuration to be serialized
return array(
'key' => $key,
'token' => $token
);
}
/**
* 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 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);
$key = isset($options['key']) ? $options['key'] : '';
$token = isset($options['token']) ? $options['token'] : '';
$helpURL = $this->params->get('helpurl', 'https://github.com/akeeba/loginguard/wiki/Pushbullet');
// Send a push message with a new code and ask the user to enter it.
$this->sendCode($key, $token);
return array(
// Custom HTML to display above the TFA form
'pre_message' => '',
// How to render the TFA code field. "input" (HTML input element) or "custom" (custom HTML)
'field_type' => 'input',
// The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type.
'input_type' => 'text',
// Placeholder text for the HTML input box. Leave empty if you don't need it.
'placeholder' => JText::_('PLG_LOGINGUARD_PUSHBULLET_LBL_SETUP_PLACEHOLDER'),
// Label to show above the HTML input box. Leave empty if you don't need it.
'label' => JText::_('PLG_LOGINGUARD_PUSHBULLET_LBL_SETUP_LABEL'),
// Custom HTML. Only used when field_type = custom.
'html' => '',
// Custom HTML to display below the TFA form
'post_message' => '',
// URL for help content
'help_url' => $helpURL,
);
}
/**
* 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 validatng 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 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)
$options = $this->_decodeRecordOptions($record);
$key = isset($options['key']) ? $options['key'] : '';
// If there is no key in the options throw an error
if (empty($key))
{
return false;
}
// Check the TFA code for validity
$totp = new Totp();
return $totp->checkCode($key, $code);
}
/**
* Decodes the options from a #__loginguard_tfa record into an options object.
*
* @param stdClass $record
*
* @return array
*/
private function _decodeRecordOptions($record)
{
$options = array(
'key' => '',
'token' => ''
);
if (!empty($record->options))
{
$recordOptions = $record->options;
$options = array_merge($options, $recordOptions);
}
return $options;
}
/**
* Creates a new TOTP code based on secret key $key and sends it to the user via PushBullet using the access token
* $token.
*
* @param string $key The TOTP secret key
* @param string $token The PushBullet access token
* @param User $user The Joomla! user to use
*
* @return void
*
* @throws LoginGuardPushbulletApiException If something goes wrong
*/
public function sendCode($key, $token, User $user = null)
{
// Make sure we have a user
if (!is_object($user) || !($user instanceof User))
{
$user = Factory::getUser();
}
// Get the API objects
$totp = new Totp();
$pushBullet = new LoginGuardPushbulletApi($token);
// Create the list of variable replacements
$code = $totp->getCode($key);
$replacements = array(
'[CODE]' => $code,
'[SITENAME]' => Factory::getConfig()->get('sitename'),
'[SITEURL]' => Uri::base(),
'[USERNAME]' => $user->username,
'[EMAIL]' => $user->email,
'[FULLNAME]' => $user->name,
);
// Get the title and body of the push message
$subject = JText::_('PLG_LOGINGUARD_PUSHBULLET_PUSH_TITLE');
$subject = str_ireplace(array_keys($replacements), array_values($replacements), $subject);
$message = JText::_('PLG_LOGINGUARD_PUSHBULLET_PUSH_MESSAGE');
$message = str_ireplace(array_keys($replacements), array_values($replacements), $message);
// Push the message to all of the user's devices
$pushBullet->pushNote('', $subject, $message);
}
/**
* Handle the OAuth2 callback
*
* The user is redirected to the callback URL by PushBullet itself. A code is sent back as a query string parameter.
* The code is sent back to PushBullet and we are given back a token. What happens next depends on the state URL
* parameter.
*
* If state=0 the 2SV setup was initiated by the frontend of the site. Therefore we just need to save the token in
* the session and redirect the user back to the 2SV method setup page. This will be picked up by the
* onLoginGuardTfaGetSetup method and a code will be sent to the user which he has to enter to finalize the setup.
*
* If state=1 the 2SV setup was initiated by the backend of the site. The callback is always in the frontend of
* the site since PushBullet checks the path of the URL versus what has been configured. However, since I'm in the
* frontend of the site I cannot set a session variable and read it from the backend. In this case I redirect the
* browser to the backend callback URL passing the token as a query string parameter. When this is detected the
* token is read from the q.s.p. and the rest of the process described above (save to session and redirect to setup
* page) takes place.
*
* @param string $method The 2SV method used during the callback.
*
* @return bool Only returns false when this plugin is not supposed to handle the request. Redirects the
* application otherwise (no return value).
*/
public function onLoginGuardCallback($method)
{
if ($method != $this->tfaMethodName)
{
return false;
}
$app = Factory::getApplication();
$input = $app->input;
// Should I redirect to the back-end?
$backend = $input->getInt('state', 0);
// Do I have a token access variable?
$token = $input->getString('token', null);
// If I have no token and it's the front-end I have received a token in the URL fragment from PushBullet
if (empty($token) && !$this->container->platform->isBackend())
{
// The returned URL has a code query string parameter I need to use to retrieve a token
$code = $input->getString('code', null);
$api = new LoginGuardPushbulletApi($this->accessToken);
$token = $api->getToken($code, $this->clientId, $this->secret);
}
// Do I have to redirect to the backend?
if ($backend == 1)
{
$redirectURL = Uri::base() . 'administrator/index.php?option=com_loginguard&task=callback.callback&method=pushbullet&token=' . $token;
$app->redirect($redirectURL);
// Just to make IDEs happy. The application is closed above during the redirection.
return false;
}
// Set the token to the session
$session = Factory::getSession();
$session->set('pushbullet.token', $token, 'com_loginguard');
// Get the User ID for the editor page
$user_id = $session->get('pushbullet.user_id', null, 'com_loginguard');
$session->set('pushbullet.user_id', null, 'com_loginguard');
// Redirect to the editor page
$userPart = empty($user_id) ? '' : ('&user_id='. $user_id);
$redirectURL = 'index.php?option=com_loginguard&task=method.add&method=pushbullet' . $userPart;
$app->redirect($redirectURL);
// Just to make IDEs happy. The application is closed above during the redirection.
return false;
}
}