364 lines
9.0 KiB
PHP
364 lines
9.0 KiB
PHP
<?php
|
|
/* Icinga Web 2 | (c) 2021 Icinga GmbH | GPLv2+ */
|
|
|
|
namespace Icinga\Web;
|
|
|
|
use Icinga\Application\Config;
|
|
use Icinga\Authentication\Auth;
|
|
use Icinga\Crypt\AesCrypt;
|
|
use Icinga\Common\Database;
|
|
use Icinga\User;
|
|
use ipl\Sql\Expression;
|
|
use ipl\Sql\Select;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Remember me component
|
|
*
|
|
* Retains credentials for 30 days by default in order to stay signed in even after the session is closed.
|
|
*/
|
|
class RememberMe
|
|
{
|
|
use Database;
|
|
|
|
/** @var string Cookie name */
|
|
const COOKIE = 'icingaweb2-remember-me';
|
|
|
|
/** @var string Database table name */
|
|
const TABLE = 'icingaweb_rememberme';
|
|
|
|
/** @var string Encrypted password of the user */
|
|
protected $encryptedPassword;
|
|
|
|
/** @var string */
|
|
protected $username;
|
|
|
|
/** @var AesCrypt Instance for encrypting/decrypting the credentials */
|
|
protected $aesCrypt;
|
|
|
|
/** @var int Timestamp when the remember me cookie expires */
|
|
protected $expiresAt;
|
|
|
|
/**
|
|
* Get whether staying logged in is possible
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function isSupported()
|
|
{
|
|
$self = new self();
|
|
|
|
if (! $self->hasDb()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
(new AesCrypt())->getMethod();
|
|
} catch (RuntimeException $_) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get whether the remember cookie is set
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function hasCookie()
|
|
{
|
|
return isset($_COOKIE[static::COOKIE]);
|
|
}
|
|
|
|
/**
|
|
* Remove the database entry if exists and unset the remember me cookie from PHP's `$_COOKIE` superglobal
|
|
*
|
|
* @return Cookie The invalidation cookie which has to be sent to client in oder to remove the remember me cookie
|
|
*/
|
|
public static function forget()
|
|
{
|
|
if (self::hasCookie()) {
|
|
$data = explode('|', $_COOKIE[static::COOKIE]);
|
|
$iv = base64_decode(array_pop($data));
|
|
(new self())->remove(bin2hex($iv));
|
|
}
|
|
|
|
unset($_COOKIE[static::COOKIE]);
|
|
|
|
return (new Cookie(static::COOKIE))
|
|
->setHttpOnly(true)
|
|
->forgetMe();
|
|
}
|
|
|
|
/**
|
|
* Create the remember me component from the remember me cookie
|
|
*
|
|
* @return static
|
|
*/
|
|
public static function fromCookie()
|
|
{
|
|
$data = explode('|', $_COOKIE[static::COOKIE]);
|
|
$iv = base64_decode(array_pop($data));
|
|
|
|
$select = (new Select())
|
|
->from(static::TABLE)
|
|
->columns('*')
|
|
->where(['random_iv = ?' => bin2hex($iv)]);
|
|
|
|
$rememberMe = new static();
|
|
$rs = $rememberMe->getDb()->select($select)->fetch();
|
|
|
|
if (! $rs) {
|
|
throw new RuntimeException(sprintf(
|
|
"No database entry found for IV '%s'",
|
|
bin2hex($iv)
|
|
));
|
|
}
|
|
|
|
$rememberMe->aesCrypt = (new AesCrypt())
|
|
->setKey(hex2bin($rs->passphrase))
|
|
->setIV($iv);
|
|
|
|
if (count($data) > 1) {
|
|
$rememberMe->aesCrypt->setTag(
|
|
base64_decode(array_pop($data))
|
|
);
|
|
} elseif ($rememberMe->aesCrypt->isAuthenticatedEncryptionRequired()) {
|
|
throw new RuntimeException(
|
|
"The given decryption method needs a tag, but is not specified. "
|
|
. "You have probably updated the PHP version."
|
|
);
|
|
}
|
|
|
|
$rememberMe->username = $rs->username;
|
|
$rememberMe->encryptedPassword = $data[0];
|
|
|
|
return $rememberMe;
|
|
}
|
|
|
|
/**
|
|
* Create the remember me component from the given username and password
|
|
*
|
|
* @param string $username
|
|
* @param string $password
|
|
*
|
|
* @return static
|
|
*/
|
|
public static function fromCredentials($username, $password)
|
|
{
|
|
$aesCrypt = new AesCrypt();
|
|
$rememberMe = new static();
|
|
$rememberMe->encryptedPassword = $aesCrypt->encrypt($password);
|
|
$rememberMe->username = $username;
|
|
$rememberMe->aesCrypt = $aesCrypt;
|
|
|
|
return $rememberMe;
|
|
}
|
|
|
|
/**
|
|
* Remove expired remember me information from the database
|
|
*/
|
|
public static function removeExpired()
|
|
{
|
|
$rememberMe = new static();
|
|
if (! $rememberMe->hasDb()) {
|
|
return;
|
|
}
|
|
|
|
$rememberMe->getDb()->delete(static::TABLE, [
|
|
'expires_at < NOW()'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get the remember me cookie
|
|
*
|
|
* @return Cookie
|
|
*/
|
|
public function getCookie()
|
|
{
|
|
$values = [
|
|
$this->encryptedPassword,
|
|
base64_encode($this->aesCrypt->getIV()),
|
|
];
|
|
|
|
if ($this->aesCrypt->isAuthenticatedEncryptionRequired()) {
|
|
array_splice($values, 1, 0, base64_encode($this->aesCrypt->getTag()));
|
|
}
|
|
|
|
return (new Cookie(static::COOKIE))
|
|
->setExpire($this->getExpiresAt())
|
|
->setHttpOnly(true)
|
|
->setValue(implode('|', $values));
|
|
}
|
|
|
|
/**
|
|
* Get the timestamp when the cookie expires
|
|
*
|
|
* Defaults to now plus 30 days, if not set via {@link setExpiresAt()}.
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getExpiresAt()
|
|
{
|
|
if ($this->expiresAt === null) {
|
|
$this->expiresAt = time() + 60 * 60 * 24 * 30;
|
|
}
|
|
|
|
return $this->expiresAt;
|
|
}
|
|
|
|
/**
|
|
* Set the timestamp when the cookie expires
|
|
*
|
|
* @param int $expiresAt
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setExpiresAt($expiresAt)
|
|
{
|
|
$this->expiresAt = $expiresAt;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Authenticate via the remember me cookie
|
|
*
|
|
* @return bool
|
|
*
|
|
* @throws \Icinga\Exception\AuthenticationException
|
|
*/
|
|
public function authenticate()
|
|
{
|
|
$auth = Auth::getInstance();
|
|
$authChain = $auth->getAuthChain();
|
|
$authChain->setSkipExternalBackends(true);
|
|
$user = new User($this->username);
|
|
if (! $user->hasDomain()) {
|
|
$user->setDomain(Config::app()->get('authentication', 'default_domain'));
|
|
}
|
|
|
|
$authenticated = $authChain->authenticate(
|
|
$user,
|
|
$this->aesCrypt->decrypt($this->encryptedPassword)
|
|
);
|
|
|
|
if ($authenticated) {
|
|
$auth->setAuthenticated($user);
|
|
}
|
|
|
|
return $authenticated;
|
|
}
|
|
|
|
/**
|
|
* Persist the remember me information into the database
|
|
*
|
|
* To remove any previous stored information, set the iv
|
|
*
|
|
* @param string|null $iv To remove a specific iv record from the database
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function persist($iv = null)
|
|
{
|
|
if ($iv) {
|
|
$this->remove(bin2hex($iv));
|
|
}
|
|
|
|
$this->getDb()->insert(static::TABLE, [
|
|
'username' => $this->username,
|
|
'passphrase' => bin2hex($this->aesCrypt->getKey()),
|
|
'random_iv' => bin2hex($this->aesCrypt->getIV()),
|
|
'http_user_agent' => (new UserAgent)->getAgent(),
|
|
'expires_at' => date('Y-m-d H:i:s', $this->getExpiresAt()),
|
|
'ctime' => new Expression('NOW()'),
|
|
'mtime' => new Expression('NOW()')
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Remove remember me information from the database on the basis of iv
|
|
*
|
|
* @param string $iv
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function remove($iv)
|
|
{
|
|
$this->getDb()->delete(static::TABLE, [
|
|
'random_iv = ?' => $iv
|
|
]);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Create renewed remember me cookie
|
|
*
|
|
* @return static New remember me cookie which has to be sent to the client
|
|
*/
|
|
public function renew()
|
|
{
|
|
return static::fromCredentials(
|
|
$this->username,
|
|
$this->aesCrypt->decrypt($this->encryptedPassword)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get all users using remember me cookie
|
|
*
|
|
* @return array Array of users
|
|
*/
|
|
public static function getAllUser()
|
|
{
|
|
$rememberMe = new static();
|
|
if (! $rememberMe->hasDb()) {
|
|
return [];
|
|
}
|
|
|
|
$select = (new Select())
|
|
->from(static::TABLE)
|
|
->columns('username')
|
|
->groupBy('username');
|
|
|
|
return $rememberMe->getDb()->select($select)->fetchAll();
|
|
}
|
|
|
|
/**
|
|
* Get all remember me entries from the database of the given user.
|
|
*
|
|
* @param $username
|
|
*
|
|
* @return array Array of database entries
|
|
*/
|
|
public static function getAllByUsername($username)
|
|
{
|
|
$rememberMe = new static();
|
|
if (! $rememberMe->hasDb()) {
|
|
return [];
|
|
}
|
|
|
|
$select = (new Select())
|
|
->from(static::TABLE)
|
|
->columns(['http_user_agent', 'random_iv'])
|
|
->where(['username = ?' => $username]);
|
|
|
|
return $rememberMe->getDb()->select($select)->fetchAll();
|
|
}
|
|
|
|
/**
|
|
* Get the AesCrypt instance
|
|
*
|
|
* @return AesCrypt
|
|
*/
|
|
public function getAesCrypt()
|
|
{
|
|
return $this->aesCrypt;
|
|
}
|
|
}
|