summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Web/RememberMe.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Web/RememberMe.php')
-rw-r--r--library/Icinga/Web/RememberMe.php363
1 files changed, 363 insertions, 0 deletions
diff --git a/library/Icinga/Web/RememberMe.php b/library/Icinga/Web/RememberMe.php
new file mode 100644
index 0000000..1002396
--- /dev/null
+++ b/library/Icinga/Web/RememberMe.php
@@ -0,0 +1,363 @@
+<?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;
+ }
+}