Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Changes:
* The `0000-00-00 00:00:00` is added for clarity/consistency, as this is probably the default behaviour of your database already.
* Removed unused index `consent.deleted_at`. Delete this from your production database if it's there.
* Added a specific error page for unsolicited SAML responses (IdP-initiated SSO without a prior AuthnRequest).
* A new parameter `wayf.preferred_idp_entity_ids` must be added to `parameters.yml`. To display a set of IdPs prominent at the top of the WAYF, add the entityId's of those IdPs to this parameter.
* To keep the old behaviour, set the value to `[]`

* Stabilized consent checks
* In order to make the consent hashes more robust, a more consistent way of hashing the user attributes has been introduced
Expand Down
5 changes: 5 additions & 0 deletions config/packages/parameters.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ parameters:
wayf.display_default_idp_banner_on_wayf: true
wayf.default_idp_entity_id: https://default-idp.dev.openconext.local

## Ordered list of IdP entity IDs to feature prominently at the top of the WAYF.
## These IdPs appear above the search field and are excluded from the regular searchable list.
## An empty list means no behaviour change.
wayf.preferred_idp_entity_ids: []

## Toggle display & content of global site notice
global.site_notice.show: false
global.site_notice.allowed.tags: '<a><u><i><br><wbr><strong><em><blink><marquee><p><ul><ol><dl><li><dd><dt><div><span><blockquote><hr><h2></h2><h3><h4><h5><h6>'
Expand Down
2 changes: 1 addition & 1 deletion config/services/ci/controllers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ services:
engineblock.functional_test.controller.wayf:
class: OpenConext\EngineBlockFunctionalTestingBundle\Controllers\WayfController
arguments:
- '@twig'
- '@OpenConext\EngineBlockBundle\Service\WayfRenderer'

engineblock.functional_test.controller.feedback:
class: OpenConext\EngineBlockFunctionalTestingBundle\Controllers\FeedbackController
Expand Down
11 changes: 11 additions & 0 deletions config/services/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@ services:
_defaults:
public: true

OpenConext\EngineBlock\Service\Wayf\IdpSplitter:

OpenConext\EngineBlockBundle\Service\WayfViewModelFactory:
arguments:
$wayfExtension: '@OpenConext\EngineBlockBundle\Twig\Extensions\Extension\Wayf'

OpenConext\EngineBlockBundle\Service\WayfRenderer:
autowire: true

OpenConext\EngineBlockBundle\Bridge\EngineBlockBootstrapper:
autowire: true
autoconfigure: true
arguments:
$preferredIdpEntityIds: '%wayf.preferred_idp_entity_ids%'
tags:
- { name: kernel.event_subscriber }

