-
Notifications
You must be signed in to change notification settings - Fork 27
Add support for preferred IdPs in WAYF display #1985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # Testing | ||
|
|
||
| ## WAYF functional-testing page | ||
|
|
||
| The functional-testing route renders the WAYF page with synthetic IdP data, controllable via query parameters. Use it for manual verification and as the base URL for Cypress tests. | ||
|
|
||
| **Base URL:** `https://engine.dev.openconext.local/functional-testing/wayf` | ||
|
|
||
| ### Query parameters | ||
|
|
||
| | Parameter | Type | Default | Description | | ||
| |---|---|---|---| | ||
| | `lang` | string | `en` | Locale (`en` or `nl`) | | ||
| | `connectedIdps` | int | `5` | Number of connected IdPs to generate | | ||
| | `unconnectedIdps` | int | `0` | Number of unconnected IdPs to generate | | ||
| | `randomIdps` | int | `0` | Generate N IdPs with random (Faker) names instead; overrides connected/unconnected | | ||
| | `addDiscoveries` | bool | `true` | Add discovery entries to IdP 1 (gives it 3 list entries instead of 1) | | ||
| | `preferredIdpEntityIds[]` | string[] | `[]` | Entity IDs to feature in the preferred section (array syntax required) | | ||
| | `defaultIdpEntityId` | string | - | Entity ID of the default IdP (shows banner) | | ||
| | `showIdPBanner` | bool | `true` | Whether to show the default IdP banner | | ||
| | `displayUnconnectedIdpsWayf` | bool | `false` | Show unconnected IdPs with a "Request access" button | | ||
| | `backLink` | bool | `false` | Show "Return to service provider" back link | | ||
| | `rememberChoiceFeature` | bool | `false` | Show "Remember my choice" checkbox | | ||
| | `cutoffPointForShowingUnfilteredIdps` | int | `100` | Hide the IdP list until the user searches when list length exceeds this value | | ||
|
|
||
| #### Baseline | ||
| - [Default (5 connected IdPs)](https://engine.dev.openconext.local/functional-testing/wayf) | ||
| - [Dutch locale](https://engine.dev.openconext.local/functional-testing/wayf?lang=nl) | ||
| - [10 IdPs](https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=10&addDiscoveries=false) | ||
| - [Random IdPs (Faker names)](https://engine.dev.openconext.local/functional-testing/wayf?randomIdps=8) | ||
|
|
||
| #### Cutoff / search | ||
| - [Cutoff active - list hidden until search](https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=6&cutoffPointForShowingUnfilteredIdps=5) | ||
|
|
||
| #### Unconnected IdPs / request access | ||
| - [Unconnected IdPs visible, no request access](https://engine.dev.openconext.local/functional-testing/wayf?unconnectedIdps=3) | ||
| - [Unconnected IdPs + request access button](https://engine.dev.openconext.local/functional-testing/wayf?unconnectedIdps=3&displayUnconnectedIdpsWayf=true) | ||
|
|
||
| #### UI features | ||
| - [Back link](https://engine.dev.openconext.local/functional-testing/wayf?backLink=true) | ||
| - [Remember my choice](https://engine.dev.openconext.local/functional-testing/wayf?rememberChoiceFeature=true) | ||
| - [Default IdP banner](https://engine.dev.openconext.local/functional-testing/wayf?defaultIdpEntityId=https%3A%2F%2Fexample.com%2FentityId%2F3&showIdPBanner=true&addDiscoveries=false) | ||
|
|
||
| #### Preferred IdPs | ||
| - [1 preferred IdP](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&addDiscoveries=false) | ||
| - [2 preferred IdPs](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F2&addDiscoveries=false) | ||
| - [Preferred = default IdP > banner suppressed](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&defaultIdpEntityId=https%3A%2F%2Fexample.com%2FentityId%2F1&showIdPBanner=true&addDiscoveries=false) | ||
| - [Preferred ≠ default IdP > both visible](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&defaultIdpEntityId=https%3A%2F%2Fexample.com%2FentityId%2F2&showIdPBanner=true&addDiscoveries=false) | ||
| - [Preferred IdP with discoveries (1 IdP > 3 entries)](https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1) | ||
|
|
||
|
|
||
| - [All features enabled](https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=8&unconnectedIdps=2&displayUnconnectedIdpsWayf=true&preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1&defaultIdpEntityId=https%3A%2F%2Fexample.com%2FentityId%2F2&showIdPBanner=true&backLink=true&rememberChoiceFeature=true&addDiscoveries=false) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Copyright 2026 SURFnet B.V. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| namespace OpenConext\EngineBlock\Service\Wayf; | ||
|
|
||
| final class IdpSplitter | ||
| { | ||
| /** | ||
| * Splits the full IdP list into preferred (connected, in configured order) and regular (everything else). | ||
| * Preferred IdPs that are not connected are excluded from both sections. | ||
| * | ||
| * @param array $idpList Full transformed IdP list | ||
| * @param array $preferredEntityIds Ordered list of entity IDs to feature at the top | ||
| * @return array{preferred: array, regular: array} | ||
| */ | ||
| public function split(array $idpList, array $preferredEntityIds): array | ||
| { | ||
| if (empty($preferredEntityIds)) { | ||
| return ['preferred' => [], 'regular' => $idpList]; | ||
| } | ||
|
|
||
| $orderMap = array_flip($preferredEntityIds); | ||
| $preferredBuckets = array_fill(0, count($preferredEntityIds), []); | ||
| $regular = []; | ||
|
|
||
| foreach ($idpList as $idp) { | ||
| $entityId = $idp['EntityID']; | ||
| if (isset($orderMap[$entityId])) { | ||
| if ($idp['Access'] === '1') { | ||
| $preferredBuckets[$orderMap[$entityId]][] = $idp; | ||
| } | ||
| // Unconnected preferred IdPs are excluded from both sections. | ||
| } else { | ||
| $regular[] = $idp; | ||
| } | ||
| } | ||
|
|
||
| $mergeArgs = array_values(array_filter($preferredBuckets)); | ||
| $preferred = empty($mergeArgs) ? [] : array_merge(...$mergeArgs); | ||
|
|
||
| return ['preferred' => $preferred, 'regular' => $regular]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Copyright 2026 SURFnet B.V. | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace OpenConext\EngineBlockBundle\Service; | ||
|
|
||
| use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider; | ||
| use OpenConext\EngineBlock\Service\Wayf\IdpSplitter; | ||
| use Twig\Environment; | ||
|
|
||
| class WayfRenderer | ||
| { | ||
| public function __construct( | ||
| private readonly WayfViewModelFactory $factory, | ||
| private readonly IdpSplitter $splitter, | ||
| private readonly Environment $twig, | ||
| ) { | ||
| } | ||
|
|
||
| /** | ||
| * @SuppressWarnings(PHPMD.ExcessiveParameterList) | ||
| */ | ||
| public function render( | ||
| array $idpList, | ||
| array $preferredIdpEntityIds, | ||
| string $action, | ||
| string $currentLocale, | ||
| string $defaultIdpEntityId, | ||
| bool $shouldDisplayBanner, | ||
| bool $backLink, | ||
| int $cutoffPoint, | ||
| bool $rememberChoice, | ||
| bool $showRequestAccess, | ||
| string $requestId, | ||
| ServiceProvider $serviceProvider, | ||
| ): string { | ||
| $split = $this->splitter->split($idpList, $preferredIdpEntityIds); | ||
| $showPreferredIdps = !empty($split['preferred']); | ||
| $isDefaultIdpPreferred = in_array($defaultIdpEntityId, $preferredIdpEntityIds, true); | ||
|
|
||
| $showIdPBanner = $shouldDisplayBanner | ||
| && $this->isDefaultIdpPresent($idpList) | ||
| && (!$showPreferredIdps || !$isDefaultIdpPreferred); | ||
|
|
||
| $viewModel = $this->factory->create( | ||
| idpList: $idpList, | ||
| regularIdpList: $split['regular'], | ||
| preferredIdpList: $split['preferred'], | ||
| showPreferredIdps: $showPreferredIdps, | ||
| action: $action, | ||
| greenHeader: $serviceProvider->getDisplayName($currentLocale), | ||
| helpLink: '/authentication/idp/help-discover?lang=' . $currentLocale, | ||
| backLink: $backLink, | ||
| cutoffPointForShowingUnfilteredIdps: $cutoffPoint, | ||
| showIdPBanner: $showIdPBanner, | ||
| rememberChoiceFeature: $rememberChoice, | ||
| showRequestAccess: $showRequestAccess, | ||
| requestId: $requestId, | ||
| serviceProvider: $serviceProvider, | ||
| showRequestAccessContainer: true, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If it should always be |
||
| ); | ||
|
|
||
| return $this->twig->render('@theme/Authentication/View/Proxy/wayf.html.twig', $viewModel->toArray()); | ||
| } | ||
|
|
||
| private function isDefaultIdpPresent(array $idpList): bool | ||
| { | ||
| return array_any($idpList, fn($idp) => ($idp['isDefaultIdp'] ?? false) === true); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The banner is suppressed when the default IdP is in
$preferredIdpEntityIds, but this is the raw config list it doesn't know whether the IdP is actually connected to this SP.IdpSplittersilently drops disconnected preferred IdPs, so the default IdP can be "on the guest list" but never show up to the party.When this happens the default IdP is invisible everywhere: not in the preferred section (dropped by splitter), not in the regular list (preferred IdPs are excluded from regular), and the banner is suppressed too.