From 7e3a950a1d34ee40ce8413486c6a7eb6efaeb3af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Bar=C3=A1nek?= <0Baranek.dominik0@gmail.com> Date: Wed, 10 Aug 2022 16:48:26 +0200 Subject: [PATCH] feat: aarc_discovery_hint --- .gitignore | 2 + README.md | 10 +- composer.json | 3 +- config-templates/module_campusmultiauth.php | 24 +- lib/Auth/Source/Campusidp.php | 434 +++++++++++++++++- templates/includes/individual-identities.twig | 2 +- templates/selectsource.twig | 8 +- www/idpSearch.php | 83 +--- www/resources/campus-idp.js | 4 + www/selectsource.php | 50 +- 10 files changed, 506 insertions(+), 114 deletions(-) diff --git a/.gitignore b/.gitignore index 42a118a..50f8ca1 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 c92d710..81041fb 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 6412e00..4c519df 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 817f9e2..7c0e5c5 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 e51198d..9211de3 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 3f1fc6e..1b2a47d 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 636dd3e..86f7f35 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 bb485f8..5f5a4b1 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 26dea11..fa28f84 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 f7b85ca..43b8809 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 { -- GitLab