primo commit
This commit is contained in:
@ -0,0 +1,632 @@
|
||||
<?php
|
||||
/**
|
||||
* @package FOF
|
||||
* @copyright Copyright (c)2010-2022 Nicholas K. Dionysopoulos / Akeeba Ltd
|
||||
* @license GNU General Public License version 3, or later
|
||||
*/
|
||||
|
||||
namespace FOF40\TransparentAuthentication;
|
||||
|
||||
defined('_JEXEC') || die;
|
||||
|
||||
use FOF40\Container\Container;
|
||||
use FOF40\Encrypt\Aes;
|
||||
use FOF40\Encrypt\Totp;
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
/**
|
||||
* Retrieves the values for transparent authentication from the request
|
||||
*/
|
||||
class TransparentAuthentication
|
||||
{
|
||||
/** Use HTTP Basic Authentication with time-based one time passwords */
|
||||
const Auth_HTTPBasicAuth_TOTP = 1;
|
||||
|
||||
/** Use Query String Parameter authentication with time-based one time passwords */
|
||||
const Auth_QueryString_TOTP = 2;
|
||||
|
||||
/** Use HTTP Basic Authentication with plain text username and password */
|
||||
const Auth_HTTPBasicAuth_Plaintext = 3;
|
||||
|
||||
/** Use single query string parameter authentication with JSON-encoded, plain text username and password */
|
||||
const Auth_QueryString_Plaintext = 4;
|
||||
|
||||
/** Use two query string parameters for plain text username and password authentication */
|
||||
const Auth_SplitQueryString_Plaintext = 5;
|
||||
|
||||
/** @var int The time step for TOTP authentication */
|
||||
protected $timeStep = 6;
|
||||
|
||||
/** @var string The TOTP secret key */
|
||||
protected $totpKey = '';
|
||||
|
||||
/** @var array Enabled authentication methods, see the class constants */
|
||||
protected $authenticationMethods = [3, 4, 5];
|
||||
|
||||
/** @var string The username required for the Auth_HTTPBasicAuth_TOTP method */
|
||||
protected $basicAuthUsername = '_fof_auth';
|
||||
|
||||
/** @var string The query parameter for the Auth_QueryString_Plaintext method */
|
||||
protected $queryParam = '_fofauthentication';
|
||||
|
||||
/** @var string The query parameter for the username in the Auth_SplitQueryString_Plaintext method */
|
||||
protected $queryParamUsername = '_fofusername';
|
||||
|
||||
/** @var string The query parameter for the password in the Auth_SplitQueryString_Plaintext method */
|
||||
protected $queryParamPassword = '_fofpassword';
|
||||
|
||||
/** @var bool Should I log out the user after the dispatcher exits? */
|
||||
protected $logoutOnExit = true;
|
||||
|
||||
/** @var Container The container we are attached to */
|
||||
protected $container;
|
||||
|
||||
/** @var string Internal variable */
|
||||
private $cryptoKey = '';
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
*
|
||||
* The optional $config array can contain the following values (corresponding to the same-named properties of this
|
||||
* class): timeStep, totpKey, cryptoKey, basicAuthUsername, queryParam, queryParamUsername, queryParamPassword,
|
||||
* logoutOnExit. See the property descriptions for more information.
|
||||
*
|
||||
* @param \FOF40\Container\Container $container
|
||||
* @param array $config
|
||||
*/
|
||||
function __construct(Container $container, array $config = [])
|
||||
{
|
||||
$this->container = $container;
|
||||
|
||||
// Initialise from the $config array
|
||||
$knownKeys = [
|
||||
'timeStep', 'totpKey', 'cryptoKey', 'basicAuthUsername', 'queryParam', 'queryParamUsername',
|
||||
'queryParamPassword', 'logoutOnExit',
|
||||
];
|
||||
|
||||
foreach ($knownKeys as $key)
|
||||
{
|
||||
if (isset($config[$key]))
|
||||
{
|
||||
$this->$key = $config[$key];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['authenticationMethods']))
|
||||
{
|
||||
$this->authenticationMethods = $this->parseAuthenticationMethods($config['authenticationMethods']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the enabled authentication methods
|
||||
*
|
||||
* @return int[]
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getAuthenticationMethods(): array
|
||||
{
|
||||
return $this->authenticationMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the enabled authentication methods
|
||||
*
|
||||
* @param int[] $authenticationMethods
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setAuthenticationMethods(array $authenticationMethods): void
|
||||
{
|
||||
$this->authenticationMethods = [];
|
||||
|
||||
foreach ($authenticationMethods as $method)
|
||||
{
|
||||
$this->addAuthenticationMethod($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable an authentication method
|
||||
*
|
||||
* @param integer $method
|
||||
*/
|
||||
public function addAuthenticationMethod(int $method): void
|
||||
{
|
||||
$validMethods = [
|
||||
self::Auth_HTTPBasicAuth_Plaintext, self::Auth_HTTPBasicAuth_TOTP,
|
||||
self::Auth_QueryString_Plaintext, self::Auth_QueryString_TOTP,
|
||||
self::Auth_SplitQueryString_Plaintext,
|
||||
];
|
||||
|
||||
if (!in_array($method, $validMethods))
|
||||
{
|
||||
throw new \InvalidArgumentException(Text::sprintf('LIB_FOF40_TRANSPARENTAUTHENTICATION_INVALIDAUTHMETHOD', $method));
|
||||
}
|
||||
|
||||
if (!in_array($method, $this->authenticationMethods))
|
||||
{
|
||||
$this->authenticationMethods[] = $method;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable an authentication method
|
||||
*
|
||||
* @param integer $method
|
||||
*/
|
||||
public function removeAuthenticationMethod(int $method): void
|
||||
{
|
||||
if (in_array($method, $this->authenticationMethods))
|
||||
{
|
||||
$key = array_search($method, $this->authenticationMethods);
|
||||
unset($this->authenticationMethods[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the required username for the HTTP Basic Authentication with TOTP method
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getBasicAuthUsername(): string
|
||||
{
|
||||
return $this->basicAuthUsername;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the required username for the HTTP Basic Authentication with TOTP method
|
||||
*
|
||||
* @param string $basicAuthUsername
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setBasicAuthUsername(string $basicAuthUsername): void
|
||||
{
|
||||
$this->basicAuthUsername = $basicAuthUsername;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query parameter for the Auth_QueryString_TOTP method
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getQueryParam(): string
|
||||
{
|
||||
return $this->queryParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query parameter for the Auth_QueryString_TOTP method
|
||||
*
|
||||
* @param string $queryParam
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setQueryParam(string $queryParam): void
|
||||
{
|
||||
$this->queryParam = $queryParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query string for the password in the Auth_SplitQueryString_Plaintext method
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getQueryParamPassword(): string
|
||||
{
|
||||
return $this->queryParamPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query string for the password in the Auth_SplitQueryString_Plaintext method
|
||||
*
|
||||
* @param string $queryParamPassword
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setQueryParamPassword(string $queryParamPassword): void
|
||||
{
|
||||
$this->queryParamPassword = $queryParamPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query string for the username in the Auth_SplitQueryString_Plaintext method
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getQueryParamUsername(): string
|
||||
{
|
||||
return $this->queryParamUsername;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the query string for the username in the Auth_SplitQueryString_Plaintext method
|
||||
*
|
||||
* @param string $queryParamUsername
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setQueryParamUsername(string $queryParamUsername): void
|
||||
{
|
||||
$this->queryParamUsername = $queryParamUsername;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time step in seconds for the TOTP in the Auth_HTTPBasicAuth_TOTP method
|
||||
*
|
||||
* @return int
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getTimeStep(): int
|
||||
{
|
||||
return $this->timeStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time step in seconds for the TOTP in the Auth_HTTPBasicAuth_TOTP method
|
||||
*
|
||||
* @param int $timeStep
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setTimeStep(int $timeStep): void
|
||||
{
|
||||
$this->timeStep = $timeStep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the secret key for the TOTP in the Auth_HTTPBasicAuth_TOTP method
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getTotpKey(): string
|
||||
{
|
||||
return $this->totpKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the secret key for the TOTP in the Auth_HTTPBasicAuth_TOTP method
|
||||
*
|
||||
* @param string $totpKey
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setTotpKey(string $totpKey): void
|
||||
{
|
||||
$this->totpKey = $totpKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should I log out when the dispatcher finishes?
|
||||
*
|
||||
* @return boolean
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getLogoutOnExit(): bool
|
||||
{
|
||||
return $this->logoutOnExit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the log out on exit flag (for testing)
|
||||
*
|
||||
* @param boolean $logoutOnExit
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setLogoutOnExit(bool $logoutOnExit): void
|
||||
{
|
||||
$this->logoutOnExit = $logoutOnExit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to get the transparent authentication credentials from the request
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getTransparentAuthenticationCredentials(): ?array
|
||||
{
|
||||
$return = null;
|
||||
|
||||
// Always run onFOFGetTransparentAuthenticationCredentials. These methods take precedence over anything else.
|
||||
$this->container->platform->importPlugin('user');
|
||||
$this->container->platform->importPlugin('fof');
|
||||
$pluginResults = $this->container->platform->runPlugins('onFOFGetTransparentAuthenticationCredentials', [$this->container]);
|
||||
|
||||
foreach ($pluginResults as $result)
|
||||
{
|
||||
if (empty($result))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($result))
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure there are enabled transparent authentication methods
|
||||
if (empty($this->authenticationMethods))
|
||||
{
|
||||
return $return;
|
||||
}
|
||||
|
||||
$input = $this->container->input;
|
||||
|
||||
foreach ($this->authenticationMethods as $method)
|
||||
{
|
||||
switch ($method)
|
||||
{
|
||||
case self::Auth_HTTPBasicAuth_TOTP:
|
||||
if (empty($this->totpKey))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (empty($this->basicAuthUsername))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (!isset($_SERVER['PHP_AUTH_USER']))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (!isset($_SERVER['PHP_AUTH_PW']))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if ($_SERVER['PHP_AUTH_USER'] != $this->basicAuthUsername)
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$encryptedData = $_SERVER['PHP_AUTH_PW'];
|
||||
|
||||
return $this->decryptWithTOTP($encryptedData);
|
||||
|
||||
case self::Auth_QueryString_TOTP:
|
||||
if (empty($this->queryParam))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$encryptedData = $input->get($this->queryParam, '', 'raw');
|
||||
|
||||
if (empty($encryptedData))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$return = $this->decryptWithTOTP($encryptedData);
|
||||
|
||||
if (!is_null($return))
|
||||
{
|
||||
return $return;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case self::Auth_HTTPBasicAuth_Plaintext:
|
||||
if (!isset($_SERVER['PHP_AUTH_USER']))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (!isset($_SERVER['PHP_AUTH_PW']))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $_SERVER['PHP_AUTH_USER'],
|
||||
'password' => $_SERVER['PHP_AUTH_PW'],
|
||||
];
|
||||
|
||||
case self::Auth_QueryString_Plaintext:
|
||||
if (empty($this->queryParam))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$jsonEncoded = $input->get($this->queryParam, '', 'raw');
|
||||
|
||||
if (empty($jsonEncoded))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$authInfo = json_decode($jsonEncoded, true);
|
||||
|
||||
if (!is_array($authInfo))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (!array_key_exists('username', $authInfo) || !array_key_exists('password', $authInfo))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
return $authInfo;
|
||||
|
||||
case self::Auth_SplitQueryString_Plaintext:
|
||||
if (empty($this->queryParamUsername))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (empty($this->queryParamPassword))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$username = $input->get($this->queryParamUsername, '', 'raw');
|
||||
$password = $input->get($this->queryParamPassword, '', 'raw');
|
||||
|
||||
if (empty($username))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (empty($password))
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a list of transparent authentication methods (array or comma separated list of integers or method names)
|
||||
* and converts it into an array of integers this class understands.
|
||||
*
|
||||
* @param string|int[]|string[] $methods
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function parseAuthenticationMethods($methods): array
|
||||
{
|
||||
if (empty($methods))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!is_array($methods))
|
||||
{
|
||||
$methods = explode(',', $methods);
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($methods as $method)
|
||||
{
|
||||
if (empty($method))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$method = trim($method);
|
||||
|
||||
if ((int) $method == $method)
|
||||
{
|
||||
$return[] = (int) $method;
|
||||
}
|
||||
|
||||
switch ($method)
|
||||
{
|
||||
case 'HTTPBasicAuth_TOTP':
|
||||
$return[] = 1;
|
||||
break;
|
||||
|
||||
case 'QueryString_TOTP':
|
||||
$return[] = 2;
|
||||
break;
|
||||
|
||||
case 'HTTPBasicAuth_Plaintext':
|
||||
$return[] = 3;
|
||||
break;
|
||||
|
||||
case 'QueryString_Plaintext':
|
||||
$return[] = 4;
|
||||
break;
|
||||
|
||||
case 'SplitQueryString_Plaintext':
|
||||
$return[] = 5;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts a transparent authentication message using a TOTP
|
||||
*
|
||||
* @param string $encryptedData The encrypted data
|
||||
*
|
||||
* @return array|null The decrypted data
|
||||
*/
|
||||
private function decryptWithTOTP(string $encryptedData): ?array
|
||||
{
|
||||
if (empty($this->totpKey))
|
||||
{
|
||||
$this->cryptoKey = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$totp = new Totp($this->timeStep);
|
||||
$period = $totp->getPeriod();
|
||||
$period--;
|
||||
|
||||
for ($i = 0; $i <= 2; $i++)
|
||||
{
|
||||
$time = ($period + $i) * $this->timeStep;
|
||||
$otp = $totp->getCode($this->totpKey, $time);
|
||||
$this->cryptoKey = hash('sha256', $this->totpKey . $otp);
|
||||
|
||||
$aes = new Aes();
|
||||
$aes->setPassword($this->cryptoKey);
|
||||
|
||||
try
|
||||
{
|
||||
$ret = $aes->decryptString($encryptedData);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$ret = rtrim($ret, "\000");
|
||||
|
||||
$ret = json_decode($ret, true);
|
||||
|
||||
if (!is_array($ret))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!array_key_exists('username', $ret))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!array_key_exists('password', $ret))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Successful decryption!
|
||||
return $ret;
|
||||
}
|
||||
|
||||
// Obviously if we're here we could not decrypt anything. Bail out.
|
||||
$this->cryptoKey = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user