633 lines
13 KiB
PHP
633 lines
13 KiB
PHP
<?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;
|
|
}
|
|
}
|