diff --git a/README.md b/README.md index 12dd3ffd5e46f267c812e2027cc4b64b888a1f8d..9b35ec2c7c177d73264180987be786b29cadfab8 100644 --- a/README.md +++ b/README.md @@ -173,21 +173,23 @@ In addition to the remember me function, you can turn on security images. Image `showFreshImage` - if set to true, the security image is fetched everytime user access the login page. Otherwise, it is stored in the cookie. Default `false`. +`storageClass` - an implementation of `SimpleSAML\Module\campusmultiauth\Data\Storage`, default `SimpleSAML\Module\campusmultiauth\Data\DatabaseStorage`. + +`pictureStorage` - if some other storage than `SimpleSAML\Module\campusmultiauth\Data\DatabaseStorage` is used (e.g. `SimpleSAML\Module\campusmultiauth\Data\PerunStorage`), this is the place for the configuration of the storage. + +`security.cookie.path` - cookie path. + +`security.cookie.samesite` - cookie SameSite. + `pictureDir` - if set, the security image is stored in this directory instead of the cookie. The cookie than contains only a link to the picture. Also, if this option is enabled, `securityImageSalt` and `pictureBaseURL` are mandatory. Default `null`. `securityImageSalt` - a salt which is used in the filename of the picture if the `pictureDir` is on. `pictureBaseURL` - base URL to the pictures if the `pictureDir` is on. -`storageClass` - an implementation of `SimpleSAML\Module\campusmultiauth\Data\Storage`, default `SimpleSAML\Module\campusmultiauth\Data\DatabaseStorage`. - `pictures_table` - name of the table with security images, default `security_image`. -`pictureStorage` - if some other storage than `SimpleSAML\Module\campusmultiauth\Data\DatabaseStorage` is used (e.g. `SimpleSAML\Module\campusmultiauth\Data\PerunStorage`), this is the place for the configuration of the storage. - -`security.cookie.path` - cookie path. - -`security.cookie.samesite` - cookie SameSite. +`texts_table` - default `alternative_text`. You can also add an alternative text to images. User can specify his/her own text, so this is an additional antiphishing feature. If user does not have the alternative text set, the alt is an empty string. In case he/she does not have the image set, this text will be displayed instead of it. ## Hinting diff --git a/config-templates/module_campusmultiauth.php b/config-templates/module_campusmultiauth.php index 81c68d766c7ef1d9a48e2820567368d25a3cd9da..d466b1ef90b2d6546bd5c29f502335e2f1b7eae3 100644 --- a/config-templates/module_campusmultiauth.php +++ b/config-templates/module_campusmultiauth.php @@ -140,6 +140,7 @@ $config = [ // 'ldap.basedn' => '', // 'search.filter' => '', // 'attribute' => '', + // 'alternative_text_attribute' => '', // ], ], // 'uidName' => '', diff --git a/lib/Auth/Process/RememberMe.php b/lib/Auth/Process/RememberMe.php index e801fe5446d35ae899b2f6a721a427a29b4ae8d7..74573d33ff2f531c6aef3dfb8b67708ee9d790c4 100644 --- a/lib/Auth/Process/RememberMe.php +++ b/lib/Auth/Process/RememberMe.php @@ -203,6 +203,7 @@ class RememberMe extends ProcessingFilter if (!$this->showFreshImage) { $payload['security_image'] = Utils::getSecurityImageOfUser($username); + $payload['alternative_text'] = Utils::getAlternativeTextOfUser($username); } $this->setCookie($payload); diff --git a/lib/Data/DatabaseStorage.php b/lib/Data/DatabaseStorage.php index 061c8d0145d248a8adf2d83d6cc2ee15d31ac3d8..be5d60f3c3e19101bf136fd915b871865a5804d5 100644 --- a/lib/Data/DatabaseStorage.php +++ b/lib/Data/DatabaseStorage.php @@ -22,6 +22,11 @@ class DatabaseStorage implements Storage */ private $pictures_table; + /** + * DB table name for texts. + */ + private $texts_table; + /** * DB table name for tokens. */ @@ -48,9 +53,13 @@ class DatabaseStorage implements Storage $imagesConfiguration = $this->config->getConfigItem('security_images', []); $this->db = Database::getInstance($this->config->getConfigItem('store', [])); + $this->pictures_table = $this->db->applyPrefix( $imagesConfiguration->getString('pictures_table', 'security_image') ); + $this->texts_table = $this->db->applyPrefix( + $imagesConfiguration->getString('texts_table', 'alternative_text') + ); $this->tokens_table = $this->db->applyPrefix( $this->config->getString('tokens_table', 'cookie_counter') ); @@ -61,17 +70,17 @@ class DatabaseStorage implements Storage */ public function getSecurityImageOfUser(string $uid): ?string { - $query = 'SELECT picture FROM ' . $this->pictures_table - . ' WHERE ' . self::UID_COL . '=:userid'; - $statement = $this->db->read($query, [ - 'userid' => $uid, - ]); - $picture = $statement->fetchColumn(); - if ($picture === false) { - return null; - } + $query = 'SELECT picture FROM ' . $this->pictures_table . ' WHERE ' . self::UID_COL . '=:userid'; + return $this->getSecurityAttributeOfUser($uid, $query); + } - return $picture; + /** + * @override + */ + public function getAlternativeTextOfUser(string $uid): ?string + { + $query = 'SELECT alternative_text FROM ' . $this->texts_table . ' WHERE ' . self::UID_COL . '=:userid'; + return $this->getSecurityAttributeOfUser($uid, $query); } /** @@ -140,4 +149,18 @@ class DatabaseStorage implements Storage return (bool) $this->db->write($query, $params); } + + private function getSecurityAttributeOfUser(string $uid, string $query) + { + $statement = $this->db->read($query, [ + 'userid' => $uid, + ]); + + $attribute = $statement->fetchColumn(); + if ($attribute === false) { + return null; + } + + return $attribute; + } } diff --git a/lib/Data/PerunStorage.php b/lib/Data/PerunStorage.php index ab7d44cb62db9e8b8214778ae8f3b0fdbda0cb8d..bfffdb9905bd80e6f6eb748b23da72040944b639 100644 --- a/lib/Data/PerunStorage.php +++ b/lib/Data/PerunStorage.php @@ -59,11 +59,25 @@ class PerunStorage extends DatabaseStorage * @override */ public function getSecurityImageOfUser(string $uid): ?string + { + $attribute = $this->config->getString('attribute', null); + return $attribute === null ? null : $this->getSecurityAttributeOfUser($uid, $attribute); + } + + /** + * @override + */ + public function getAlternativeTextOfUser(string $uid): ?string + { + $attribute = $this->config->getString('alternative_text_attribute', null); + return $attribute === null ? null : $this->getSecurityAttributeOfUser($uid, $attribute); + } + + private function getSecurityAttributeOfUser(string $uid, string $attribute) { $base = $this->config->getString('ldap.basedn'); $filter = $this->config->getString('search.filter'); $filter = str_replace('%uid%', $uid, $filter); - $attribute = $this->config->getString('attribute'); try { $entries = $this->ldap->searchformultiple([$base], $filter, [$attribute], [], true, false); diff --git a/lib/Data/Storage.php b/lib/Data/Storage.php index da9f926648d2817c35076d5e8fe62a59ef448098..4ee55d64ef7004cf27087b5fb23390d33b7de5d8 100644 --- a/lib/Data/Storage.php +++ b/lib/Data/Storage.php @@ -13,6 +13,11 @@ interface Storage */ public function getSecurityImageOfUser(string $uid): ?string; + /** + * Null if user has none, text otherwise. + */ + public function getAlternativeTextOfUser(string $uid): ?string; + /** * False if not found (should not happen), counter otherwise. */ diff --git a/lib/Utils.php b/lib/Utils.php index ec4bfa6f9d52382f6e58f7766dc9e2eaa4157407..e36887e327843fb37bcb59df49068445b560bce8 100644 --- a/lib/Utils.php +++ b/lib/Utils.php @@ -29,4 +29,15 @@ class Utils return $storage->getSecurityImageOfUser($username); } + + public static function getAlternativeTextOfUser($username) + { + $storage = self::getInterfaceInstance( + 'SimpleSAML\\Module\\campusmultiauth\\Data\\Storage', + 'storageClass', + 'SimpleSAML\\Module\\campusmultiauth\\Data\\DatabaseStorage' + ); + + return $storage->getAlternativeTextOfUser($username); + } } diff --git a/templates/includes/local-login.twig b/templates/includes/local-login.twig index 1529c9425c1d1fc035e686984b50d08bf286d351..553e100cc2fc9a776037b55819bb2b84ecfc8998 100644 --- a/templates/includes/local-login.twig +++ b/templates/includes/local-login.twig @@ -13,7 +13,9 @@ </h4> {% if securityImage is defined %} - <img src='{{ securityImage|escape }}' class='security-image' alt=''> + <img src='{{ securityImage|escape('html_attr') }}' class='security-image' alt='{{ alternativeText|default('')|escape('html_attr') }}'> + {% elseif alternativeText is defined %} + <p class="security-image-text">{{ alternativeText|escape }}</p> {% endif %} {% if wrongUserPass == true %} diff --git a/www/selectsource.php b/www/selectsource.php index 16aba55c86f0090e7d1c2f9981d8779972e2a8d3..bc2e18386cce901290096542e395c781b56a34be 100644 --- a/www/selectsource.php +++ b/www/selectsource.php @@ -343,14 +343,21 @@ if ($t->data['userInfo']) { if (empty($t->data['username']) || $t->data['userInfo']['username'] === $t->data['username']) { $t->data['username'] = $t->data['userInfo']['username']; $showFreshImage = $imagesConfig->getBoolean('showFreshImage', false); + if ($showFreshImage && (($t->data['userInfo']['security_image'] ?? true) !== false)) { $t->data['securityImage'] = Utils::getSecurityImageOfUser($t->data['userInfo']['username']); } elseif (!$showFreshImage && !empty($t->data['userInfo']['security_image'])) { $t->data['securityImage'] = $t->data['userInfo']['security_image']; } + if ($showFreshImage && (($t->data['userInfo']['alternative_text'] ?? true) !== false)) { + $t->data['alternativeText'] = Utils::getAlternativeTextOfUser($t->data['userInfo']['username']); + } elseif (!$showFreshImage && !empty($t->data['userInfo']['alternative_text'])) { + $t->data['alternativeText'] = $t->data['userInfo']['alternative_text']; + } + $pictureDir = $imagesConfig->getString('pictureDir', null); - if ($t->data['securityImage'] && $pictureDir !== null) { + if (!empty($t->data['securityImage']) && $pictureDir !== null) { $pictureDataSrc = $t->data['securityImage']; if (preg_match('~^data:image/(png|jpeg|gif);base64,(.*)$~', $pictureDataSrc, $matches)) { list(, $pictureType, $pictureContent) = $matches;