diff --git a/README.md b/README.md
index 1600b14983f4c44b578063b5d638fb97d1d5280c..12dd3ffd5e46f267c812e2027cc4b64b888a1f8d 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ To achieve this, you need to define and configure an authentication source in yo
         ]
     ],
 
-Let's look at the configuration options:
+The following configuration options are available:
 
 `campusmultiauth:campusidp` defines which module and authentication source to use. This is the only mandatory option.
 
@@ -43,7 +43,7 @@ Of course, both authsources must be defined in authsources.php file. When the co
 
 ## Login page configuration
 
-The second part of the configuration is setting up the login page itself. While doing that, it's highly recommended to follow [our suggestions (Czech only)](https://gitlab.ics.muni.cz/perun-proxy-aai/simplesamlphp/simplesamlphp-module-campusmultiauth/-/wikis/Konfigura%C4%8Dn%C3%AD-doporu%C4%8Den%C3%AD). To configure the login page, you need to create a new configuration file `module_campusmultiauth.php`. In this module, there is an example configuration available at `config-templates/module_campusmultiauth.php`. In configuration file, there are following options available:
+The second part of the configuration is setting up the login page itself. While doing that, it is highly recommended to follow [our suggestions (Czech only)](https://gitlab.ics.muni.cz/perun-proxy-aai/simplesamlphp/simplesamlphp-module-campusmultiauth/-/wikis/Konfigura%C4%8Dn%C3%AD-doporu%C4%8Den%C3%AD). To configure the login page, you need to create a new configuration file `module_campusmultiauth.php`. In this module, there is an example configuration available at `config-templates/module_campusmultiauth.php`. In configuration file, there are following options available:
 
 `css_framework` - if set to `muni_jvs`, the login page displays in MUNI framework. Otherwise, Bootstrap 5 is used.
 
@@ -69,11 +69,11 @@ Footer defines the bottom of the login page. If it is not set, the footer is emp
 
 ### Components
 
-The main part of the login page. The `components` option is designed as a list, where each element represents one component. A component is a map with several possible options. The most important option is `name`. It defines the component's type. There are three possible values for `name`: `local_login`, `searchbox` and `individual_identities`. Let's take a look at each component's type separately.
+The main part of the login page. The `components` option is designed as a list, where each element represents one component. A component is a map with several possible options. The most important option is `name`. It defines the component's type. There are three possible values for `name`: `local_login`, `searchbox` and `individual_identities`.
 
 #### local_login
 
-This component represents a form with username and password. It can be used only once. It's possible to show / hide the `remember_me` checkbox by configuring the `session.rememberme.enable` option in the `config.php` file. In the module configuration, there are following options:
+This component represents a form with username and password. It can be used only once. For the Remember me functionality, see below. In the module configuration, there are following options:
 
 `username_label` - this is displayed as a label above input for the username. If you want to add localization, you can write the value as a map with language codes as keys and localized strings as values. If current language is not found in keys, the **_first one_** is used instead. If not set at all, it displays a default value.
 
@@ -97,7 +97,7 @@ Thanks to the searchbox you can search between all included identity providers.
 
 `placeholder` - text displayed as a placeholder in the searchbox. If you want to add localization, you can write the value as a map with language codes as keys and localized strings as values. If current language is not found in keys, the **_first one_** is used instead. If not set at all, it displays a default value.
 
-`filter` - if you want to display just part of identity providers available in the metadata, you can use this option. If not set, all identity providers from the metadata are included. Otherwise, identity providers to display are chosen based on the [aarc_discovery_hint](https://docs.google.com/document/d/1rHKGzPsjkbqKHxsPnCb0itRLXLtqm-A8CZ5fzzklaxc/edit) logic. However, there are some differences. The content of this option is already decoded (which means it's in the PHP format, not the JSON). Also, you can use the `entityid` claim (instead of `entity_category` / `assurance_certification` / `registration_authority`) to include or exclude specific identity providers. You can find a sample use of the `entityid` claim in [module_campusmultiauth.php](https://gitlab.ics.muni.cz/perun-proxy-aai/simplesamlphp/simplesamlphp-module-campusmultiauth/-/blob/main/config-templates/module_campusmultiauth.php) config template.
+`filter` - if you want to display just part of identity providers available in the metadata, you can use this option. If not set, all identity providers from the metadata are included. Otherwise, identity providers to display are chosen based on the [aarc_discovery_hint](https://docs.google.com/document/d/1rHKGzPsjkbqKHxsPnCb0itRLXLtqm-A8CZ5fzzklaxc/edit) logic. However, there are some differences. The content of this option is already decoded (which means it is in the PHP format, not the JSON). Also, you can use the `entityid` claim (instead of `entity_category` / `assurance_certification` / `registration_authority`) to include or exclude specific identity providers. You can find a sample use of the `entityid` claim in [module_campusmultiauth.php](https://gitlab.ics.muni.cz/perun-proxy-aai/simplesamlphp/simplesamlphp-module-campusmultiauth/-/blob/main/config-templates/module_campusmultiauth.php) config template.
 
 `priority` - can be set to `primary`, default value is `secondary`. It should be primary if you want users to use this component if they are able to.
 
@@ -131,6 +131,64 @@ Each identity is a map with the following possible options:
 
 `background_color` - background around the logo. Defined as a [CSS color value](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
 
+### Remember me and security images
+
+You can add a `remember_me` section to your configuration file to add some convenience and anti-phishing features to your `local_login` component.
+
+#### Remember me
+
+To enable the `remember_me` checkbox and optionally set it as checked by default, configure `session.rememberme.enable` and `session.rememberme.checked` options in the `config.php` file. If you want to make session longer if the checkbox is checked, use the [ExtendIdPSession](https://github.com/simplesamlphp/simplesamlphp/blob/v1.19.6/modules/core/lib/Auth/Process/ExtendIdPSession.php) auth proc filter. It is highly recommended to also set a [session checking function](https://simplesamlphp.org/docs/1.19/simplesamlphp-advancedfeatures.html#session-checking-function).
+
+You can store info about the user in the cookie, including a counter of user's visits of the login page, which is compared to the value stored in the database. This can help to detect some suspicious behaviour. To enable this feature, you have to enable the `remember_me` checkbox mentioned above and add the `RememberMe` auth proc filter. Then set the following options in the `remember_me` section of the configuration file:
+
+`nameAttr` - a name of the attribute with the user's name. Has to be present in the `$request['attributes']` in the Authproc filter. The default value is `displayName`.
+
+`store` - a database configuration, used as an argument for the `SimpleSAML\Database::getInstance()` method.
+
+`tokens_table` - a name of the database table where user tokens with counters are stored. The default value is `cookie_counter`.
+
+`signature_key` - a key used for signing JWTs.
+
+`encryption_key` - a key used for encrypting JWTs.
+
+`signature_algorithm` - a signature algorithm, default `HS512`.
+
+`encryption_algorithm` - an encryption algorithm, default `A256GCM`.
+
+`keywrap_algorithm` - a keywrap algorithm, default `A256GCMKW`.
+
+`uid_attribute` - a user's identifier attribute, default `uid`.
+
+`cipherClass` - an implementation of `SimpleSAML\Module\campusmultiauth\Security\Cipher`, default `SimpleSAML\Module\campusmultiauth\Security\JWTCipher`.
+
+`uidName` - value of this option is displayed before the user's uid attribute value, default value is empty string (which will display nothing).
+
+`cookieName` - a cookie name where the info about user is stored, default `campus_userinfo`.
+
+`dontCookieName` - if user decides not to remember login on current device, this decision will also be stored into a cookie. Value of this option is used as the name of this cookie. The default value is `campus_dont_remember`.
+
+#### Security images
+
+In addition to the remember me function, you can turn on security images. Image specific to each user will be shown on the login page if set, which proves it is not a phishing site. To configure this feature, you need to add `security_images` to the `remember_me` section and set:
+
+`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`.
+
+`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.
+
 ## Hinting
 
 To help the user choose the right institution to log in, this module supports following standards:
diff --git a/composer.json b/composer.json
index f08e15108e8ba39998d9de2030f483ed6cef4a17..1314282035bccea3179e71e11eb42daec93a5e7b 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,16 @@
     "simplesamlphp/simplesamlphp": "^1.19",
     "league/commonmark": "^2.0",
     "ext-intl": "*",
-    "ext-simplexml": "*"
+    "ext-curl": "*",
+    "ext-simplexml": "*",
+    "ext-json": "*",
+    "web-token/jwt-core": "^2.2",
+    "web-token/jwt-signature-algorithm-hmac": "^2.2",
+    "web-token/jwt-encryption-algorithm-aesgcmkw": "^2.2",
+    "web-token/jwt-encryption-algorithm-aesgcm": "^2.2",
+    "web-token/jwt-nested-token": "^2.2",
+    "web-token/jwt-checker": "^2.2",
+    "donatj/phpuseragentparser": "^1.0"
   },
   "config": {
     "platform": {
diff --git a/composer.lock b/composer.lock
index 070373919e4798cd780cc7a7fef6b64d564e65d7..9683b0a9634b621f3e79b87394ea7b21ad82c0b8 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,68 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "c92f8c66446013c68aa0cdb876b9f65c",
+    "content-hash": "97bd371d2a0b781d3564b19b45a18d50",
     "packages": [
+        {
+            "name": "brick/math",
+            "version": "0.9.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/brick/math.git",
+                "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae",
+                "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.2",
+                "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0",
+                "vimeo/psalm": "4.9.2"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Brick\\Math\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Arbitrary-precision arithmetic library",
+            "keywords": [
+                "Arbitrary-precision",
+                "BigInteger",
+                "BigRational",
+                "arithmetic",
+                "bigdecimal",
+                "bignum",
+                "brick",
+                "math"
+            ],
+            "support": {
+                "issues": "https://github.com/brick/math/issues",
+                "source": "https://github.com/brick/math/tree/0.9.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/BenMorel",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/brick/math",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2021-08-15T20:50:18+00:00"
+        },
         {
             "name": "dflydev/dot-access-data",
             "version": "v3.0.2",
@@ -81,6 +141,150 @@
             },
             "time": "2022-10-27T11:44:00+00:00"
         },
+        {
+            "name": "donatj/phpuseragentparser",
+            "version": "v1.7.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/donatj/PhpUserAgent.git",
+                "reference": "a35900b93530715f8669c10e49756adde5c8e6fc"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/donatj/PhpUserAgent/zipball/a35900b93530715f8669c10e49756adde5c8e6fc",
+                "reference": "a35900b93530715f8669c10e49756adde5c8e6fc",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0"
+            },
+            "require-dev": {
+                "camspiers/json-pretty": "~1.0",
+                "donatj/drop": "*",
+                "ext-json": "*",
+                "phpunit/phpunit": "~4.8|~9"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/UserAgentParser.php"
+                ],
+                "psr-4": {
+                    "donatj\\UserAgent\\": "src/UserAgent"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jesse G. Donat",
+                    "email": "donatj@gmail.com",
+                    "homepage": "https://donatstudios.com",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Lightning fast, minimalist PHP UserAgent string parser.",
+            "homepage": "https://donatstudios.com/PHP-Parser-HTTP_USER_AGENT",
+            "keywords": [
+                "browser",
+                "browser detection",
+                "parser",
+                "user agent",
+                "useragent"
+            ],
+            "support": {
+                "issues": "https://github.com/donatj/PhpUserAgent/issues",
+                "source": "https://github.com/donatj/PhpUserAgent/tree/v1.7.0"
+            },
+            "funding": [
+                {
+                    "url": "https://www.paypal.me/donatj/15",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/donatj",
+                    "type": "github"
+                }
+            ],
+            "time": "2022-08-06T15:41:58+00:00"
+        },
+        {
+            "name": "fgrosse/phpasn1",
+            "version": "v2.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/fgrosse/PHPASN1.git",
+                "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/eef488991d53e58e60c9554b09b1201ca5ba9296",
+                "reference": "eef488991d53e58e60c9554b09b1201ca5ba9296",
+                "shasum": ""
+            },
+            "require": {
+                "php": "~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "~2.0",
+                "phpunit/phpunit": "^6.3 || ^7.0 || ^8.0"
+            },
+            "suggest": {
+                "ext-bcmath": "BCmath is the fallback extension for big integer calculations",
+                "ext-curl": "For loading OID information from the web if they have not bee defined statically",
+                "ext-gmp": "GMP is the preferred extension for big integer calculations",
+                "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "FG\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Friedrich GroĂźe",
+                    "email": "friedrich.grosse@gmail.com",
+                    "homepage": "https://github.com/FGrosse",
+                    "role": "Author"
+                },
+                {
+                    "name": "All contributors",
+                    "homepage": "https://github.com/FGrosse/PHPASN1/contributors"
+                }
+            ],
+            "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.",
+            "homepage": "https://github.com/FGrosse/PHPASN1",
+            "keywords": [
+                "DER",
+                "asn.1",
+                "asn1",
+                "ber",
+                "binary",
+                "decoding",
+                "encoding",
+                "x.509",
+                "x.690",
+                "x509",
+                "x690"
+            ],
+            "support": {
+                "issues": "https://github.com/fgrosse/PHPASN1/issues",
+                "source": "https://github.com/fgrosse/PHPASN1/tree/v2.4.0"
+            },
+            "time": "2021-12-11T12:41:06+00:00"
+        },
         {
             "name": "gettext/gettext",
             "version": "v4.8.7",
@@ -238,16 +442,16 @@
         },
         {
             "name": "league/commonmark",
-            "version": "2.3.6",
+            "version": "2.3.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/commonmark.git",
-                "reference": "857afc47ce113454bd629037213378ba3219dd40"
+                "reference": "a36bd2be4f5387c0f3a8792a0d76b7d68865abbf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/857afc47ce113454bd629037213378ba3219dd40",
-                "reference": "857afc47ce113454bd629037213378ba3219dd40",
+                "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/a36bd2be4f5387c0f3a8792a0d76b7d68865abbf",
+                "reference": "a36bd2be4f5387c0f3a8792a0d76b7d68865abbf",
                 "shasum": ""
             },
             "require": {
@@ -340,7 +544,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-30T16:45:38+00:00"
+            "time": "2022-11-03T17:29:46+00:00"
         },
         {
             "name": "league/config",
@@ -2758,6 +2962,141 @@
             },
             "time": "2020-08-27T12:51:10+00:00"
         },
+        {
+            "name": "spomky-labs/aes-key-wrap",
+            "version": "v6.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Spomky-Labs/aes-key-wrap.git",
+                "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Spomky-Labs/aes-key-wrap/zipball/97388255a37ad6fb1ed332d07e61fa2b7bb62e0d",
+                "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d",
+                "shasum": ""
+            },
+            "require": {
+                "ext-mbstring": "*",
+                "lib-openssl": "*",
+                "php": ">=7.2",
+                "thecodingmachine/safe": "^1.1"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.0",
+                "phpstan/phpstan": "^0.12",
+                "phpstan/phpstan-beberlei-assert": "^0.12",
+                "phpstan/phpstan-deprecation-rules": "^0.12",
+                "phpstan/phpstan-phpunit": "^0.12",
+                "phpstan/phpstan-strict-rules": "^0.12",
+                "phpunit/phpunit": "^7.0|^8.0|^9.0",
+                "thecodingmachine/phpstan-safe-rule": "^1.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.0.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "AESKW\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky-Labs/aes-key-wrap/contributors"
+                }
+            ],
+            "description": "AES Key Wrap for PHP.",
+            "homepage": "https://github.com/Spomky-Labs/aes-key-wrap",
+            "keywords": [
+                "A128KW",
+                "A192KW",
+                "A256KW",
+                "RFC3394",
+                "RFC5649",
+                "aes",
+                "key",
+                "padding",
+                "wrap"
+            ],
+            "support": {
+                "issues": "https://github.com/Spomky-Labs/aes-key-wrap/issues",
+                "source": "https://github.com/Spomky-Labs/aes-key-wrap/tree/v6.0.0"
+            },
+            "time": "2020-08-01T14:07:55+00:00"
+        },
+        {
+            "name": "spomky-labs/base64url",
+            "version": "v2.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Spomky-Labs/base64url.git",
+                "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d",
+                "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpstan/extension-installer": "^1.0",
+                "phpstan/phpstan": "^0.11|^0.12",
+                "phpstan/phpstan-beberlei-assert": "^0.11|^0.12",
+                "phpstan/phpstan-deprecation-rules": "^0.11|^0.12",
+                "phpstan/phpstan-phpunit": "^0.11|^0.12",
+                "phpstan/phpstan-strict-rules": "^0.11|^0.12"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Base64Url\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky-Labs/base64url/contributors"
+                }
+            ],
+            "description": "Base 64 URL Safe Encoding/Decoding PHP Library",
+            "homepage": "https://github.com/Spomky-Labs/base64url",
+            "keywords": [
+                "base64",
+                "rfc4648",
+                "safe",
+                "url"
+            ],
+            "support": {
+                "issues": "https://github.com/Spomky-Labs/base64url/issues",
+                "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/Spomky",
+                    "type": "github"
+                },
+                {
+                    "url": "https://www.patreon.com/FlorentMorselli",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2020-11-03T09:10:25+00:00"
+        },
         {
             "name": "symfony/cache",
             "version": "v5.4.15",
@@ -5112,6 +5451,145 @@
             ],
             "time": "2022-10-03T15:15:50+00:00"
         },
+        {
+            "name": "thecodingmachine/safe",
+            "version": "v1.3.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/thecodingmachine/safe.git",
+                "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc",
+                "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2"
+            },
+            "require-dev": {
+                "phpstan/phpstan": "^0.12",
+                "squizlabs/php_codesniffer": "^3.2",
+                "thecodingmachine/phpstan-strict-rules": "^0.12"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "0.1-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "deprecated/apc.php",
+                    "deprecated/libevent.php",
+                    "deprecated/mssql.php",
+                    "deprecated/stats.php",
+                    "lib/special_cases.php",
+                    "generated/apache.php",
+                    "generated/apcu.php",
+                    "generated/array.php",
+                    "generated/bzip2.php",
+                    "generated/calendar.php",
+                    "generated/classobj.php",
+                    "generated/com.php",
+                    "generated/cubrid.php",
+                    "generated/curl.php",
+                    "generated/datetime.php",
+                    "generated/dir.php",
+                    "generated/eio.php",
+                    "generated/errorfunc.php",
+                    "generated/exec.php",
+                    "generated/fileinfo.php",
+                    "generated/filesystem.php",
+                    "generated/filter.php",
+                    "generated/fpm.php",
+                    "generated/ftp.php",
+                    "generated/funchand.php",
+                    "generated/gmp.php",
+                    "generated/gnupg.php",
+                    "generated/hash.php",
+                    "generated/ibase.php",
+                    "generated/ibmDb2.php",
+                    "generated/iconv.php",
+                    "generated/image.php",
+                    "generated/imap.php",
+                    "generated/info.php",
+                    "generated/ingres-ii.php",
+                    "generated/inotify.php",
+                    "generated/json.php",
+                    "generated/ldap.php",
+                    "generated/libxml.php",
+                    "generated/lzf.php",
+                    "generated/mailparse.php",
+                    "generated/mbstring.php",
+                    "generated/misc.php",
+                    "generated/msql.php",
+                    "generated/mysql.php",
+                    "generated/mysqli.php",
+                    "generated/mysqlndMs.php",
+                    "generated/mysqlndQc.php",
+                    "generated/network.php",
+                    "generated/oci8.php",
+                    "generated/opcache.php",
+                    "generated/openssl.php",
+                    "generated/outcontrol.php",
+                    "generated/password.php",
+                    "generated/pcntl.php",
+                    "generated/pcre.php",
+                    "generated/pdf.php",
+                    "generated/pgsql.php",
+                    "generated/posix.php",
+                    "generated/ps.php",
+                    "generated/pspell.php",
+                    "generated/readline.php",
+                    "generated/rpminfo.php",
+                    "generated/rrd.php",
+                    "generated/sem.php",
+                    "generated/session.php",
+                    "generated/shmop.php",
+                    "generated/simplexml.php",
+                    "generated/sockets.php",
+                    "generated/sodium.php",
+                    "generated/solr.php",
+                    "generated/spl.php",
+                    "generated/sqlsrv.php",
+                    "generated/ssdeep.php",
+                    "generated/ssh2.php",
+                    "generated/stream.php",
+                    "generated/strings.php",
+                    "generated/swoole.php",
+                    "generated/uodbc.php",
+                    "generated/uopz.php",
+                    "generated/url.php",
+                    "generated/var.php",
+                    "generated/xdiff.php",
+                    "generated/xml.php",
+                    "generated/xmlrpc.php",
+                    "generated/yaml.php",
+                    "generated/yaz.php",
+                    "generated/zip.php",
+                    "generated/zlib.php"
+                ],
+                "psr-4": {
+                    "Safe\\": [
+                        "lib/",
+                        "deprecated/",
+                        "generated/"
+                    ]
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "PHP core functions that throw exceptions instead of returning FALSE on error",
+            "support": {
+                "issues": "https://github.com/thecodingmachine/safe/issues",
+                "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3"
+            },
+            "time": "2020-10-28T17:51:34+00:00"
+        },
         {
             "name": "twig/extensions",
             "version": "v1.5.4",
@@ -5252,6 +5730,581 @@
             ],
             "time": "2022-09-28T08:40:08+00:00"
         },
+        {
+            "name": "web-token/jwt-checker",
+            "version": "v2.2.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/web-token/jwt-checker.git",
+                "reference": "5f31d98155951739e2fae7455e8466ccddd08f50"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/web-token/jwt-checker/zipball/5f31d98155951739e2fae7455e8466ccddd08f50",
+                "reference": "5f31d98155951739e2fae7455e8466ccddd08f50",
+                "shasum": ""
+            },
+            "require": {
+                "web-token/jwt-core": "^2.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jose\\Component\\Checker\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky"
+                },
+                {
+                    "name": "All contributors",
+                    "homepage": "https://github.com/web-token/jwt-checker/contributors"
+                }
+            ],
+            "description": "Checker component of the JWT Framework.",
+            "homepage": "https://github.com/web-token",
+            "keywords": [
+                "JOSE",
+                "JWE",
+                "JWK",
+                "JWKSet",
+                "JWS",
+                "Jot",
+                "RFC7515",
+                "RFC7516",
+                "RFC7517",
+                "RFC7518",
+                "RFC7519",
+                "RFC7520",
+                "bundle",
+                "jwa",
+                "jwt",
+                "symfony"
+            ],
+            "support": {
+                "source": "https://github.com/web-token/jwt-checker/tree/v2.2.11"
+            },
+            "funding": [
+                {
+                    "url": "https://www.patreon.com/FlorentMorselli",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2021-03-17T14:55:52+00:00"
+        },
+        {
+            "name": "web-token/jwt-core",
+            "version": "v2.2.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/web-token/jwt-core.git",
+                "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/web-token/jwt-core/zipball/53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678",
+                "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678",
+                "shasum": ""
+            },
+            "require": {
+                "brick/math": "^0.8.17|^0.9",
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "fgrosse/phpasn1": "^2.0",
+                "php": ">=7.2",
+                "spomky-labs/base64url": "^1.0|^2.0"
+            },
+            "conflict": {
+                "spomky-labs/jose": "*"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jose\\Component\\Core\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky"
+                },
+                {
+                    "name": "All contributors",
+                    "homepage": "https://github.com/web-token/jwt-framework/contributors"
+                }
+            ],
+            "description": "Core component of the JWT Framework.",
+            "homepage": "https://github.com/web-token",
+            "keywords": [
+                "JOSE",
+                "JWE",
+                "JWK",
+                "JWKSet",
+                "JWS",
+                "Jot",
+                "RFC7515",
+                "RFC7516",
+                "RFC7517",
+                "RFC7518",
+                "RFC7519",
+                "RFC7520",
+                "bundle",
+                "jwa",
+                "jwt",
+                "symfony"
+            ],
+            "support": {
+                "source": "https://github.com/web-token/jwt-core/tree/v2.2.11"
+            },
+            "funding": [
+                {
+                    "url": "https://www.patreon.com/FlorentMorselli",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2021-03-17T14:55:52+00:00"
+        },
+        {
+            "name": "web-token/jwt-encryption",
+            "version": "v2.2.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/web-token/jwt-encryption.git",
+                "reference": "3b8d67d7c5c013750703e7c27f1001544407bbb2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/web-token/jwt-encryption/zipball/3b8d67d7c5c013750703e7c27f1001544407bbb2",
+                "reference": "3b8d67d7c5c013750703e7c27f1001544407bbb2",
+                "shasum": ""
+            },
+            "require": {
+                "web-token/jwt-core": "^2.1"
+            },
+            "suggest": {
+                "web-token/jwt-encryption-algorithm-aescbc": "AES CBC Based Content Encryption Algorithms",
+                "web-token/jwt-encryption-algorithm-aesgcm": "AES GCM Based Content Encryption Algorithms",
+                "web-token/jwt-encryption-algorithm-aesgcmkw": "AES GCM Key Wrapping Based Key Encryption Algorithms",
+                "web-token/jwt-encryption-algorithm-aeskw": "AES Key Wrapping Based Key Encryption Algorithms",
+                "web-token/jwt-encryption-algorithm-dir": "Direct Key Encryption Algorithms",
+                "web-token/jwt-encryption-algorithm-ecdh-es": "ECDH-ES Based Key Encryption Algorithms",
+                "web-token/jwt-encryption-algorithm-experimental": "Experimental Key and Signature Algorithms",
+                "web-token/jwt-encryption-algorithm-pbes2": "PBES2 Based Key Encryption Algorithms",
+                "web-token/jwt-encryption-algorithm-rsa": "RSA Based Key Encryption Algorithms"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jose\\Component\\Encryption\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky"
+                },
+                {
+                    "name": "All contributors",
+                    "homepage": "https://github.com/web-token/jwt-encryption/contributors"
+                }
+            ],
+            "description": "Encryption component of the JWT Framework.",
+            "homepage": "https://github.com/web-token",
+            "keywords": [
+                "JOSE",
+                "JWE",
+                "JWK",
+                "JWKSet",
+                "JWS",
+                "Jot",
+                "RFC7515",
+                "RFC7516",
+                "RFC7517",
+                "RFC7518",
+                "RFC7519",
+                "RFC7520",
+                "bundle",
+                "jwa",
+                "jwt",
+                "symfony"
+            ],
+            "support": {
+                "source": "https://github.com/web-token/jwt-encryption/tree/v2.2.11"
+            },
+            "funding": [
+                {
+                    "url": "https://www.patreon.com/FlorentMorselli",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2021-03-17T14:55:52+00:00"
+        },
+        {
+            "name": "web-token/jwt-encryption-algorithm-aesgcm",
+            "version": "v2.2.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/web-token/jwt-encryption-algorithm-aesgcm.git",
+                "reference": "d42cc486218e4fcf1eaf7238f1a3f704cc777f70"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/web-token/jwt-encryption-algorithm-aesgcm/zipball/d42cc486218e4fcf1eaf7238f1a3f704cc777f70",
+                "reference": "d42cc486218e4fcf1eaf7238f1a3f704cc777f70",
+                "shasum": ""
+            },
+            "require": {
+                "ext-openssl": "*",
+                "web-token/jwt-encryption": "^2.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jose\\Component\\Encryption\\Algorithm\\ContentEncryption\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky"
+                },
+                {
+                    "name": "All contributors",
+                    "homepage": "https://github.com/web-token/jwt-framework/contributors"
+                }
+            ],
+            "description": "AES GCM Based Content Encryption Algorithms the JWT Framework.",
+            "homepage": "https://github.com/web-token",
+            "keywords": [
+                "JOSE",
+                "JWE",
+                "JWK",
+                "JWKSet",
+                "JWS",
+                "Jot",
+                "RFC7515",
+                "RFC7516",
+                "RFC7517",
+                "RFC7518",
+                "RFC7519",
+                "RFC7520",
+                "bundle",
+                "jwa",
+                "jwt",
+                "symfony"
+            ],
+            "support": {
+                "source": "https://github.com/web-token/jwt-encryption-algorithm-aesgcm/tree/v2.2.11"
+            },
+            "funding": [
+                {
+                    "url": "https://www.patreon.com/FlorentMorselli",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2021-01-21T19:18:03+00:00"
+        },
+        {
+            "name": "web-token/jwt-encryption-algorithm-aesgcmkw",
+            "version": "v2.2.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/web-token/jwt-encryption-algorithm-aesgcmkw.git",
+                "reference": "cac6936c3739e7ef147053167b150ff6b996b725"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/web-token/jwt-encryption-algorithm-aesgcmkw/zipball/cac6936c3739e7ef147053167b150ff6b996b725",
+                "reference": "cac6936c3739e7ef147053167b150ff6b996b725",
+                "shasum": ""
+            },
+            "require": {
+                "ext-openssl": "*",
+                "spomky-labs/aes-key-wrap": "^5.0|^6.0",
+                "web-token/jwt-encryption": "^2.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jose\\Component\\Encryption\\Algorithm\\KeyEncryption\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky"
+                },
+                {
+                    "name": "All contributors",
+                    "homepage": "https://github.com/web-token/jwt-framework/contributors"
+                }
+            ],
+            "description": "AES GCM Key Wrapping Based Key Encryption Algorithms the JWT Framework.",
+            "homepage": "https://github.com/web-token",
+            "keywords": [
+                "JOSE",
+                "JWE",
+                "JWK",
+                "JWKSet",
+                "JWS",
+                "Jot",
+                "RFC7515",
+                "RFC7516",
+                "RFC7517",
+                "RFC7518",
+                "RFC7519",
+                "RFC7520",
+                "bundle",
+                "jwa",
+                "jwt",
+                "symfony"
+            ],
+            "support": {
+                "source": "https://github.com/web-token/jwt-encryption-algorithm-aesgcmkw/tree/v2.2.11"
+            },
+            "funding": [
+                {
+                    "url": "https://www.patreon.com/FlorentMorselli",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2021-01-21T19:18:03+00:00"
+        },
+        {
+            "name": "web-token/jwt-nested-token",
+            "version": "v2.2.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/web-token/jwt-nested-token.git",
+                "reference": "2e0f8b4c91f64cf04964afea0483980b6039b493"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/web-token/jwt-nested-token/zipball/2e0f8b4c91f64cf04964afea0483980b6039b493",
+                "reference": "2e0f8b4c91f64cf04964afea0483980b6039b493",
+                "shasum": ""
+            },
+            "require": {
+                "web-token/jwt-encryption": "^2.1",
+                "web-token/jwt-signature": "^2.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jose\\Component\\NestedToken\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky"
+                },
+                {
+                    "name": "All contributors",
+                    "homepage": "https://github.com/web-token/jwt-nested-token/contributors"
+                }
+            ],
+            "description": "Nested Token component of the JWT Framework.",
+            "homepage": "https://github.com/web-token",
+            "keywords": [
+                "JOSE",
+                "JWE",
+                "JWK",
+                "JWKSet",
+                "JWS",
+                "Jot",
+                "RFC7515",
+                "RFC7516",
+                "RFC7517",
+                "RFC7518",
+                "RFC7519",
+                "RFC7520",
+                "bundle",
+                "jwa",
+                "jwt",
+                "symfony"
+            ],
+            "support": {
+                "source": "https://github.com/web-token/jwt-nested-token/tree/v2.2.11"
+            },
+            "funding": [
+                {
+                    "url": "https://www.patreon.com/FlorentMorselli",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2021-01-21T19:18:03+00:00"
+        },
+        {
+            "name": "web-token/jwt-signature",
+            "version": "v2.2.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/web-token/jwt-signature.git",
+                "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/web-token/jwt-signature/zipball/015b59aaf3b6e8fb9f5bd1338845b7464c7d8103",
+                "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103",
+                "shasum": ""
+            },
+            "require": {
+                "web-token/jwt-core": "^2.1"
+            },
+            "suggest": {
+                "web-token/jwt-signature-algorithm-ecdsa": "ECDSA Based Signature Algorithms",
+                "web-token/jwt-signature-algorithm-eddsa": "EdDSA Based Signature Algorithms",
+                "web-token/jwt-signature-algorithm-experimental": "Experimental Signature Algorithms",
+                "web-token/jwt-signature-algorithm-hmac": "HMAC Based Signature Algorithms",
+                "web-token/jwt-signature-algorithm-none": "None Signature Algorithm",
+                "web-token/jwt-signature-algorithm-rsa": "RSA Based Signature Algorithms"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jose\\Component\\Signature\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky"
+                },
+                {
+                    "name": "All contributors",
+                    "homepage": "https://github.com/web-token/jwt-signature/contributors"
+                }
+            ],
+            "description": "Signature component of the JWT Framework.",
+            "homepage": "https://github.com/web-token",
+            "keywords": [
+                "JOSE",
+                "JWE",
+                "JWK",
+                "JWKSet",
+                "JWS",
+                "Jot",
+                "RFC7515",
+                "RFC7516",
+                "RFC7517",
+                "RFC7518",
+                "RFC7519",
+                "RFC7520",
+                "bundle",
+                "jwa",
+                "jwt",
+                "symfony"
+            ],
+            "support": {
+                "source": "https://github.com/web-token/jwt-signature/tree/v2.2.11"
+            },
+            "funding": [
+                {
+                    "url": "https://www.patreon.com/FlorentMorselli",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2021-03-01T19:55:28+00:00"
+        },
+        {
+            "name": "web-token/jwt-signature-algorithm-hmac",
+            "version": "v2.2.11",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/web-token/jwt-signature-algorithm-hmac.git",
+                "reference": "d208b1c50b408fa711bfeedeed9fb5d9be1d3080"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-hmac/zipball/d208b1c50b408fa711bfeedeed9fb5d9be1d3080",
+                "reference": "d208b1c50b408fa711bfeedeed9fb5d9be1d3080",
+                "shasum": ""
+            },
+            "require": {
+                "web-token/jwt-signature": "^2.1"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Jose\\Component\\Signature\\Algorithm\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Florent Morselli",
+                    "homepage": "https://github.com/Spomky"
+                },
+                {
+                    "name": "All contributors",
+                    "homepage": "https://github.com/web-token/jwt-framework/contributors"
+                }
+            ],
+            "description": "HMAC Based Signature Algorithms the JWT Framework.",
+            "homepage": "https://github.com/web-token",
+            "keywords": [
+                "JOSE",
+                "JWE",
+                "JWK",
+                "JWKSet",
+                "JWS",
+                "Jot",
+                "RFC7515",
+                "RFC7516",
+                "RFC7517",
+                "RFC7518",
+                "RFC7519",
+                "RFC7520",
+                "bundle",
+                "jwa",
+                "jwt",
+                "symfony"
+            ],
+            "support": {
+                "source": "https://github.com/web-token/jwt-signature-algorithm-hmac/tree/v2.2.11"
+            },
+            "funding": [
+                {
+                    "url": "https://www.patreon.com/FlorentMorselli",
+                    "type": "patreon"
+                }
+            ],
+            "time": "2021-01-21T19:18:03+00:00"
+        },
         {
             "name": "webmozart/assert",
             "version": "1.11.0",
@@ -5368,7 +6421,9 @@
     "platform": {
         "php": "^7.4",
         "ext-intl": "*",
-        "ext-simplexml": "*"
+        "ext-curl": "*",
+        "ext-simplexml": "*",
+        "ext-json": "*"
     },
     "platform-dev": [],
     "platform-overrides": {
diff --git a/config-templates/module_campusmultiauth.php b/config-templates/module_campusmultiauth.php
index 455c7645a19bc5293398df451406c86658da847b..81c68d766c7ef1d9a48e2820567368d25a3cd9da 100644
--- a/config-templates/module_campusmultiauth.php
+++ b/config-templates/module_campusmultiauth.php
@@ -121,4 +121,51 @@ $config = [
         //    'identifier.attr.name' => 'OIDCClientID',
         //    'url.attr.name' => 'rploginurl',
     ],
+    'remember_me' => [
+        'security_images' => [
+            //    'pictureDir' => '',
+            //    'showFreshImage' => false,
+            //    'securityImageSalt' => '',
+            //    'pictureBaseURL' => '',
+            //    'pictures_table' => '',
+            //    'pictureStorage' => [
+            //        'ldap.hostname' => '',
+            //        'ldap.port' => 0,
+            //        'ldap.enable_tls' => false,
+            //        'ldap.debug' => false,
+            //        'ldap.referrals' => false,
+            //        'ldap.timeout' => 0,
+            //        'ldap.username' => '',
+            //        'ldap.password' => '',
+            //        'ldap.basedn' => '',
+            //        'search.filter' => '',
+            //        'attribute' => '',
+            //    ],
+        ],
+        //    'uidName' => '',
+        //    'cookieName' => '',
+        //    'nameAttr' => '',
+        //    'cipherClass' => '',
+        //    'storageClass' => '',
+        //    'security.cookie.path' => '',
+        //    'security.cookie.samesite' => '',
+        'store' => [
+            'database.dsn' => 'dsn',
+            'database.username' => 'username',
+            'database.password' => 'password',
+        ],
+        'tokens_table' => 'tokens_table',
+        'signature_key' => [
+            'kty' => 'oct',
+            'k' => 'tCUdnHNp8xH/egDmzwxEkI1BzknCJmAt1khoQsfm9+FNSwIwq9ILN6GYBWjEAoykttrXx5aI/lRdyyGjheRj/g==',
+        ],
+        'encryption_key' => [
+            'kty' => 'oct',
+            'k' => 'BdateloTM7i01lo9L0bfctTJ/2B9E2VCfrTqdhqxilg=',
+        ],
+        //    'uid_attribute' => '',
+        //    'signature_algorithm' => '',
+        //    'encryption_algorithm' => '',
+        //    'keywrap_algorithm' => '',
+    ],
 ];
diff --git a/lib/Auth/Process/RememberMe.php b/lib/Auth/Process/RememberMe.php
new file mode 100644
index 0000000000000000000000000000000000000000..e801fe5446d35ae899b2f6a721a427a29b4ae8d7
--- /dev/null
+++ b/lib/Auth/Process/RememberMe.php
@@ -0,0 +1,372 @@
+<?php
+
+namespace SimpleSAML\Module\campusmultiauth\Auth\Process;
+
+use SimpleSAML\Auth\ProcessingFilter;
+use SimpleSAML\Configuration;
+use SimpleSAML\Logger;
+use SimpleSAML\Module\campusmultiauth\Constants;
+use SimpleSAML\Module\campusmultiauth\Fingerprinting;
+use SimpleSAML\Module\campusmultiauth\Utils;
+use SimpleSAML\Module\core\Stats\Output\Log;
+use SimpleSAML\Utils\HTTP;
+
+/**
+ * Inspired by the Facebook class.
+ *
+ * @see https://github.com/simplesamlphp/simplesamlphp/blob/simplesamlphp-1.14/modules/authfacebook/lib/Facebook.php
+ */
+class RememberMe extends ProcessingFilter
+{
+    /**
+     * Name of the GET parameter to clear username.
+     */
+    public const CLEAR_USERNAME_PARAM = 'init';
+
+    /**
+     * Value of the GET parameter to clear username.
+     */
+    public const CLEAR_USERNAME_VALUE = 'true';
+
+    /**
+     * The lifetime of the cookie.
+     */
+    private const COOKIE_LIFETIME = 31536000;
+
+    /**
+     * The secure parameter of the cookie.
+     */
+    private const COOKIE_SECURE = true;
+
+    /**
+     * The http_only parameter of the cookie.
+     */
+    private const COOKIE_HTTPONLY = true;
+
+    /**
+     * Default cookie path.
+     */
+    private const DEFAULT_COOKIE_PATH = '/';
+
+    /**
+     * \SimpleSAML\Module\campusmultiauth\Security\Cipher implementation.
+     */
+    private $cipher;
+
+    /**
+     * Whether to store security image into the cookie or leave it to the login screen to fetch the fresh image.
+     */
+    private $showFreshImage;
+
+    /**
+     * \SimpleSAML\Module\campusmultiauth\Data\Storage implementation.
+     */
+    private $storage;
+
+    /**
+     * Cookie path.
+     */
+    private $cookiePath;
+
+    /**
+     * Cookie SameSite.
+     */
+    private $cookieSameSite;
+
+    /**
+     * Name of the cookie.
+     */
+    private $cookieName;
+
+    /**
+     * Name of the don't remember me cookie.
+     */
+    private $dontCookieName;
+
+    /**
+     * Name of the name attribute.
+     */
+    private $nameAttr;
+
+    /**
+     * The constructor.
+     *
+     * @override
+     *
+     * @param mixed|null $config
+     * @param mixed|null $reserved
+     */
+    public function __construct($config = null, $reserved = null)
+    {
+        if ($config) {
+            parent::__construct($config, $reserved);
+        }
+
+        $configuration = Configuration::getOptionalConfig('module_campusmultiauth.php')
+            ->getConfigItem('remember_me', []);
+
+        $imagesConfiguration = $configuration->getConfigItem('security_images', []);
+
+        $this->showFreshImage = $imagesConfiguration->getBoolean('showFreshImage', false);
+
+        $this->cookiePath = $configuration->getString('security.cookie.path', self::DEFAULT_COOKIE_PATH);
+        $this->cookieSameSite = $configuration->getString('security.cookie.samesite', null);
+        $this->cookieName = $configuration->getString('cookieName', 'campus_userinfo');
+        $this->dontCookieName = $configuration->getString('dontCookieName', 'campus_dont_remember');
+        $this->nameAttr = $configuration->getString('nameAttr', 'displayName');
+    }
+
+    /**
+     * Get user info from a cookie.
+     */
+    public function getUserInfo(bool $updateCounter = true)
+    {
+        // cookie is present
+        if (!isset($_COOKIE[$this->cookieName])) {
+            return false;
+        }
+
+        // cookie is valid
+        try {
+            $data = json_decode($this->getCipher()->decrypt($_COOKIE[$this->cookieName]), true);
+        } catch (\Exception $e) {
+            $this->deleteCookie();
+
+            return false;
+        }
+
+        // browser match
+        if ($data['browser'] !== $this->getBrowserFingerprint()) {
+            Logger::warning(sprintf('campusmultiauth: Cookie browser mismatch with id %d', $data['id']));
+            $this->deleteCookie();
+
+            return false;
+        }
+
+        // counter match
+        $storage = $this->getStorage();
+        if ($storage->getCookieCounter($data['username'], $data['id']) !== $data['counter']) {
+            // replayed cookie
+            Logger::warning(
+                sprintf('campusmultiauth: Replayed cookie with id %d and counter %d', $data['id'], $data['counter'])
+            );
+            $this->deleteCookie();
+
+            return false;
+        }
+
+        if ($updateCounter) {
+            // increment counter
+            $storage->increaseCookieCounter($data['username'], $data['id']);
+            ++$data['counter'];
+
+            // update cookie
+            $this->setCookie($data);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Save user info in a cookie.
+     */
+    public function setUserInfo(string $username, string $name)
+    {
+        $browser = $this->getBrowserFingerprint();
+
+        $userInfo = $this->getUserInfo(false);
+        $id = null;
+        $counter = 0;
+        $storage = $this->getStorage();
+        if ($userInfo !== false && $userInfo['username'] === $username && $userInfo['browser'] === $browser) {
+            $id = $userInfo['id'];
+            $counter = $userInfo['counter'];
+        }
+        $id = $storage->increaseCookieCounter($username, $id);
+        if ($id === null) {
+            Logger::error('Could not insert cookie counter into database.');
+            $this->deleteCookie();
+
+            return;
+        }
+        ++$counter;
+
+        $payload = [
+            'username' => $username,
+            'name' => $name,
+            'browser' => $browser,
+            'id' => $id,
+            'counter' => $counter,
+        ];
+
+        Logger::debug('Setting user info cookie: ' . print_r($payload, true));
+
+        if (!$this->showFreshImage) {
+            $payload['security_image'] = Utils::getSecurityImageOfUser($username);
+        }
+
+        $this->setCookie($payload);
+    }
+
+    /**
+     * Delete the cookie.
+     */
+    public function deleteCookie()
+    {
+        $this->deleteACookie($this->cookieName);
+    }
+
+    public function getDontCookieName()
+    {
+        return $this->dontCookieName;
+    }
+
+    /**
+     * The constructor.
+     *
+     * @override
+     *
+     * @param mixed $request
+     */
+    public function process(&$request)
+    {
+        $uid_attribute = Configuration::getOptionalConfig('module_campusmultiauth.php')
+            ->getConfigItem('remember_me', [])
+            ->getString('uid_attribute', 'uid');
+
+        if (
+            !empty($request['RememberMe'])
+            && !empty($request['Attributes'][$uid_attribute])
+            && !empty($request['Attributes'][$this->nameAttr][0])
+        ) {
+            $uid = $request['Attributes'][$uid_attribute][0];
+            $name = $request['Attributes'][$this->nameAttr][0];
+            $this->setUserInfo($uid, $name);
+            $this->deleteACookie($this->dontCookieName);
+        }
+
+        if (!empty($request['DontRememberMe'])) {
+            $this->setACookie($this->dontCookieName, 'Yes');
+            $this->deleteCookie();
+        }
+    }
+
+    /**
+     * Get hyperlink for the "this is not my username" button.
+     *
+     * @param mixed $authState
+     */
+    public static function getOtherUsernameLink($authState)
+    {
+        $link = HTTP::getSelfURL();
+        $link = HTTP::addURLParameters($link, [
+            self::CLEAR_USERNAME_PARAM => self::CLEAR_USERNAME_VALUE,
+        ]);
+
+        return HTTP::addURLParameters($link, [
+            'AuthState' => $authState,
+        ]);
+    }
+
+    /**
+     * Get info about the browser, which should not change too often.
+     */
+    protected function getBrowserFingerprint()
+    {
+        return Fingerprinting::getBrowserFingerprint();
+    }
+
+    private function getStorage()
+    {
+        if (!$this->storage) {
+            $this->storage = Utils::getInterfaceInstance(
+                'SimpleSAML\\Module\\campusmultiauth\\Data\\Storage',
+                'storageClass',
+                'SimpleSAML\\Module\\campusmultiauth\\Data\\DatabaseStorage'
+            );
+        }
+
+        return $this->storage;
+    }
+
+    private function setCookie(array $data)
+    {
+        $cookie_value = $this->getCipher()->encrypt(json_encode($data));
+        $this->setACookie($this->cookieName, $cookie_value);
+
+        if ($this->cookiePath !== '/') {
+            HTTP::setCookie($this->cookieName, null, [
+                'secure' => self::COOKIE_SECURE,
+                'httponly' => self::COOKIE_HTTPONLY,
+                'path' => '/',
+                'samesite' => $this->cookieSameSite,
+            ], false);
+        }
+        if ($this->cookiePath !== '/simplesaml/module.php/core') {
+            HTTP::setCookie($this->cookieName, null, [
+                'secure' => self::COOKIE_SECURE,
+                'httponly' => self::COOKIE_HTTPONLY,
+                'path' => '/simplesaml/module.php/core',
+                'samesite' => $this->cookieSameSite,
+            ], false);
+        }
+    }
+
+    private function setACookie(string $name, string $value)
+    {
+        $_COOKIE[$name] = $value;
+
+        HTTP::setCookie($name, $value, [
+            'lifetime' => self::COOKIE_LIFETIME,
+            'secure' => self::COOKIE_SECURE,
+            'httponly' => self::COOKIE_HTTPONLY,
+            'path' => $this->cookiePath,
+            'samesite' => $this->cookieSameSite,
+        ], false);
+    }
+
+    /**
+     * Delete a cookie.
+     */
+    private function deleteACookie(string $name)
+    {
+        unset($_COOKIE[$name]);
+
+        HTTP::setCookie($name, null, [
+            'secure' => self::COOKIE_SECURE,
+            'httponly' => self::COOKIE_HTTPONLY,
+            'path' => $this->cookiePath,
+            'samesite' => $this->cookieSameSite,
+        ], false);
+
+        if ($this->cookiePath !== '/') {
+            HTTP::setCookie($name, null, [
+                'secure' => self::COOKIE_SECURE,
+                'httponly' => self::COOKIE_HTTPONLY,
+                'path' => '/',
+                'samesite' => $this->cookieSameSite,
+            ], false);
+        }
+        if ($this->cookiePath !== '/simplesaml/module.php/core') {
+            HTTP::setCookie($name, null, [
+                'secure' => self::COOKIE_SECURE,
+                'httponly' => self::COOKIE_HTTPONLY,
+                'path' => '/simplesaml/module.php/core',
+                'samesite' => $this->cookieSameSite,
+            ], false);
+        }
+    }
+
+    private function getCipher()
+    {
+        if (empty($this->cipher)) {
+            $this->cipher = Utils::getInterfaceInstance(
+                'SimpleSAML\\Module\\campusmultiauth\\Security\\Cipher',
+                'cipherClass',
+                'SimpleSAML\\Module\\campusmultiauth\\Security\\JWTCipher'
+            );
+        }
+
+        return $this->cipher;
+    }
+}
diff --git a/lib/Auth/Source/Campusidp.php b/lib/Auth/Source/Campusidp.php
index f84b8b66e78f8caf28a1ed45573cf5fa70c148ea..5bd5229b3f554a98705dab182a4e6e8b5a7905ac 100644
--- a/lib/Auth/Source/Campusidp.php
+++ b/lib/Auth/Source/Campusidp.php
@@ -38,10 +38,6 @@ class Campusidp extends Source
 
     public const COOKIE_PREFIX = 'campusidp_';
 
-    public const COOKIE_USERNAME = 'username';
-
-    public const COOKIE_PASSWORD = 'password';
-
     public const IDP_HINT_BUTTONS_LIMIT = 5;
 
     // idp hinting
diff --git a/lib/Data/DatabaseStorage.php b/lib/Data/DatabaseStorage.php
new file mode 100644
index 0000000000000000000000000000000000000000..061c8d0145d248a8adf2d83d6cc2ee15d31ac3d8
--- /dev/null
+++ b/lib/Data/DatabaseStorage.php
@@ -0,0 +1,143 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Data;
+
+use SimpleSAML\Configuration;
+use SimpleSAML\Database;
+
+/**
+ * Implementation of Storage using Database.
+ */
+class DatabaseStorage implements Storage
+{
+    /**
+     * Name of the column with uid.
+     */
+    private const UID_COL = 'userid';
+
+    /**
+     * DB table name for pictures.
+     */
+    private $pictures_table;
+
+    /**
+     * DB table name for tokens.
+     */
+    private $tokens_table;
+
+    /**
+     * Configuration.
+     */
+    private $config;
+
+    /**
+     * Database instance.
+     */
+    private $db;
+
+    /**
+     * @override
+     */
+    public function __construct()
+    {
+        $this->config = Configuration::getOptionalConfig('module_campusmultiauth.php')
+            ->getConfigItem('remember_me', []);
+
+        $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->tokens_table = $this->db->applyPrefix(
+            $this->config->getString('tokens_table', 'cookie_counter')
+        );
+    }
+
+    /**
+     * @override
+     */
+    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;
+        }
+
+        return $picture;
+    }
+
+    /**
+     * @override
+     */
+    public function getCookieCounter(string $uid, int $id): ?int
+    {
+        $query = 'SELECT counter FROM ' . $this->tokens_table
+            . ' WHERE ' . self::UID_COL . ' = :userid AND id = :id LIMIT 1';
+        $params = [
+            'userid' => $uid,
+            'id' => $id,
+        ];
+        $statement = $this->db->read($query, $params);
+        $counter = $statement->fetchColumn();
+        if ($counter === false) {
+            return null;
+        }
+
+        return (int) $counter;
+    }
+
+    /**
+     * @override
+     */
+    public function increaseCookieCounter(string $uid, ?int $id = null): ?int
+    {
+        $success = true;
+        if ($id === null) {
+            $id = $this->insert($uid);
+        } else {
+            $success = $this->update($uid, $id);
+        }
+
+        if ($id === null || !$success) {
+            return null;
+        }
+
+        return $id;
+    }
+
+    private function insert(string $uid): ?int
+    {
+        $query = 'INSERT INTO ' . $this->tokens_table . ' (' . self::UID_COL . ', id) VALUES (:userid, :id)';
+        $i = 0;
+        $params = [
+            'userid' => $uid,
+        ];
+        do {
+            $new_id = random_int(1, PHP_INT_MAX);
+            $params['id'] = $new_id;
+            $success = $this->db->write($query, $params);
+        } while (!$success && $i++ < 3);
+
+        return $success ? $new_id : null;
+    }
+
+    private function update(string $uid, int $id): bool
+    {
+        $params = [
+            'userid' => $uid,
+            'id' => $id,
+        ];
+        $query = 'UPDATE ' . $this->tokens_table . ' SET counter=counter+1'
+            . ' WHERE ' . self::UID_COL . '=:userid AND id=:id';
+
+        return (bool) $this->db->write($query, $params);
+    }
+}
diff --git a/lib/Data/PerunStorage.php b/lib/Data/PerunStorage.php
new file mode 100644
index 0000000000000000000000000000000000000000..ab7d44cb62db9e8b8214778ae8f3b0fdbda0cb8d
--- /dev/null
+++ b/lib/Data/PerunStorage.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Data;
+
+use SimpleSAML\Configuration;
+use SimpleSAML\Logger;
+use SimpleSAML\Module\ldap\Auth\Ldap;
+use SimpleSAML\Module\campusmultiauth\Constants;
+
+/**
+ * Implementation of Storage using Perun LDAP and Database.
+ */
+class PerunStorage extends DatabaseStorage
+{
+    /**
+     * Configuration.
+     */
+    private $config;
+
+    /**
+     * LDAP instance.
+     */
+    private $ldap;
+
+    /**
+     * @override
+     */
+    public function __construct()
+    {
+        parent::__construct();
+        $this->config = Configuration::getOptionalConfig('module_campusmultiauth.php')
+            ->getConfigItem('remember_me', [])
+            ->getConfigItem('security_images', [])
+            ->getConfigItem('pictureStorage', []);
+
+        $hostname = $this->config->getString('ldap.hostname');
+        $port = $this->config->getInteger('ldap.port', 389);
+        $enable_tls = $this->config->getBoolean('ldap.enable_tls', false);
+        $debug = $this->config->getBoolean('ldap.debug', false);
+        $referrals = $this->config->getBoolean('ldap.referrals', true);
+        $timeout = $this->config->getInteger('ldap.timeout', 0);
+        $username = $this->config->getString('ldap.username', null);
+        $password = $this->config->getString('ldap.password', null);
+
+        try {
+            $this->ldap = new Ldap($hostname, $enable_tls, $debug, $timeout, $port, $referrals);
+        } catch (\Exception $e) {
+            // Added this warning in case $this->getLdap() fails
+            Logger::warning('PerunStorage: LDAP exception ' . $e);
+
+            return;
+        }
+        $this->ldap->bind($username, $password);
+    }
+
+    /**
+     * @override
+     */
+    public function getSecurityImageOfUser(string $uid): ?string
+    {
+        $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);
+        } catch (\Exception $e) {
+            $entries = [];
+        }
+        if (count($entries) < 1 || empty($entries[0][$attribute])) {
+            return null;
+        }
+
+        return $entries[0][$attribute][0];
+    }
+}
diff --git a/lib/Data/Storage.php b/lib/Data/Storage.php
new file mode 100644
index 0000000000000000000000000000000000000000..da9f926648d2817c35076d5e8fe62a59ef448098
--- /dev/null
+++ b/lib/Data/Storage.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Data;
+
+interface Storage
+{
+    public function __construct();
+
+    /**
+     * Null if user has none, URL otherwise.
+     */
+    public function getSecurityImageOfUser(string $uid): ?string;
+
+    /**
+     * False if not found (should not happen), counter otherwise.
+     */
+    public function getCookieCounter(string $uid, int $id): ?int;
+
+    /**
+     * Increment a counter for a user. Returns the cookie id.
+     */
+    public function increaseCookieCounter(string $uid, ?int $id): ?int;
+}
diff --git a/lib/Fingerprint.php b/lib/Fingerprint.php
new file mode 100644
index 0000000000000000000000000000000000000000..f645d10ac002d2a0daaf3e9df4991a523e01d7ab
--- /dev/null
+++ b/lib/Fingerprint.php
@@ -0,0 +1,10 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth;
+
+abstract class Fingerprint
+{
+    abstract public function getValue();
+}
diff --git a/lib/Fingerprint/Browser.php b/lib/Fingerprint/Browser.php
new file mode 100644
index 0000000000000000000000000000000000000000..80869b830e34e4225c804e865762ffa31abd7790
--- /dev/null
+++ b/lib/Fingerprint/Browser.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint;
+
+use donatj\UserAgent\UserAgentParser;
+
+abstract class Browser extends \SimpleSAML\Module\campusmultiauth\Fingerprint
+{
+    public function getValue()
+    {
+        $ua = self::getBrowserInfo();
+
+        return $this->getProperty($ua);
+    }
+
+    abstract protected function getProperty($ua);
+
+    private static function getBrowserInfo()
+    {
+        $parser = new UserAgentParser();
+
+        return $parser->parse();
+    }
+}
diff --git a/lib/Fingerprint/Browser/Name.php b/lib/Fingerprint/Browser/Name.php
new file mode 100644
index 0000000000000000000000000000000000000000..5bc2f2707161a1dd85e52a1056061288e9092fcd
--- /dev/null
+++ b/lib/Fingerprint/Browser/Name.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Browser;
+
+class Name extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Browser
+{
+    protected function getProperty($ua)
+    {
+        return $ua->browser();
+    }
+}
diff --git a/lib/Fingerprint/Browser/Platform.php b/lib/Fingerprint/Browser/Platform.php
new file mode 100644
index 0000000000000000000000000000000000000000..3851a7b5c502afb5e2a5874d98d877384e8999db
--- /dev/null
+++ b/lib/Fingerprint/Browser/Platform.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Browser;
+
+class Platform extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Browser
+{
+    protected function getProperty($ua)
+    {
+        return $ua->platform();
+    }
+}
diff --git a/lib/Fingerprint/Header.php b/lib/Fingerprint/Header.php
new file mode 100644
index 0000000000000000000000000000000000000000..c593f883e427462fbb2b6f38143042ba0b13fc45
--- /dev/null
+++ b/lib/Fingerprint/Header.php
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint;
+
+abstract class Header extends \SimpleSAML\Module\campusmultiauth\Fingerprint
+{
+    public function getValue()
+    {
+        return isset($_SERVER[$this->getHeaderName()]) ? $_SERVER[$this->getHeaderName()] : false;
+    }
+
+    /**
+     * @returns string
+     */
+    abstract protected function getHeaderName();
+}
diff --git a/lib/Fingerprint/Header/Accept.php b/lib/Fingerprint/Header/Accept.php
new file mode 100644
index 0000000000000000000000000000000000000000..5771520d922f27daa99d3eeb4603d28ded85e168
--- /dev/null
+++ b/lib/Fingerprint/Header/Accept.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Header;
+
+class Accept extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Header
+{
+    protected function getHeaderName()
+    {
+        return 'HTTP_ACCEPT';
+    }
+}
diff --git a/lib/Fingerprint/Header/AcceptCharset.php b/lib/Fingerprint/Header/AcceptCharset.php
new file mode 100644
index 0000000000000000000000000000000000000000..36c8c288a5dd306f726cbd8bebc8e3b0378589f7
--- /dev/null
+++ b/lib/Fingerprint/Header/AcceptCharset.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Header;
+
+class AcceptCharset extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Header
+{
+    protected function getHeaderName()
+    {
+        return 'HTTP_ACCEPT_CHARSET';
+    }
+}
diff --git a/lib/Fingerprint/Header/AcceptEncoding.php b/lib/Fingerprint/Header/AcceptEncoding.php
new file mode 100644
index 0000000000000000000000000000000000000000..9da8ff6c847ca87e34c3c8cf90bc4a73d688adf7
--- /dev/null
+++ b/lib/Fingerprint/Header/AcceptEncoding.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Header;
+
+class AcceptEncoding extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Header
+{
+    protected function getHeaderName()
+    {
+        return 'HTTP_ACCEPT_ENCODING';
+    }
+}
diff --git a/lib/Fingerprint/Header/AcceptLanguage.php b/lib/Fingerprint/Header/AcceptLanguage.php
new file mode 100644
index 0000000000000000000000000000000000000000..0eb6fae39de244a5672d47f6ec75da917aeefdb3
--- /dev/null
+++ b/lib/Fingerprint/Header/AcceptLanguage.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Header;
+
+class AcceptLanguage extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Header
+{
+    protected function getHeaderName()
+    {
+        return 'HTTP_ACCEPT_LANGUAGE';
+    }
+}
diff --git a/lib/Fingerprint/Header/Connection.php b/lib/Fingerprint/Header/Connection.php
new file mode 100644
index 0000000000000000000000000000000000000000..b2edaeef62ff460306bdd998e0a136724e9701d2
--- /dev/null
+++ b/lib/Fingerprint/Header/Connection.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Header;
+
+class Connection extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Header
+{
+    protected function getHeaderName()
+    {
+        return 'HTTP_CONNECTION';
+    }
+}
diff --git a/lib/Fingerprint/Header/DNT.php b/lib/Fingerprint/Header/DNT.php
new file mode 100644
index 0000000000000000000000000000000000000000..dd12cadf87ef870d0d80080e89569a7f2ba433d7
--- /dev/null
+++ b/lib/Fingerprint/Header/DNT.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Header;
+
+class DNT extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Header
+{
+    protected function getHeaderName()
+    {
+        return 'HTTP_DNT';
+    }
+}
diff --git a/lib/Fingerprint/Header/UpgradeInsecureRequests.php b/lib/Fingerprint/Header/UpgradeInsecureRequests.php
new file mode 100644
index 0000000000000000000000000000000000000000..1a7f55c4fcf856a8dc63ab4f6468a71520a076ef
--- /dev/null
+++ b/lib/Fingerprint/Header/UpgradeInsecureRequests.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Header;
+
+class UpgradeInsecureRequests extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Header
+{
+    protected function getHeaderName()
+    {
+        return 'HTTP_UPGRADE_INSECURE_REQUESTS';
+    }
+}
diff --git a/lib/Fingerprint/Header/XRequestedWith.php b/lib/Fingerprint/Header/XRequestedWith.php
new file mode 100644
index 0000000000000000000000000000000000000000..45c098116ab010f98639b96bbf32951f6a575236
--- /dev/null
+++ b/lib/Fingerprint/Header/XRequestedWith.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint\Header;
+
+class XRequestedWith extends \SimpleSAML\Module\campusmultiauth\Fingerprint\Header
+{
+    protected function getHeaderName()
+    {
+        return 'HTTP_X_REQUESTED_WITH';
+    }
+}
diff --git a/lib/Fingerprint/HeaderPresenceAndOrder.php b/lib/Fingerprint/HeaderPresenceAndOrder.php
new file mode 100644
index 0000000000000000000000000000000000000000..671c3e138b8ac908ab2ea7bfa8bc65bb6f192a37
--- /dev/null
+++ b/lib/Fingerprint/HeaderPresenceAndOrder.php
@@ -0,0 +1,26 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Fingerprint;
+
+class HeaderPresenceAndOrder extends \SimpleSAML\Module\campusmultiauth\Fingerprint
+{
+    private const HEADERS = ['client-ip', 'x-forwarded-for', 'x-forwarded', 'x-cluster-client-ip', 'forwarded-for',
+        'forwarded', 'via', 'accept', 'accept-charset', 'accept-encoding', 'accept-language', 'connection',
+        'cookie', 'content-length', 'host', 'referer', 'user-agent', 'x-requested-with', 'dnt',
+        'upgrade-insecure-requests', ];
+
+    public function getValue()
+    {
+        return array_keys(
+            array_filter(
+                getallheaders(),
+                function ($var) {
+                    return in_array(strtolower($var), self::HEADERS, true);
+                },
+                ARRAY_FILTER_USE_KEY
+            )
+        );
+    }
+}
diff --git a/lib/Fingerprinting.php b/lib/Fingerprinting.php
new file mode 100644
index 0000000000000000000000000000000000000000..de90e4479a1eae6ae7db063cd1568dbc67a5e12a
--- /dev/null
+++ b/lib/Fingerprinting.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth;
+
+class Fingerprinting
+{
+    /**
+     * Hash algorithm used for browser fingerprint.
+     */
+    private const HASH_ALG = 'sha512';
+
+    /**
+     * Class prefix for fingerprint bits.
+     */
+    private const CLASS_PREFIX = '\\SimpleSAML\\Module\\campusmultiauth\\Fingerprint\\';
+
+    /**
+     * Bits of information used for the fingerprint.
+     */
+    private const BITS = [
+        'Header\\Accept',
+        'Header\\AcceptCharset',
+        'Header\\AcceptEncoding',
+        'Header\\AcceptLanguage',
+        'Header\\Connection',
+        'Header\\DNT',
+        'Header\\XRequestedWith',
+        'Header\\UpgradeInsecureRequests',
+        'Browser\\Name',
+        'Browser\\Platform',
+    ];
+
+    public static function getBrowserFingerprint()
+    {
+        $info = [];
+        foreach (self::BITS as $bit) {
+            $className = self::CLASS_PREFIX . $bit;
+            $info[$bit] = (new $className())->getValue();
+        }
+
+        return hash(self::HASH_ALG, serialize($info));
+    }
+}
diff --git a/lib/Security/Cipher.php b/lib/Security/Cipher.php
new file mode 100644
index 0000000000000000000000000000000000000000..06a8d5a168156d4ad3c590aa2bdb7b1607496499
--- /dev/null
+++ b/lib/Security/Cipher.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace SimpleSAML\Module\campusmultiauth\Security;
+
+interface Cipher
+{
+    public function __construct();
+
+    /**
+     * Encrypt the data.
+     *
+     * @return string
+     */
+    public function encrypt(string $data);
+
+    /**
+     * Decrypt the data.
+     *
+     * @return might return false if data is currupted, string otherwise
+     */
+    public function decrypt(string $data);
+}
diff --git a/lib/Security/JWTCipher.php b/lib/Security/JWTCipher.php
new file mode 100644
index 0000000000000000000000000000000000000000..c3a797090ba57f3867e36c580c8a0fc42b4c38af
--- /dev/null
+++ b/lib/Security/JWTCipher.php
@@ -0,0 +1,237 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth\Security;
+
+use Jose\Component\Core\AlgorithmManager;
+use Jose\Component\Core\JWK;
+use Jose\Component\Core\JWKSet;
+use Jose\Component\Encryption\Compression\CompressionMethodManager;
+use Jose\Component\Encryption\Compression\Deflate;
+use Jose\Component\Encryption\JWEBuilder;
+use Jose\Component\Encryption\JWEDecrypter;
+use Jose\Component\Encryption\JWELoader;
+use Jose\Component\Encryption\Serializer\JSONFlattenedSerializer as JWEJSONFlattenedSerializer;
+use Jose\Component\Encryption\Serializer\JWESerializerManager;
+use Jose\Component\NestedToken\NestedTokenBuilder;
+use Jose\Component\NestedToken\NestedTokenLoader;
+use Jose\Component\Signature\JWSBuilder;
+use Jose\Component\Signature\JWSLoader;
+use Jose\Component\Signature\JWSVerifier;
+use Jose\Component\Signature\Serializer\JSONFlattenedSerializer as JWSJSONFlattenedSerializer;
+use Jose\Component\Signature\Serializer\JWSSerializerManager;
+use SimpleSAML\Configuration;
+use SimpleSAML\Logger;
+
+class JWTCipher implements Cipher
+{
+    private $builder;
+
+    private $loader;
+
+    private $signature_key;
+
+    private $signature_keyset;
+
+    private $encryption_key;
+
+    private $encryption_keyset;
+
+    private $signature_algorithm;
+
+    private $encryption_algorithm;
+
+    private $keywrap_algorithm;
+
+    /**
+     * @override
+     */
+    public function __construct()
+    {
+        $moduleConfig = Configuration::getOptionalConfig('module_campusmultiauth.php')
+            ->getConfigItem('remember_me', []);
+
+        $signatureKey = $moduleConfig->getArray('signature_key');
+        $this->signature_key = new JWK($signatureKey);
+        $this->signature_keyset = JWKSet::createFromKeyData([
+            'keys' => [$signatureKey],
+        ]);
+        $encryptionKey = $moduleConfig->getArray('encryption_key');
+        $this->encryption_key = new JWK($encryptionKey);
+        $this->encryption_keyset = JWKSet::createFromKeyData([
+            'keys' => [$encryptionKey],
+        ]);
+        $this->signature_algorithm = self::getAlgorithm(
+            'Signature\\Algorithm',
+            $moduleConfig->getString('signature_algorithm', 'HS512')
+        );
+        $this->encryption_algorithm = self::getAlgorithm(
+            'Encryption\\Algorithm\\ContentEncryption',
+            $moduleConfig->getString('encryption_algorithm', 'A256GCM')
+        );
+        $this->keywrap_algorithm = self::getAlgorithm(
+            'Encryption\\Algorithm\\KeyEncryption',
+            $moduleConfig->getString('keywrap_algorithm', 'A256GCMKW')
+        );
+    }
+
+    /**
+     * @override
+     */
+    public function encrypt(string $data)
+    {
+        if (!$this->builder) {
+            $this->builder = $this->getBuilder();
+        }
+
+        $token = $this->builder->create(
+        // The payload to protect
+            $data,
+            // A list of signatures
+            [[
+                'key' => $this->signature_key,
+                'protected_header' => [
+                    'alg' => $this->signature_algorithm->name(),
+                ],
+            ]],
+            // The serialization mode for the JWS
+            'jws_json_flattened',
+            // The shared protected header
+            [
+                'alg' => $this->keywrap_algorithm->name(),
+                'enc' => $this->encryption_algorithm->name(),
+            ],
+            // The shared unprotected header
+            [],
+            // A list of recipients
+            [[
+                'key' => $this->encryption_key,
+                'header' => [],
+            ]],
+            // The serialization mode for the JWE.
+            'jwe_json_flattened'
+        );
+
+        Logger::debug(sprintf('Encrypted JWT: %s', $token));
+
+        return $token;
+    }
+
+    /**
+     * @override
+     */
+    public function decrypt(string $data)
+    {
+        if (!$this->loader) {
+            $this->loader = $this->getLoader();
+        }
+
+        $jws = $this->loader->load($data, $this->encryption_keyset, $this->signature_keyset);
+
+        $payload = $jws->getPayload();
+
+        Logger::debug(sprintf('Decrypted JWT: %s', $payload));
+
+        return $payload;
+    }
+
+    private static function getAlgorithm($path, $className)
+    {
+        $classPath = sprintf('Jose\\Component\\%s\\%s', $path, $className);
+        if (!class_exists($classPath)) {
+            throw new \Exception('Invalid algorithm specified: ' . $classPath);
+        }
+
+        return new $classPath();
+    }
+
+    /**
+     * @return JWSBuilder
+     */
+    private function getJWSBuilder()
+    {
+        $algorithmManager = new AlgorithmManager([$this->signature_algorithm]);
+
+        return new JWSBuilder($algorithmManager);
+    }
+
+    /**
+     * @return JWEBuilder
+     */
+    private function getJWEBuilder()
+    {
+        $keyEncryptionAlgorithmManager = new AlgorithmManager([$this->keywrap_algorithm]);
+
+        $contentEncryptionAlgorithmManager = new AlgorithmManager([$this->encryption_algorithm]);
+
+        $compressionMethodManager = new CompressionMethodManager([new Deflate()]);
+
+        return new JWEBuilder(
+            $keyEncryptionAlgorithmManager,
+            $contentEncryptionAlgorithmManager,
+            $compressionMethodManager
+        );
+    }
+
+    /**
+     * @return NestedTokenBuilder
+     */
+    private function getBuilder()
+    {
+        $jweBuilder = $this->getJWEBuilder();
+        $jwsBuilder = $this->getJWSBuilder();
+
+        $jweSerializerManager = new JWESerializerManager([new JWEJSONFlattenedSerializer()]);
+        $jwsSerializerManager = new JWSSerializerManager([new JWSJSONFlattenedSerializer()]);
+
+        return new NestedTokenBuilder($jweBuilder, $jweSerializerManager, $jwsBuilder, $jwsSerializerManager);
+    }
+
+    /**
+     * @return JWELoader
+     */
+    private function getJWELoader()
+    {
+        $keyEncryptionAlgorithmManager = new AlgorithmManager([$this->keywrap_algorithm]);
+
+        $contentEncryptionAlgorithmManager = new AlgorithmManager([$this->encryption_algorithm]);
+
+        $compressionMethodManager = new CompressionMethodManager([new Deflate()]);
+
+        $jweDecrypter = new JWEDecrypter(
+            $keyEncryptionAlgorithmManager,
+            $contentEncryptionAlgorithmManager,
+            $compressionMethodManager
+        );
+
+        $serializerManager = new JWESerializerManager([new JWEJSONFlattenedSerializer()]);
+
+        return new JWELoader($serializerManager, $jweDecrypter, null);
+    }
+
+    /**
+     * @return JWSLoader
+     */
+    private function getJWSLoader()
+    {
+        $algorithmManager = new AlgorithmManager([$this->signature_algorithm]);
+
+        $jwsVerifier = new JWSVerifier($algorithmManager);
+
+        $serializerManager = new JWSSerializerManager([new JWSJSONFlattenedSerializer()]);
+
+        return new JWSLoader($serializerManager, $jwsVerifier, null);
+    }
+
+    /**
+     * @return NestedTokenLoader
+     */
+    private function getLoader()
+    {
+        $jweLoader = $this->getJWELoader();
+        $jwsLoader = $this->getJWSLoader();
+
+        return new NestedTokenLoader($jweLoader, $jwsLoader);
+    }
+}
diff --git a/lib/Utils.php b/lib/Utils.php
new file mode 100644
index 0000000000000000000000000000000000000000..ec4bfa6f9d52382f6e58f7766dc9e2eaa4157407
--- /dev/null
+++ b/lib/Utils.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SimpleSAML\Module\campusmultiauth;
+
+use SimpleSAML\Configuration;
+
+class Utils
+{
+    public static function getInterfaceInstance($interface, $optionName, $defaultClassPath)
+    {
+        $config = Configuration::getOptionalConfig('module_campusmultiauth.php')->getConfigItem('remember_me', []);
+        $classPath = $config->getString($optionName, $defaultClassPath);
+        if (!in_array($interface, class_implements($classPath), true)) {
+            throw new \Exception('Invalid ' . $optionName . ' specified: ' . $classPath);
+        }
+
+        return new $classPath();
+    }
+
+    public static function getSecurityImageOfUser($username)
+    {
+        $storage = self::getInterfaceInstance(
+            'SimpleSAML\\Module\\campusmultiauth\\Data\\Storage',
+            'storageClass',
+            'SimpleSAML\\Module\\campusmultiauth\\Data\\DatabaseStorage'
+        );
+
+        return $storage->getSecurityImageOfUser($username);
+    }
+}
diff --git a/locales/cs/LC_MESSAGES/campusmultiauth.po b/locales/cs/LC_MESSAGES/campusmultiauth.po
index 329df661d47b19db4f38d0182ede7c9a66f87a2c..30b052a0f196354de5deb97d56762729f340d651 100644
--- a/locales/cs/LC_MESSAGES/campusmultiauth.po
+++ b/locales/cs/LC_MESSAGES/campusmultiauth.po
@@ -78,3 +78,6 @@ msgstr "Z důvodu neaktivity je nutné stránku obnovit."
 
 msgid "{campusmultiauth:refresh}"
 msgstr "obnovit"
+
+msgid "{campusmultiauth:different_account}"
+msgstr "Přihlásit se jiným účtem"
diff --git a/locales/en/LC_MESSAGES/campusmultiauth.po b/locales/en/LC_MESSAGES/campusmultiauth.po
index 50ce5b477be1574639027be92d52d9deeea0d1a3..97dd8b785f9933f4c946a7d4c444d39971f3c632 100644
--- a/locales/en/LC_MESSAGES/campusmultiauth.po
+++ b/locales/en/LC_MESSAGES/campusmultiauth.po
@@ -78,3 +78,6 @@ msgstr "Because of inactivity, it is needed to refresh the page."
 
 msgid "{campusmultiauth:refresh}"
 msgstr "refresh"
+
+msgid "{campusmultiauth:different_account}"
+msgstr "Use a different account to log in"
diff --git a/templates/includes/local-login.twig b/templates/includes/local-login.twig
index ef22a9a9836d20c78d5466b879f4c2257db5cb85..1529c9425c1d1fc035e686984b50d08bf286d351 100644
--- a/templates/includes/local-login.twig
+++ b/templates/includes/local-login.twig
@@ -12,6 +12,10 @@
         {% endif %}
     </h4>
 
+    {% if securityImage is defined %}
+        <img src='{{ securityImage|escape }}' class='security-image' alt=''>
+    {% endif %}
+
     {% if wrongUserPass == true %}
         {% if muni_jvs %}
             <div class="message message--error" role="alert">
@@ -28,22 +32,40 @@
     {% endif %}
 
     <div class="margin-bottom-24 text-left{% if wrongUserPass and muni_jvs %} error{% endif %}">
-        <label for="username" class="{% if muni_jvs %}color-{{ configuration.priority }}{% else %}form-label text-{% if configuration.priority == 'primary' %}dark{% else %}muted{% endif %}{% endif %}">
-            {% if attribute(configuration.username_label, currentLanguage) is defined %}{{ attribute(configuration.username_label, currentLanguage) }}
-            {% elseif configuration.username_label is defined and configuration.username_label is iterable and configuration.username_label is not empty %}{{ configuration.username_label | first }}
-            {% elseif configuration.username_label is defined and configuration.username_label is not iterable %}{{ configuration.username_label }}
-            {% else %}{{ '{campusmultiauth:username_label}'|trans }}
-            {% endif %}
-        </label>
-        <br>
-        {% if muni_jvs %}<span class="inp-fix">{% endif %}
-            <input id="username" class="{% if muni_jvs %}inp-text input-height border color-border-{{ configuration.priority }}{% else %}input-height form-control shadow-sm border-2{% if wrongUserPass %} is-invalid{% else %} border-{% if configuration.priority == 'primary' %}dark{% else %}muted{% endif %}{% endif %}{% endif %}"
-                   type="text" name="username"{% if cookie_username is not null %} value="{{ cookie_username }}"{% endif %} placeholder="{% if attribute(configuration.username_placeholder, currentLanguage) is defined %}{{ attribute(configuration.username_placeholder, currentLanguage) }}
-                {% elseif configuration.username_placeholder is defined and configuration.username_placeholder is iterable and configuration.username_placeholder is not empty %}{{ configuration.username_placeholder | first }}
-                {% elseif configuration.username_placeholder is defined and configuration.username_placeholder is not iterable %}{{ configuration.username_placeholder }}
-                {% else %}{{ '{campusmultiauth:username_placeholder}'|trans }}
-                {% endif %}" required>
-        {% if muni_jvs %}</span>{% endif %}
+        {% if userInfo is same as false %}
+            <label for="username" class="{% if muni_jvs %}color-{{ configuration.priority }}{% else %}form-label text-{% if configuration.priority == 'primary' %}dark{% else %}muted{% endif %}{% endif %}">
+                {% if attribute(configuration.username_label, currentLanguage) is defined %}{{ attribute(configuration.username_label, currentLanguage) }}
+                {% elseif configuration.username_label is defined and configuration.username_label is iterable and configuration.username_label is not empty %}{{ configuration.username_label | first }}
+                {% elseif configuration.username_label is defined and configuration.username_label is not iterable %}{{ configuration.username_label }}
+                {% else %}{{ '{campusmultiauth:username_label}'|trans }}
+                {% endif %}
+            </label>
+            <br>
+            {% if muni_jvs %}<span class="inp-fix">{% endif %}
+                <input id="username" class="{% if muni_jvs %}inp-text input-height border color-border-{{ configuration.priority }}{% else %}input-height form-control shadow-sm border-2{% if wrongUserPass %} is-invalid{% else %} border-{% if configuration.priority == 'primary' %}dark{% else %}muted{% endif %}{% endif %}{% endif %}"
+                       type="text"{% if autofocus == 'username' %} autofocus{% endif%}  name="username" placeholder="{% if attribute(configuration.username_placeholder, currentLanguage) is defined %}{{ attribute(configuration.username_placeholder, currentLanguage) }}
+                    {% elseif configuration.username_placeholder is defined and configuration.username_placeholder is iterable and configuration.username_placeholder is not empty %}{{ configuration.username_placeholder | first }}
+                    {% elseif configuration.username_placeholder is defined and configuration.username_placeholder is not iterable %}{{ configuration.username_placeholder }}
+                    {% else %}{{ '{campusmultiauth:username_placeholder}'|trans }}
+                    {% endif %}" required>
+            {% if muni_jvs %}</span>{% endif %}
+        {% else %}
+            <div class="{% if muni_jvs %}box-bg box-bg--white-border u-pt-30 u-pb-30 inp-fix inp-icon inp-icon--after color-border-{{ configuration.priority }}{% else %}border-2 solid py-4 ps-4 position-relative {% if configuration.priority == 'primary' %}border-dark{% else %}border-muted{% endif %}{% endif %}">
+                <strong id="remembered-name">{{ userInfo.name|escape }}</strong>
+                <br>
+                <small>{{ uidName }} {{ username|escape }}</small>
+                <input class="hidden" readonly name="username" id="username" autocomplete="username" value="{{ username }}" />
+                {% if forceUsername is not defined or not forceUsername %}
+                    <a {% if not muni_jvs %}class="position-absolute remember-me-times"{% endif%}href="{{ differentUsername }}" title="{{ '{campusmultiauth:different_account}'|trans }}">
+                        {% if muni_jvs %}
+                            <span class="icon icon-times"></span>
+                        {% else %}
+                            <i class="fas fa-times"></i>
+                        {% endif %}
+                    </a>
+                {% endif %}
+            </div>
+        {% endif %}
     </div>
     <div class="margin-bottom-24 text-left{% if wrongUserPass and muni_jvs %} error{% endif %}">
         <label for="password" class="{% if muni_jvs %}color-{{ configuration.priority }}{% else %}form-label text-{% if configuration.priority == 'primary' %}dark{% else %}muted{% endif %}{% endif %}">
@@ -69,7 +91,7 @@
                        class="form-control shadow-sm border-2 input-height{% if wrongUserPass %} is-invalid{% else %} border-muted{% endif %}"
                    {% endif %}
                {% endif %}
-                   type="password" name="password" autocomplete="current-password"{% if cookie_password is not null %} value="{{ cookie_password }}"{% endif %} placeholder="{% if attribute(configuration.password_placeholder, currentLanguage) is defined %}{{ attribute(configuration.password_placeholder, currentLanguage) }}
+                   type="password" name="password"{% if autofocus == 'password' %} autofocus{% endif %} autocomplete="current-password" placeholder="{% if attribute(configuration.password_placeholder, currentLanguage) is defined %}{{ attribute(configuration.password_placeholder, currentLanguage) }}
                 {% elseif configuration.password_placeholder is defined and configuration.password_placeholder is iterable and configuration.password_placeholder is not empty %}{{ configuration.password_placeholder | first }}
                 {% elseif configuration.password_placeholder is defined and configuration.password_placeholder is not iterable %}{{ configuration.password_placeholder }}
                 {% else %}{{ '{campusmultiauth:password_placeholder}'|trans }}
@@ -83,13 +105,14 @@
         {% endif %}
     </div>
 
-    {% if rememberme_enabled %}
+    {% if rememberme_enabled and userInfo is same as false %}
         <div class="margin-bottom-24 text-left">
             {% if muni_jvs %}
                 <label class="inp-item inp-item--checkbox" for="remember_me">
-                    <input id="remember_me" type="checkbox" name="remember_me" value="Yes">
+                    <input id="remember_me" type="checkbox"{% if dontRemember is same as false and rememberme_checked is same as true %} checked{% endif %} name="remember_me" value="Yes">
                     <span>{{ '{campusmultiauth:remember}'|trans }}</span>
                 </label>
+                <input type="hidden" id="dont_remember_me" name="dont_remember_me" value="{% if dontRemember is same as true or rememberme_checked is same as false %}Yes{% endif %}" />
             {% else %}
                 <input type="checkbox" class="form-check-input border{% if configuration.priority == 'primary' %} border-dark{% else %} border-muted{% endif %}" id="remember_me" name="remember_me" value="Yes">
                 <label class="form-check-label margin-left-12 text-{% if configuration.priority == 'primary' %}dark{% else %}muted{% endif %}" for="remember_me">{{ '{campusmultiauth:remember}'|trans }}</label>
@@ -97,7 +120,7 @@
         </div>
     {% endif %}
 
-    <div class="margin-bottom-24" id="submit">
+    <div class="margin-bottom-24{% if not rememberme_enabled or userInfo is not same as false %} margin-top-48{% endif %}" id="submit">
         {% if muni_jvs %}
             <button id="submit_button" class="btn {% if wayf_config.muni_faculty is defined %}btn-{{ wayf_config.muni_faculty }}{% else %}btn-primary {% endif %}{% if configuration.priority == 'secondary' %}btn-border{% endif %} btn-lg" type="submit">
                 <span>
diff --git a/www/resources/campus-idp.css b/www/resources/campus-idp.css
index 050e75f6532b091f443ddeb6d006c1478c22c589..ed2e8c7b7d84add27d82c4a8f092be607fb2b2b0 100644
--- a/www/resources/campus-idp.css
+++ b/www/resources/campus-idp.css
@@ -124,6 +124,14 @@ body {
   background-color: #ffffff;
 }
 
+.security-image {
+  object-fit: scale-down;
+  margin: calc(1em - 25px) auto 1em;
+  display: block;
+  max-width: 500px;
+  max-height: 150px;
+}
+
 .individual-identity-logo {
   width: 50px;
   height: 50px;
@@ -139,6 +147,11 @@ body {
   padding: 0;
 }
 
+.remember-me-times {
+  right: 5%;
+  top: 40%;
+}
+
 .idp-text {
   margin-bottom: 0;
   margin-left: 25px;
@@ -201,6 +214,10 @@ body {
   margin-top: 12px;
 }
 
+.margin-top-48 {
+  margin-top: 48px;
+}
+
 .margin-left-12 {
   margin-left: 12px;
 }
@@ -329,6 +346,10 @@ body {
   text-align: center;
 }
 
+#username.hidden {
+  display: none;
+}
+
 #content {
   margin: 0 36px 24px 36px;
 }
@@ -382,6 +403,10 @@ body {
   padding-top: 24px;
 }
 
+.solid {
+  border-style: solid;
+}
+
 @media screen and (min-width: 768px) {
   .wrap {
     margin: 0 0 36px 0;
@@ -458,6 +483,10 @@ body {
     margin-top: -12px;
   }
 
+  .security-image {
+    margin-top: calc(1em - 60px);
+  }
+
   #content {
     margin: 0;
   }
diff --git a/www/resources/campus-idp.js b/www/resources/campus-idp.js
index 75f2a8fc5fb9bf12ba9f9aa9e4027435a9617b8c..45d9bb59763908da0d7091ddfd78aeae39a70c69 100644
--- a/www/resources/campus-idp.js
+++ b/www/resources/campus-idp.js
@@ -1,4 +1,5 @@
 import dialogPolyfill from "dialog-polyfill";
+import PrivateWindow from "./extra/PrivateWindowCheck";
 
 function hideElement(element) {
   element.classList.add("vhide", "d-none");
@@ -94,6 +95,17 @@ function selectizeLoad(query, callback) {
   });
 }
 
+function isDNT() {
+  return (
+    (window.doNotTrack && window.doNotTrack === "1") ||
+    (navigator.doNotTrack &&
+      (navigator.doNotTrack === "yes" || navigator.doNotTrack === "1")) ||
+    (navigator.msDoNotTrack && navigator.msDoNotTrack === "1") ||
+    ("msTrackingProtectionEnabled" in window.external &&
+      window.external.msTrackingProtectionEnabled())
+  );
+}
+
 document.addEventListener("DOMContentLoaded", function () {
   // show dialog after the specified timeout to refresh the page
   const dialog = document.getElementById("refresh-required-dialog");
@@ -104,6 +116,29 @@ document.addEventListener("DOMContentLoaded", function () {
     }, dialog.dataset.timeout * 1000);
   }
 
+  // uncheck remember me in private windows and update dontRememberMe
+  const rememberMe = document.getElementById("remember_me");
+  if (rememberMe) {
+    const dontRememberMe = document.getElementById("dont_remember_me");
+    if (dontRememberMe) {
+      rememberMe.addEventListener("change", () => {
+        dontRememberMe.value = rememberMe.checked ? "" : "Yes";
+      });
+    }
+
+    if (rememberMe.checked) {
+      if (isDNT()) {
+        rememberMe.checked = false;
+      } else {
+        PrivateWindow.then((isPrivate) => {
+          if (isPrivate) {
+            rememberMe.checked = false;
+          }
+        });
+      }
+    }
+  }
+
   var moreOptions = document.querySelectorAll(".more-options");
   if (moreOptions) {
     moreOptions.forEach(function (showButton) {
diff --git a/www/resources/extra/PrivateWindowCheck.js b/www/resources/extra/PrivateWindowCheck.js
new file mode 100644
index 0000000000000000000000000000000000000000..d2da39836713c591da1b79692fa252f78b474874
--- /dev/null
+++ b/www/resources/extra/PrivateWindowCheck.js
@@ -0,0 +1,92 @@
+/* https://github.com/jLynx/PrivateWindowCheck/blob/ea5c4f8ede26e9fac34f87e9e8a606e336efbcb2/PrivateWindowCheck.js */
+
+async function chrome76Detection() {
+  if ("storage" in navigator && "estimate" in navigator.storage) {
+    const { usage, quota } = await navigator.storage.estimate();
+    if (quota < 120000000) return true;
+    else return false;
+  } else {
+    return false;
+  }
+}
+
+function isNewChrome() {
+  var pieces = navigator.userAgent.match(
+    /Chrom(?:e|ium)\/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/
+  );
+  if (pieces == null || pieces.length != 5) {
+    return undefined;
+  }
+  major = pieces.map((piece) => parseInt(piece, 10))[1];
+  if (major >= 76) return true;
+  return false;
+}
+
+module.exports = new Promise(function (resolve, reject) {
+  try {
+    var isSafari =
+      navigator.vendor &&
+      navigator.vendor.indexOf("Apple") > -1 &&
+      navigator.userAgent &&
+      navigator.userAgent.indexOf("CriOS") == -1 &&
+      navigator.userAgent.indexOf("FxiOS") == -1;
+
+    if (isSafari) {
+      //Safari
+      var e = false;
+      if (window.safariIncognito) {
+        e = true;
+      } else {
+        try {
+          window.openDatabase(null, null, null, null);
+          window.localStorage.setItem("test", 1);
+          resolve(false);
+        } catch (t) {
+          e = true;
+          resolve(true);
+        }
+        void !e && ((e = !1), window.localStorage.removeItem("test"));
+      }
+    } else if (navigator.userAgent.includes("Firefox")) {
+      //Firefox
+      var db = indexedDB.open("test");
+      db.onerror = function () {
+        resolve(true);
+      };
+      db.onsuccess = function () {
+        resolve(false);
+      };
+    } else if (
+      navigator.userAgent.includes("Edge") ||
+      navigator.userAgent.includes("Trident") ||
+      navigator.userAgent.includes("msie")
+    ) {
+      //Edge or IE
+      if (!window.indexedDB && (window.PointerEvent || window.MSPointerEvent))
+        resolve(true);
+      resolve(false);
+    } else {
+      //Normally ORP or Chrome
+      //Other
+      if (isNewChrome()) resolve(chrome76Detection());
+
+      const fs = window.RequestFileSystem || window.webkitRequestFileSystem;
+      if (!fs) resolve(null);
+      else {
+        fs(
+          window.TEMPORARY,
+          100,
+          function (fs) {
+            resolve(false);
+          },
+          function (err) {
+            resolve(true);
+          }
+        );
+      }
+    }
+  } catch (err) {
+    console.log(err);
+    resolve(null);
+  }
+});
diff --git a/www/selectsource.php b/www/selectsource.php
index d259616c2d71c0e4631dfc0d656267d0c33f6084..16aba55c86f0090e7d1c2f9981d8779972e2a8d3 100644
--- a/www/selectsource.php
+++ b/www/selectsource.php
@@ -6,9 +6,12 @@ use League\CommonMark\CommonMarkConverter;
 use SimpleSAML\Auth\State;
 use SimpleSAML\Configuration;
 use SimpleSAML\Error\BadRequest;
+use SimpleSAML\Logger;
 use SimpleSAML\Metadata\MetaDataStorageHandler;
 use SimpleSAML\Module;
+use SimpleSAML\Module\campusmultiauth\Auth\Process\RememberMe;
 use SimpleSAML\Module\campusmultiauth\Auth\Source\Campusidp;
+use SimpleSAML\Module\campusmultiauth\Utils;
 use SimpleSAML\XHTML\Template;
 
 if (!array_key_exists('AuthState', $_REQUEST) && !array_key_exists('authstate', $_POST)) {
@@ -131,13 +134,11 @@ if (array_key_exists('source', $_POST)) {
         if (empty($_POST['username']) || empty($_POST['password'])) {
             $_REQUEST['wrongUserPass'] = true;
         } else {
-            if (
-                array_key_exists('remember_me', $_POST) &&
-                $_POST['remember_me'] === 'Yes' &&
-                Configuration::getInstance()->getBoolean('session.rememberme.enable', false)
-            ) {
-                Campusidp::setCookie(Campusidp::COOKIE_USERNAME, $_POST['username']);
-                Campusidp::setCookie(Campusidp::COOKIE_PASSWORD, $_POST['password']);
+            if (!empty($_POST['dont_remember_me']) && $_POST['dont_remember_me'] === 'Yes') {
+                $state['DontRememberMe'] = true;
+            }
+            if (!empty($_POST['remember_me']) && $_POST['remember_me'] === 'Yes') {
+                $state['RememberMe'] = true;
             }
 
             Campusidp::delegateAuthentication($_POST['source'], $state);
@@ -232,13 +233,12 @@ $t->data['authstate'] = $authStateId;
 $t->data['currentUrl'] = htmlentities($_SERVER['PHP_SELF']);
 $t->data['wayf_config'] = $wayfConfig;
 $t->data['rememberme_enabled'] = Configuration::getInstance()->getBoolean('session.rememberme.enable', false);
+$t->data['rememberme_checked'] = Configuration::getInstance()->getBoolean('session.rememberme.checked', false);
 $t->data['muni_jvs'] = ($wayfConfig['css_framework'] ?? 'bootstrap5') === 'muni_jvs';
 $t->data['idps'] = $idps;
 $t->data['no_js_display_index'] = !empty($_POST['componentIndex']) ? $_POST['componentIndex'] : null;
 $t->data['user_pass_source_name'] = $state[Campusidp::USER_PASS_SOURCE_NAME];
 $t->data['sp_source_name'] = $state[Campusidp::SP_SOURCE_NAME];
-$t->data['cookie_username'] = Campusidp::getCookie(Campusidp::COOKIE_USERNAME);
-$t->data['cookie_password'] = Campusidp::getCookie(Campusidp::COOKIE_PASSWORD);
 
 if (!empty($hintedIdps)) {
     $t->data['idpsToShow'] = $hintedIdps;
@@ -310,5 +310,94 @@ if (Campusidp::getCookie(Campusidp::COOKIE_PREVIOUS_IDPS) === null) {
     );
 }
 
+$rememberMe = new RememberMe();
+
+$dontRemember = filter_input(
+    INPUT_COOKIE,
+    $rememberMe->getDontCookieName(),
+    FILTER_DEFAULT,
+    [
+        'default' => '',
+    ]
+) === 'Yes';
+
+$t->data['dontRemember'] = $dontRemember;
+
+$config = Configuration::getOptionalConfig('module_campusmultiauth.php')->getConfigItem('remember_me', []);
+$imagesConfig = $config->getConfigItem('security_images', []);
+
+// verify cookie (if present), update counter and cookie
+
+if (
+    empty($t->data['forceUsername'])
+    && !empty($_GET[RememberMe::CLEAR_USERNAME_PARAM])
+) {
+    $t->data['username'] = '';
+    $t->data['userInfo'] = false;
+    $rememberMe->deleteCookie();
+}
+
+$t->data['userInfo'] = $dontRemember ? false : $rememberMe->getUserInfo();
+
+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'];
+        }
+
+        $pictureDir = $imagesConfig->getString('pictureDir', null);
+        if ($t->data['securityImage'] && $pictureDir !== null) {
+            $pictureDataSrc = $t->data['securityImage'];
+            if (preg_match('~^data:image/(png|jpeg|gif);base64,(.*)$~', $pictureDataSrc, $matches)) {
+                list(, $pictureType, $pictureContent) = $matches;
+                $pictureContent = base64_decode($pictureContent, true);
+                if ($pictureContent !== false) {
+                    $pictureFileName = sprintf(
+                        '%s-%s.%s',
+                        $t->data['username'],
+                        hash('sha256', $imagesConfig->getString('securityImageSalt') . $t->data['username']),
+                        $pictureType
+                    );
+                    if (!file_exists($pictureDir) && !mkdir($pictureDir, 0755, true)) {
+                        throw new \Error('Folder for security images does not exist and could not be created.');
+                    }
+                    $pictureFilePath = rtrim($pictureDir, '/') . '/' . $pictureFileName;
+                    file_put_contents($pictureFilePath, $pictureContent);
+                    if (image_type_to_mime_type(exif_imagetype($pictureFilePath)) !== 'image/' . $pictureType) {
+                        Logger::warning('Invalid security image, type mismatch: ' . $t->data['securityImage']);
+                        unlink($pictureFilePath);
+                    } else {
+                        $t->data['securityImage'] = sprintf(
+                            '%s/%s',
+                            rtrim($imagesConfig->getString('pictureBaseURL'), '/'),
+                            $pictureFileName
+                        );
+                    }
+                }
+            }
+        }
+    } elseif ($t->data['username'] !== $t->data['userInfo']['username']) {
+        $t->data['userInfo'] = false;
+    }
+}
+
+if (!empty($t->data['username'])) {
+    $t->data['autofocus'] = 'password';
+} else {
+    $t->data['autofocus'] = 'username';
+}
+
+$t->data['uidName'] = $config->getString('uidName', '');
+
+if ($t->data['userInfo'] !== false) {
+    $t->data['accessTarget'] = 'remembered-name';
+}
+
+$t->data['differentUsername'] = RememberMe::getOtherUsernameLink($authStateId);
+
 $t->show();
 exit();