diff --git a/.gitignore b/.gitignore
index 42a118a097e5514f900b001957a69432042741cd..50f8ca1f5becfefd7384a9e0fcfe0d5ea86cf9cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,8 @@ composer.phar
 /.phpunit.cache
 .phpunit.result.cache
 
+.idea/
+
 # Node
 logs
 *.log
diff --git a/README.md b/README.md
index c92d710e2ad1985cb96f285aadd74e49b9c08e45..81041fb5202da5bcff6638d8dda635ca8094debd 100644
--- a/README.md
+++ b/README.md
@@ -91,15 +91,13 @@ This component represents a form with username and password. It can be used only
 
 #### searchbox
 
-Thanks to searchbox you can search between all included identity providers. This components may be used multiple times.
+Thanks to the searchbox you can search between all included identity providers. This component may be used multiple times.
 
 `title` - text displayed above the component. 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.
 
 `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.
 
-`include` - 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, included are only identity providers mentioned here. This option is a map with three possible keys: `upstream_idps`, `tags` and `registration_authorities`. If you want to include single IdP, you can add its identifier (e.g. entityID) to the `upstream_idps` list. In case you want to include a group of identity providers, you may tag some of them in the [module metarefresh](https://github.com/simplesamlphp/simplesamlphp-module-metarefresh/blob/master/docs/simplesamlphp-automated_metadata.md) and then include them by adding their tag to the `tags` list. Every identity provider also has information about its registration authority (e.g. [http://www.eduid.cz/](http://www.eduid.cz/)). If you add some registration authority to the `registration_authorities` list, all identity providers from this authority will be included.
-
-`exclude` - if you want to display just part of identity providers available in the metadata, you can use this option. Each identity provider mentioned here will be excluded from the included ones. This option is a map with three possible keys: `upstream_idps`, `tags` and `registration_authorities`. If you want to exclude single IdP, you can add its identifier (e.g. entityID) to the `upstream_idps` list. In case you want to exclude a group of identity providers, you may tag some of them in the [module metarefresh](https://github.com/simplesamlphp/simplesamlphp-module-metarefresh/blob/master/docs/simplesamlphp-automated_metadata.md) and then exclude them by adding their tag to the `tags` list. Every identity provider also has information about its registration authority (e.g. [http://www.eduid.cz/](http://www.eduid.cz/)). If you add some registration authority to the `registration_authorities` list, all identity providers from this authority will be excluded.
+`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.
 
 `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.
 
@@ -137,6 +135,10 @@ Each identity is a map with the following possible options:
 
 To help the user choose the right institution to log in, this module supports following standards:
 
+### [aarc_discovery_hint (aarc_discovery_hint_uri)](https://docs.google.com/document/d/1rHKGzPsjkbqKHxsPnCb0itRLXLtqm-A8CZ5fzzklaxc/edit)
+
+A service provider can choose which identity provider(s) should user use. If there is only one option, the user is redirected directly to the identity provider. Otherwise, user chooses from identity providers sent in the `aarc_discovery_hint` parameter. In addition to this standard, service provider can use the `entityid` claim (instead of `entity_category` / `assurance_certification` / `registration_authority`) to include or exclude specific identity providers.
+
 ### [aarc_idp_hint](https://zenodo.org/record/4596667/files/AARC-G061-A_specification_for_IdP_hinting.pdf)
 
 A service provider can choose which identity provider should user use, he/she then skips the login page and is redirected to the targeted identity provider.
diff --git a/composer.json b/composer.json
index 6412e003f0137cc5e9b5cd0277f87295a52850bc..4c519df3108620022ad8f4f3261c1e2fe927ac11 100644
--- a/composer.json
+++ b/composer.json
@@ -16,7 +16,8 @@
     "simplesamlphp/composer-module-installer": "~1.0",
     "simplesamlphp/simplesamlphp": "^1.19",
     "league/commonmark": "^1.0",
-    "ext-intl": "*"
+    "ext-intl": "*",
+    "ext-simplexml": "*"
   },
   "config": {
     "allow-plugins": {
diff --git a/config-templates/module_campusmultiauth.php b/config-templates/module_campusmultiauth.php
index 817f9e24b6174273a1f455bf0bb4fb693b62abef..7c0e5c5152daa858696a0a24304d73a753cb340b 100644
--- a/config-templates/module_campusmultiauth.php
+++ b/config-templates/module_campusmultiauth.php
@@ -40,15 +40,21 @@ $config = [
                 'cs' => 'Vyhledejte napĹ™. CEITEC',
                 'en' => 'Search e.g. CEITEC',
             ],
-            'include' => [
-                'upstream_idps' => [],
-                'tags' => ['edugain'],
-                'registration_authorities' => [],
-            ],
-            'exclude' => [
-                'upstream_idps' => [],
-                'tags' => [],
-                'registration_authorities' => [],
+            'filter' => [
+                'exclude' => [
+                    'any_of' => [
+                        0 => [
+                            'entityid' => [
+                                'equals' => 'https://www.vutbr.cz/SSO/saml2/idp'
+                            ],
+                        ],
+                        1 => [
+                            'entityid' => [
+                                'equals' => 'https://idp2.ics.muni.cz/idp/shibboleth'
+                            ],
+                        ],
+                    ],
+                ],
             ],
             'logos' => [
                 'https://idp2.ics.muni.cz/idp/shibboleth' => 'https://id.muni.cz/android-chrome-192x192.png',
diff --git a/lib/Auth/Source/Campusidp.php b/lib/Auth/Source/Campusidp.php
index e51198d2ccab6e6f6036696690b54ed45d61eb9c..9211de34236a4b4b73e04afc9b839467b847575b 100644
--- a/lib/Auth/Source/Campusidp.php
+++ b/lib/Auth/Source/Campusidp.php
@@ -12,6 +12,7 @@ use SimpleSAML\Configuration;
 use SimpleSAML\Error;
 use SimpleSAML\Error\UnserializableException;
 use SimpleSAML\Logger;
+use SimpleSAML\Metadata\MetaDataStorageHandler;
 use SimpleSAML\Module;
 use SimpleSAML\Module\core\Auth\UserPassBase;
 use SimpleSAML\Module\ldap\Auth\Ldap;
@@ -43,6 +44,42 @@ class Campusidp extends Source
 
     public const IDP_HINT_BUTTONS_LIMIT = 5;
 
+    // idp hinting
+
+    public const IDPHINT = 'idphint';
+
+    public const AARC_IDP_HINT = 'aarc_idp_hint';
+
+    public const AARC_DISCOVERY_HINT = 'aarc_discovery_hint';
+
+    public const AARC_DISCOVERY_HINT_URI = 'aarc_discovery_hint_uri';
+
+    public const INCLUDE = 'include';
+
+    public const EXCLUDE = 'exclude';
+
+    public const ALL_OF = 'all_of';
+
+    public const ANY_OF = 'any_of';
+
+    public const ENTITY_CATEGORY = 'entity_category';
+
+    public const ASSURANCE_CERTIFICATION = 'assurance_certification';
+
+    public const REGISTRATION_AUTHORITY = 'registration_authority';
+
+    public const ENTITYID = 'entityid';
+
+    public const CONTAINS = 'contains';
+
+    public const EQUALS = 'equals';
+
+    public const MATCHES = 'matches';
+
+    public const ENTITY_CATEGORY_ATTR_NAME = 'http://macedir.org/entity-category';
+
+    public const ASSURANCE_CERTIFICATION_ATTR_NAME = 'urn:oasis:names:tc:SAML:attribute:assurance-certification';
+
     private $sources;
 
     private $userPassSourceName;
@@ -92,12 +129,20 @@ class Campusidp extends Source
 
     public function authenticate(&$state)
     {
-        if (array_key_exists('aarc_idp_hint', $_REQUEST)) {
-            $state['aarc_idp_hint'] = $_REQUEST['aarc_idp_hint'];
+        if (array_key_exists(self::AARC_IDP_HINT, $_REQUEST)) {
+            $state[self::AARC_IDP_HINT] = $_REQUEST[self::AARC_IDP_HINT];
+        }
+
+        if (array_key_exists(self::AARC_DISCOVERY_HINT, $_REQUEST)) {
+            $state[self::AARC_DISCOVERY_HINT] = $_REQUEST[self::AARC_DISCOVERY_HINT];
+        }
+
+        if (array_key_exists(self::AARC_DISCOVERY_HINT_URI, $_REQUEST)) {
+            $state[self::AARC_DISCOVERY_HINT_URI] = $_REQUEST[self::AARC_DISCOVERY_HINT_URI];
         }
 
-        if (array_key_exists('idphint', $_REQUEST)) {
-            $state['idphint'] = $_REQUEST['idphint'];
+        if (array_key_exists(self::IDPHINT, $_REQUEST)) {
+            $state[self::IDPHINT] = $_REQUEST[self::IDPHINT];
         }
 
         $state[self::AUTHID] = $this->authId;
@@ -225,10 +270,346 @@ class Campusidp extends Source
         return '';
     }
 
+    public static function getHintedIdps($hint)
+    {
+        if (array_key_exists(self::AARC_DISCOVERY_HINT_URI, $hint)) {
+            $discoveryHint = json_decode(file_get_contents($hint[self::AARC_DISCOVERY_HINT_URI]), true);
+        } elseif (array_key_exists(self::AARC_DISCOVERY_HINT, $hint)) {
+            $discoveryHint = $hint[self::AARC_DISCOVERY_HINT];
+        } else {
+            return null;
+        }
+
+        $metadataStorageHandler = MetaDataStorageHandler::getMetadataHandler();
+        $metadata = $metadataStorageHandler->getList();
+
+        $idps = [];
+
+        if (array_key_exists(self::INCLUDE, $discoveryHint)) {
+            if (empty($discoveryHint[self::INCLUDE])) {
+                return [];
+            } else {
+                foreach ($discoveryHint[self::INCLUDE] as $key => $value) {
+                    if ($key === self::ALL_OF) {
+                        $idps = array_merge($idps, self::getAllOfIdps($value, $metadata));
+                    } elseif ($key === self::ANY_OF) {
+                        $idps = array_merge($idps, self::getAnyOfIdps($value, $metadata));
+                    }
+                }
+            }
+        } else {
+            $idps = array_keys($metadata);
+        }
+
+        $idps = array_unique($idps);
+
+        if (!empty($discoveryHint[self::EXCLUDE])) {
+            foreach ($discoveryHint[self::EXCLUDE] as $key => $value) {
+                if ($key === self::ALL_OF) {
+                    $idps = array_diff($idps, self::getAllOfIdps($value, $metadata));
+                } elseif ($key === self::ANY_OF) {
+                    $r = self::getAnyOfIdps($value, $metadata);
+                    $idps = array_diff($idps, $r);
+                }
+            }
+        }
+
+        // TODO preferred
+
+        return $idps;
+    }
+
+    public static function getAllOfIdps($claim, $metadata, $type = null)
+    {
+        $result = [];
+        $isFirst = true;
+
+        if ($type === null) {
+            foreach ($claim as $array) {
+                foreach ($array as $key => $value) {
+                    switch ($key) {
+                        case self::ALL_OF:
+                            $isFirst ?
+                                $result = array_merge($result, self::getAllOfIdps($value, $metadata)) :
+                                $result = array_intersect($result, self::getAllOfIdps($value, $metadata));
+                            $isFirst = false;
+                            break;
+                        case self::ANY_OF:
+                            $isFirst ?
+                                $result = array_merge($result, self::getAnyOfIdps($value, $metadata)) :
+                                $result = array_intersect($result, self::getAnyOfIdps($value, $metadata));
+                            $isFirst = false;
+                            break;
+                        case self::ENTITY_CATEGORY:
+                            $isFirst ?
+                                $result = array_merge($result, self::getEntityCategoryIdps($value, $metadata)) :
+                                $result = array_intersect($result, self::getEntityCategoryIdps($value, $metadata));
+                            $isFirst = false;
+                            break;
+                        case self::ASSURANCE_CERTIFICATION:
+                            $isFirst ?
+                                $result = array_merge($result, self::getAssuranceCertificationIdps($value, $metadata)) :
+                                $result = array_intersect($result, self::getAssuranceCertificationIdps($value, $metadata));
+                            $isFirst = false;
+
+                            break;
+                        case self::REGISTRATION_AUTHORITY:
+                            $isFirst ?
+                                $result = array_merge($result, self::getRegistrationAuthorityIdps($value, $metadata)) :
+                                $result = array_intersect($result, self::getRegistrationAuthorityIdps($value, $metadata));
+                            $isFirst = false;
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            }
+        } else {
+            foreach ($claim as $item) {
+                switch ($type) {
+                    case self::ENTITY_CATEGORY:
+                        $isFirst ?
+                            $result = array_merge($result, self::getEntityCategoryIdps([self::CONTAINS => $item], $metadata)) :
+                            $result = array_intersect($result, self::getEntityCategoryIdps([self::CONTAINS => $item], $metadata));
+                        $isFirst = false;
+                        break;
+                    case self::ASSURANCE_CERTIFICATION:
+                        $isFirst ?
+                            $result = array_merge($result, self::getAssuranceCertificationIdps([self::CONTAINS => $item], $metadata)) :
+                            $result = array_intersect($result, self::getAssuranceCertificationIdps([self::CONTAINS => $item], $metadata));
+                        $isFirst = false;
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+
+        return array_unique($result);
+    }
+
+    public static function getAnyOfIdps($claim, $metadata, $type = null)
+    {
+        $result = [];
+
+        if ($type === null) {
+            foreach ($claim as $array) {
+                foreach ($array as $key => $value) {
+                    switch ($key) {
+                        case self::ALL_OF:
+                            $result = array_merge($result, self::getAllOfIdps($value, $metadata));
+                            break;
+                        case self::ANY_OF:
+                            $result = array_merge($result, self::getAnyOfIdps($value, $metadata));
+                            break;
+                        case self::ENTITY_CATEGORY:
+                            $result = array_merge($result, self::getEntityCategoryIdps($value, $metadata));
+                            break;
+                        case self::ASSURANCE_CERTIFICATION:
+                            $result = array_merge($result, self::getAssuranceCertificationIdps($value, $metadata));
+                            break;
+                        case self::REGISTRATION_AUTHORITY:
+                            $result = array_merge($result, self::getRegistrationAuthorityIdps($value, $metadata));
+                            break;
+                        case self::ENTITYID:
+                            $result = array_merge($result, self::getEntityidIdp($value, $metadata));
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            }
+        } else {
+            foreach ($claim as $item) {
+                switch ($type) {
+                    case self::ENTITY_CATEGORY:
+                        $result = array_merge($result, self::getEntityCategoryIdps([self::CONTAINS => $item], $metadata));
+                        break;
+                    case self::ASSURANCE_CERTIFICATION:
+                        $result = array_merge($result, self::getAssuranceCertificationIdps([self::CONTAINS => $item], $metadata));
+                        break;
+                    case self::REGISTRATION_AUTHORITY:
+                        $result = array_merge($result, self::getRegistrationAuthorityIdps([self::EQUALS => $item], $metadata));
+                        break;
+                    case self::ENTITYID:
+                        $result = array_merge($result, self::getEntityidIdp([self::EQUALS => $item], $metadata));
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+
+        return array_unique($result);
+    }
+
+    public static function getEntityCategoryIdps($claim, $metadata)
+    {
+        $result = [];
+
+        switch (array_key_first($claim)) {
+            case self::ALL_OF:
+                $result = array_merge($result, self::getAllOfIdps($claim[self::ALL_OF], $metadata, self::ENTITY_CATEGORY));
+                break;
+            case self::ANY_OF:
+                $result = array_merge($result, self::getAnyOfIdps($claim[self::ANY_OF], $metadata, self::ENTITY_CATEGORY));
+                break;
+            case self::CONTAINS:
+                foreach ($metadata as $entityid => $idpMetadata) {
+                    $entityCategories = self::getIdpEntityCategories($idpMetadata);
+
+                    if (self::contains($claim[self::CONTAINS], $entityCategories)) {
+                        $result[] = $entityid;
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+
+        return $result;
+    }
+
+    public static function getAssuranceCertificationIdps($claim, $metadata)
+    {
+        $result = [];
+
+        switch (array_key_first($claim)) {
+            case self::ALL_OF:
+                $result = array_merge($result, self::getAllOfIdps($claim[self::ALL_OF], $metadata, self::ASSURANCE_CERTIFICATION));
+                break;
+            case self::ANY_OF:
+                $result = array_merge($result, self::getAnyOfIdps($claim[self::ANY_OF], $metadata, self::ASSURANCE_CERTIFICATION));
+                break;
+            case self::CONTAINS:
+                foreach ($metadata as $entityid => $idpMetadata) {
+                    $assuranceCertifications = self::getIdpAssuranceCertifications($idpMetadata);
+
+                    if (self::contains($claim[self::CONTAINS], $assuranceCertifications)) {
+                        $result[] = $entityid;
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+
+        return $result;
+    }
+
+    public static function getRegistrationAuthorityIdps($claim, $metadata)
+    {
+        $result = [];
+
+        switch (array_key_first($claim)) {
+            case self::ANY_OF:
+                $result = array_merge($result, self::getAnyOfIdps($claim[self::ANY_OF], $metadata, self::REGISTRATION_AUTHORITY));
+                break;
+            case self::EQUALS:
+                foreach ($metadata as $entityid => $idpMetadata) {
+                    if (!empty($idpMetadata['RegistrationInfo']['registrationAuthority']) &&
+                        self::equals($idpMetadata['RegistrationInfo']['registrationAuthority'], $claim[self::EQUALS])) {
+                        $result[] = $entityid;
+                    }
+                }
+                break;
+            case self::MATCHES:
+                foreach ($metadata as $entityid => $idpMetadata) {
+                    if (!empty($idpMetadata['RegistrationInfo']['registrationAuthority']) &&
+                        self::matches($idpMetadata['RegistrationInfo']['registrationAuthority'], $claim[self::MATCHES])) {
+                        $result[] = $entityid;
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+
+        return $result;
+    }
+
+    public static function getEntityidIdp($claim, $metadata)
+    {
+        $result = [];
+
+        switch (array_key_first($claim)) {
+            case self::ANY_OF:
+                $result = array_merge($result, self::getAnyOfIdps($claim[self::ANY_OF], $metadata, self::ENTITYID));
+                break;
+            case self::EQUALS:
+                if (self::contains($claim[self::EQUALS], array_keys($metadata))) {
+                    $result[] = $claim[self::EQUALS];
+                }
+                break;
+            case self::MATCHES:
+                foreach (array_keys($metadata) as $entityid) {
+                    if (self::matches($entityid, $claim[self::MATCHES])) {
+                        $result[] = $entityid;
+                    }
+                }
+                break;
+            default:
+                break;
+        }
+
+        return $result;
+    }
+
+    public static function getIdpEntityCategories($idpMetadata)
+    {
+        return self::getAttrValues($idpMetadata, self::ENTITY_CATEGORY_ATTR_NAME);
+    }
+
+    public static function getIdpAssuranceCertifications($idpMetadata)
+    {
+        return self::getAttrValues($idpMetadata, self::ASSURANCE_CERTIFICATION_ATTR_NAME);
+    }
+
+    /**
+     * @deprecated
+     */
+    public static function getAttrValues($idpMetadata, $attrName)
+    {
+        $result = [];
+
+        if (empty($idpMetadata['entityDescriptor'])) {
+            return $result;
+        }
+
+        $xmlStr = base64_decode($idpMetadata['entityDescriptor']);
+        $xml = @simplexml_load_string($xmlStr); // temporary solution
+
+        $xml->registerXPathNamespace('md', 'urn:oasis:names:tc:SAML:2.0:metadata');
+        $xml->registerXPathNamespace('mdattr', 'urn:oasis:names:tc:SAML:metadata:attribute');
+        $xml->registerXPathNamespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion');
+
+        $attrs = $xml->xpath('//saml:Attribute[@Name="' . $attrName . '"]/saml:AttributeValue');
+        foreach ($attrs as $attr) {
+            $result[] = $attr->__toString();
+        }
+
+        return $result;
+    }
+
+    public static function contains($needle, $haystack)
+    {
+        return in_array($needle, $haystack);
+    }
+
+    public static function equals($string1, $string2)
+    {
+        return $string1 === $string2;
+    }
+
+    public static function matches($string, $pattern)
+    {
+        return preg_match($pattern, $string) === 1;
+    }
+
     public static function isIdpInCookie($idps, $entityid)
     {
         foreach ($idps as $idp) {
-            if ($idp['entityid'] === $entityid) {
+            if ($idp[self::ENTITYID] === $entityid) {
                 return true;
             }
         }
@@ -236,7 +617,7 @@ class Campusidp extends Source
         return false;
     }
 
-    public static function findSearchboxesToDisplay($hintedIdps, $config)
+    public static function findSearchboxesToDisplay($hint, $config, $state)
     {
         $result = [];
 
@@ -244,15 +625,38 @@ class Campusidp extends Source
             if ($config['components'][$i]['name'] === 'searchbox') {
                 $ch = curl_init();
 
-                curl_setopt(
-                    $ch,
-                    CURLOPT_URL,
-                    Module::getModuleURL(
-                        'campusmultiauth/idpSearch.php?idphint=' . json_encode(
-                            $hintedIdps
-                        ) . '&skipMatching=true' . '&index=' . $i
-                    )
-                );
+                if ($hint !== null) {
+                    curl_setopt(
+                        $ch,
+                        CURLOPT_URL,
+                        Module::getModuleURL(
+                            'campusmultiauth/idpSearch.php?' . self::IDPHINT . '=' . json_encode(
+                                $hint
+                            ) . '&skipMatching=true' . '&index=' . $i
+                        )
+                    );
+                } elseif (array_key_exists(self::AARC_DISCOVERY_HINT_URI, $state)) {
+                    curl_setopt(
+                        $ch,
+                        CURLOPT_URL,
+                        Module::getModuleURL(
+                            'campusmultiauth/idpSearch.php?' . self::AARC_DISCOVERY_HINT_URI . '=' . json_encode(
+                                $state['aarc_discovery_hint_uri']
+                            ) . '&skipMatching=true' . '&index=' . $i
+                        )
+                    );
+                } else {
+                    curl_setopt(
+                        $ch,
+                        CURLOPT_URL,
+                        Module::getModuleURL(
+                            'campusmultiauth/idpSearch.php?' . self::AARC_DISCOVERY_HINT . '=' . json_encode(
+                                $state['aarc_discovery_hint']
+                            ) . '&skipMatching=true' . '&index=' . $i
+                        )
+                    );
+                }
+
                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 
                 $idps = json_decode(curl_exec($ch));
diff --git a/templates/includes/individual-identities.twig b/templates/includes/individual-identities.twig
index 3f1fc6eb2e522a9b872e060e850987cfa8148198..1b2a47db260bddf622250ffa89f04db511cf4015 100644
--- a/templates/includes/individual-identities.twig
+++ b/templates/includes/individual-identities.twig
@@ -18,7 +18,7 @@
 
     {% set index = 0 %}
     {% for idp in configuration.identities %}
-        {% if idphint is not defined or idp.upstream_idp in idphint %}
+        {% if idpsToShow is not defined or idp.upstream_idp in idpsToShow %}
             <div class="{% if muni_jvs %}margin-bottom-12{% endif %}{% if index >= configuration.number_shown %} idp-hidden d-none vhide{% endif %}">
                 <button class="btn-individual-identity btn {% if muni_jvs %}btn-primary btn-border color-{{ configuration.priority }} hover-none-{{ configuration.priority }}{% else %}btn-light shadow-sm {% if configuration.priority == 'primary' %}border-dark text-dark{% else %}border-muted text-muted{% endif %} border-2{% endif %}" type="submit" name="idpentityid" value="{{ idp.upstream_idp }}">
                     {% if muni_jvs %}<span class="no-uppercase color-{{ configuration.priority }} individual-identity-span-wrap">{% endif %}
diff --git a/templates/selectsource.twig b/templates/selectsource.twig
index 636dd3ebd6797d91fa07af76f2887e184183479e..86f7f35ac61c533248124c4c47bcb1b219b62e4f 100644
--- a/templates/selectsource.twig
+++ b/templates/selectsource.twig
@@ -22,6 +22,12 @@
     {% if idphint is not defined %}{% set idphint = [] %}{% endif %}
     <meta name="idphint" content="{{ idphint | json_encode }}">
 
+    {% if aarc_discovery_hint is not defined %}{% set aarc_discovery_hint = [] %}{% endif %}
+    <meta name="aarc_discovery_hint" content="{{ aarc_discovery_hint | json_encode }}">
+
+    {% if aarc_discovery_hint_uri is not defined %}{% set aarc_discovery_hint_uri = '' %}{% endif %}
+    <meta name="aarc_discovery_hint_uri" content="{{ aarc_discovery_hint_uri | json_encode }}">
+
     <script type="module" src="/{{baseurlpath}}module.php/campusmultiauth/resources/campus-idp.js"></script>
 {% endblock %}
 
@@ -39,7 +45,7 @@
                                 {% include '@campusmultiauth/includes/individual-identities.twig' with {'configuration': hint_component_config, 'component_index': 0} %}
                             {% else %}
                                 {% for component_configuration in wayf_config.components %}
-                                    {% if component_configuration.name == 'local_login' and (idphint is not defined or component_configuration.entityid in idphint) %}
+                                    {% if component_configuration.name == 'local_login' and (idpsToShow is not defined or component_configuration.entityid in idpsToShow) %}
                                         {% include '@campusmultiauth/includes/local-login.twig' with {'configuration': component_configuration} %}
                                     {% elseif component_configuration.name == 'individual_identities' and (individual_identities_to_display is not defined or loop.index0 in individual_identities_to_display) %}
                                         {% include '@campusmultiauth/includes/individual-identities.twig' with {'configuration': component_configuration, 'component_index': loop.index0} %}
diff --git a/www/idpSearch.php b/www/idpSearch.php
index bb485f8e53a8811373dcf6ae6108f41c17162d9f..5f5a4b19c483ff1b55e5f18d66da866b4ef2fb86 100644
--- a/www/idpSearch.php
+++ b/www/idpSearch.php
@@ -14,91 +14,34 @@ $metadataStorageHandler = MetaDataStorageHandler::getMetadataHandler();
 $metadata = $metadataStorageHandler->getList();
 
 if (!empty($_GET['idphint']) && !isset($_GET['index'])) {
-    $filteredData = array_intersect_key($metadata, array_flip(json_decode($_GET['idphint'])));
+    $filteredData = array_intersect_key($metadata, array_flip(json_decode($_GET['idphint'], true)));
 } else {
     $index = $_GET['index'];
     $searchTerm = $_GET['q'] ?? '';
     $skipMatching = $_GET['skipMatching'] ?? false;
 
-
     $config = Configuration::getConfig('module_campusmultiauth.php')->toArray();
     $searchBox = $config['components'][$index];
 
-    if (!empty($_GET['idphint'])) {
+    if (!empty($_GET['aarc_discovery_hint_uri'])) {
+        $idphint = Campusidp::getHintedIdps(['aarc_discovery_hint_uri' => json_decode($_GET['aarc_discovery_hint_uri'])]);
+    } elseif (!empty($_GET['aarc_discovery_hint'])) {
+        $idphint = Campusidp::getHintedIdps(['aarc_discovery_hint' => json_decode($_GET['aarc_discovery_hint'])]);
+    } elseif (!empty($_GET['idphint'])) {
         $idphint = $_GET['idphint'];
         if (!is_array($idphint)) {
-            $idphint = json_decode($idphint);
-        }
-
-        $metadata = array_intersect_key($metadata, array_flip($idphint));
-    }
-
-    if (
-        !empty($searchBox['include']['upstream_idps']) ||
-        !empty($searchBox['include']['tags']) ||
-        !empty($searchBox['include']['registration_authorities'])
-    ) {
-        $filteredMetadata = [];
-
-        foreach ($metadata as $entityid => $idpentry) {
-            if (!empty($searchBox['include']['tags'])) {
-                foreach ($searchBox['include']['tags'] as $tag) {
-                    if (!empty($idpentry['tag']) && $tag === $idpentry['tag']) {
-                        $filteredMetadata[$entityid] = $idpentry;
-                        break;
-                    }
-                }
-            }
-
-            if (!empty($searchBox['include']['registration_authorities']) && empty($filteredMetadata[$entityid])) {
-                foreach ($searchBox['include']['registration_authorities'] as $registrationAuthority) {
-                    if (!empty($idpentry['RegistrationInfo']['registrationAuthority']) && $idpentry['RegistrationInfo']['registrationAuthority'] === $registrationAuthority) {
-                        $filteredMetadata[$entityid] = $idpentry;
-                        break;
-                    }
-                }
-            }
-
-            if (!empty($searchBox['include']['upstream_idps']) && empty($filteredMetadata[$entityid])) {
-                foreach ($searchBox['include']['upstream_idps'] as $upstreamIdp) {
-                    if ($upstreamIdp === $entityid) {
-                        $filteredMetadata[$entityid] = $idpentry;
-                        break;
-                    }
-                }
-            }
+            $idphint = json_decode($idphint, true);
         }
-
-        $metadata = $filteredMetadata;
+    } else {
+        $idphint = [];
     }
 
-    foreach ($metadata as $entityid => $idpentry) {
-        if (!empty($searchBox['exclude']['tags'])) {
-            foreach ($searchBox['exclude']['tags'] as $tag) {
-                if (!empty($idpentry['tag']) && $tag === $idpentry['tag']) {
-                    unset($metadata[$entityid]);
-                    break;
-                }
-            }
-        }
+    $metadata = array_intersect_key($metadata, array_flip($idphint));
 
-        if (!empty($searchBox['exclude']['registration_authorities']) && !empty($metadata[$entityid])) {
-            foreach ($searchBox['exclude']['registration_authorities'] as $registrationAuthority) {
-                if ($registrationAuthority === $idpentry['RegistrationInfo']['registrationAuthority']) {
-                    unset($metadata[$entityid]);
-                    break;
-                }
-            }
-        }
+    if (array_key_exists('filter', $searchBox)) {
+        $configFilteredIdps = Campusidp::getHintedIdps(['aarc_discovery_hint' => $searchBox['filter']]);
 
-        if (!empty($searchBox['exclude']['upstream_idps']) && !empty($metadata[$entityid])) {
-            foreach ($searchBox['exclude']['upstream_idps'] as $upstreamIdp) {
-                if ($upstreamIdp === $entityid) {
-                    unset($metadata[$entityid]);
-                    break;
-                }
-            }
-        }
+        $metadata = array_intersect_key($metadata, array_flip($configFilteredIdps));
     }
 
     if ($skipMatching) {
diff --git a/www/resources/campus-idp.js b/www/resources/campus-idp.js
index 26dea11732f28a76c416a3b668da943dfd0ef7fd..fa28f84ef5aa50938759db5adb37b1ddc88eadaf 100644
--- a/www/resources/campus-idp.js
+++ b/www/resources/campus-idp.js
@@ -80,6 +80,8 @@ function selectizeLoad(query, callback) {
 			q: query,
 			index: this.settings.myIndex,
 			idphint: this.settings.idphint,
+			aarc_discovery_hint: this.settings.aarcDiscoveryHint,
+			aarc_discovery_hint_uri: this.settings.aarcDiscoveryHintUri,
 			language: document.documentElement.getAttribute("lang"),
 			page_limit: 10,
 		},
@@ -182,6 +184,8 @@ document.addEventListener("DOMContentLoaded", function () {
 			idphint: JSON.parse(
 				document.querySelector('meta[name="idphint"]').content
 			),
+			aarcDiscoveryHint: document.querySelector('meta[name="aarc_discovery_hint"]').content,
+			aarcDiscoveryHintUri: document.querySelector('meta[name="aarc_discovery_hint_uri"]').content,
 			loadThrottle: 250,
 			placeholder: placeholderTexts[index] ?? defaultPlaceholder,
 			render: {
diff --git a/www/selectsource.php b/www/selectsource.php
index f7b85ca05059201fba77983b9353f61143f40b64..43b8809892074524737cbd1d2519ccffae087905 100644
--- a/www/selectsource.php
+++ b/www/selectsource.php
@@ -32,25 +32,36 @@ if (array_key_exists('aarc_idp_hint', $state)) {
     }
 }
 
-if (array_key_exists('idphint', $state)) {
-    $parts = explode(',', $state['idphint']);
+$hintedIdps = Campusidp::getHintedIdps($state);
 
-    if (count($parts) === 1) {
+if ($hintedIdps !== null || array_key_exists('idphint', $state)) {
+    if ($hintedIdps !== null && count($hintedIdps) === 1) {
+        $state['saml:idp'] = array_pop($hintedIdps);
+        Campusidp::delegateAuthentication($state[Campusidp::SP_SOURCE_NAME], $state);
+    } elseif ($hintedIdps === null && array_key_exists('idphint', $state) && count(explode(',', $state['idphint'])) === 1) {
         $state['saml:idp'] = urldecode($parts[0]);
         Campusidp::delegateAuthentication($state[Campusidp::SP_SOURCE_NAME], $state);
     } else {
-        $idphint = [];
-        foreach ($parts as $part) {
-            $idphint[] = urldecode($part);
+        $sendParsedHint = true;
+
+        if ($hintedIdps === null) {
+            $parts = explode(',', $state['idphint']);
+
+            $hintedIdps = [];
+            foreach ($parts as $part) {
+                $hintedIdps[] = urldecode($part);
+            }
+        } else {
+            $sendParsedHint = false;
         }
 
-        if (count($idphint) <= Campusidp::IDP_HINT_BUTTONS_LIMIT) {
+        if (count($hintedIdps) <= Campusidp::IDP_HINT_BUTTONS_LIMIT) {
             $ch = curl_init();
 
             curl_setopt(
                 $ch,
                 CURLOPT_URL,
-                Module::getModuleURL('campusmultiauth/idpSearch.php?idphint=' . json_encode($idphint))
+                Module::getModuleURL('campusmultiauth/idpSearch.php?idphint=' . json_encode($hintedIdps))
             );
             curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
 
@@ -224,19 +235,32 @@ $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($idphint)) {
-    $t->data['idphint'] = $idphint;
+if (!empty($hintedIdps)) {
+    $t->data['idpsToShow'] = $hintedIdps;
 
     if (empty($hintComponentConfig)) {
-        $searchboxesToDisplay = Campusidp::findSearchboxesToDisplay($idphint, $wayfConfig);
-        $individualIdentitiesToDisplay = Campusidp::findIndividualIdentitiesToDisplay($idphint, $wayfConfig);
+        if ($sendParsedHint) {
+            $t->data['idphint'] = $hintedIdps;
+            $searchboxesToDisplay = Campusidp::findSearchboxesToDisplay($hintedIdps, $wayfConfig, null);
+        } else {
+            if (!empty($state['aarc_discovery_hint'])) {
+                $t->data['aarc_discovery_hint'] = $state['aarc_discovery_hint'];
+            }
+            if (!empty($state['aarc_discovery_hint_uri'])) {
+                $t->data['aarc_discovery_hint_uri'] = $state['aarc_discovery_hint_uri'];
+            }
+
+            $searchboxesToDisplay = Campusidp::findSearchboxesToDisplay(null, $wayfConfig, $state);
+        }
+
+        $individualIdentitiesToDisplay = Campusidp::findIndividualIdentitiesToDisplay($hintedIdps, $wayfConfig);
 
         $t->data['searchboxes_to_display'] = $searchboxesToDisplay;
         $t->data['individual_identities_to_display'] = $individualIdentitiesToDisplay;
         $t->data['or_positions'] = Campusidp::getOrPositions(
             $searchboxesToDisplay,
             $individualIdentitiesToDisplay,
-            $idphint,
+            $hintedIdps,
             $wayfConfig
         );
     } else {