Expand Down
52 changes: 52 additions & 0 deletions docs/testing.md
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)
72 changes: 27 additions & 45 deletions library/EngineBlock/Corto/Module/Service/SingleSignOn.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use OpenConext\EngineBlock\Metadata\Factory\Factory\ServiceProviderFactory;
use OpenConext\EngineBlock\Metadata\Discovery;
use OpenConext\EngineBlock\Metadata\X509\KeyPairFactory;
use OpenConext\EngineBlock\Service\Wayf\WayfIdp;
use OpenConext\EngineBlockBundle\Service\DiscoverySelectionService;
use SAML2\AuthnRequest;
use SAML2\Constants;
Expand Down Expand Up @@ -462,8 +463,6 @@ protected function _showWayf(EngineBlock_Saml2_AuthnRequestAnnotationDecorator $

$currentLocale = $container->getLocaleProvider()->getLocale();

$cookies = $container->getSymfonyRequest()->cookies->all();

if ($request->isDebugRequest()) {
$serviceProvider = $this->getEngineSpRole($this->_server);
} else {
Expand All @@ -477,28 +476,21 @@ protected function _showWayf(EngineBlock_Saml2_AuthnRequestAnnotationDecorator $
$container->getDefaultIdPEntityId()
);

$defaultIdPInIdPList = $this->isDefaultIdPPresent($idpList);
$showDefaultIdpBanner = (bool) $container->shouldDisplayDefaultIdpBannerOnWayf() && $defaultIdPInIdPList;

$rememberChoiceFeature = $container->getRememberChoice();

$output = $this->twig->render(
'@theme/Authentication/View/Proxy/wayf.html.twig',
[
'action' => $action,
'greenHeader' => $serviceProvider->getDisplayName($currentLocale),
'helpLink' => '/authentication/idp/help-discover?lang=' . $currentLocale,
'backLink' => $container->isUiOptionReturnToSpActive(),
'cutoffPointForShowingUnfilteredIdps' => $container->getCutoffPointForShowingUnfilteredIdps(),
'showIdPBanner' => $showDefaultIdpBanner,
'rememberChoiceFeature' => $rememberChoiceFeature,
'showRequestAccess' => $serviceProvider->getCoins()->displayUnconnectedIdpsWayf(),
'requestId' => $request->getId(),
'serviceProvider' => $serviceProvider,
'idpList' => $idpList,
'cookies' => $cookies,
'showRequestAccessContainer' => true,
]
$diContainerRuntime = $application->getDiContainerRuntime();

$output = $diContainerRuntime->wayfRenderer->render(
idpList: $idpList,
preferredIdpEntityIds: $diContainerRuntime->getPreferredIdpEntityIds(),
action: $action,
currentLocale: $currentLocale,
defaultIdpEntityId: $container->getDefaultIdPEntityId(),
shouldDisplayBanner: (bool) $container->shouldDisplayDefaultIdpBannerOnWayf(),
backLink: $container->isUiOptionReturnToSpActive(),
cutoffPoint: $container->getCutoffPointForShowingUnfilteredIdps(),
rememberChoice: $container->getRememberChoice(),
showRequestAccess: $serviceProvider->getCoins()->displayUnconnectedIdpsWayf(),
requestId: $request->getId(),
serviceProvider: $serviceProvider,
);
$this->_server->sendOutput($output);
}
Expand Down Expand Up @@ -546,21 +538,21 @@ protected function _transformIdpsForWayf(array $idpEntityIds, $isDebugRequest, $
private function buildIdp(
?string $name,
string $logo,
$keywords,
array $keywords,
string $entityId,
bool $isAccessible,
bool $isDefaultIdP,
?string $discoveryHash
): array {
return array(
'Name' => $name,
'Logo' => $logo,
'Keywords' => $keywords,
'Access' => $isAccessible ? '1' : '0',
'ID' => md5($entityId),
'EntityID' => $entityId,
self::IS_DEFAULT_IDP_KEY => $isDefaultIdP,
'DiscoveryHash' => $discoveryHash,
): WayfIdp {
return new WayfIdp(
name: $name,
logo: $logo,
keywords: $keywords,
accessible: $isAccessible,
id: md5($entityId),
entityId: $entityId,
isDefaultIdp: $isDefaultIdP,
discoveryHash: $discoveryHash,
);
}
/**
Expand Down Expand Up @@ -675,14 +667,4 @@ protected function getEngineSpRole(EngineBlock_Corto_ProxyServer $proxyServer)
$serviceProvider = $this->_serviceProviderFactory->createEngineBlockEntityFrom($keyId);
return ServiceProvider::fromServiceProviderEntity($serviceProvider);
}

private function isDefaultIdPPresent(array $idpList): bool
{
foreach ($idpList as $idp) {
if ($idp[self::IS_DEFAULT_IDP_KEY] === true) {
return true;
}
}
return false;
}
}
57 changes: 57 additions & 0 deletions src/OpenConext/EngineBlock/Service/Wayf/IdpSplitter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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 WayfIdp[] $idpList Full transformed IdP list
* @param array $preferredEntityIds Ordered list of entity IDs to feature at the top
*/
public function split(array $idpList, array $preferredEntityIds): WayfSplitResult
{
if (empty($preferredEntityIds)) {
return new WayfSplitResult(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->accessible) {
$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 new WayfSplitResult(preferred: $preferred, regular: $regular);
}
}
51 changes: 51 additions & 0 deletions src/OpenConext/EngineBlock/Service/Wayf/WayfIdp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?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\EngineBlock\Service\Wayf;

final class WayfIdp
{
public function __construct(
public readonly ?string $name,
public readonly string $logo,
/** @var string[] */
public readonly array $keywords,
public readonly bool $accessible,
public readonly string $id,
public readonly string $entityId,
public readonly bool $isDefaultIdp,
public readonly ?string $discoveryHash,
) {
}

public function toArray(): array
{
return [
'Name' => $this->name,
'Logo' => $this->logo,
'Keywords' => $this->keywords,
'Access' => $this->accessible ? '1' : '0',
'ID' => $this->id,
'EntityID' => $this->entityId,
'isDefaultIdp' => $this->isDefaultIdp,
'DiscoveryHash' => $this->discoveryHash,
];
}
}
44 changes: 44 additions & 0 deletions src/OpenConext/EngineBlock/Service/Wayf/WayfSplitResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?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\EngineBlock\Service\Wayf;

final class WayfSplitResult
{
/**
* @param WayfIdp[] $preferred
* @param WayfIdp[] $regular
*/
public function __construct(
public readonly array $preferred,
public readonly array $regular,
) {
}

public function hasPreferred(): bool
{
return !empty($this->preferred);
}

public function containsInPreferred(string $entityId): bool
{
return array_any($this->preferred, static fn($idp) => $idp->entityId === $entityId);
}
}
11 changes: 10 additions & 1 deletion src/OpenConext/EngineBlockBundle/Bridge/DiContainerRuntime.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

namespace OpenConext\EngineBlockBundle\Bridge;

use OpenConext\EngineBlockBundle\Service\WayfRenderer;
use Twig\Environment;

/**
Expand All @@ -29,7 +30,15 @@
final readonly class DiContainerRuntime
{

public function __construct(public Environment $twig)
public function __construct(
public Environment $twig,
public WayfRenderer $wayfRenderer,
private array $preferredIdpEntityIds = [],
) {
}

public function getPreferredIdpEntityIds(): array
{
return $this->preferredIdpEntityIds;
}
}
Loading