Skip to content
Open
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
6 changes: 6 additions & 0 deletions config/services/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ services:

OpenConext\EngineBlock\Service\Wayf\IdpSplitter:

OpenConext\EngineBlockBundle\Service\RememberChoiceDurationFormatter:
autowire: true
arguments:
$lifetimeInSeconds: '%wayf.remember_choice_per_idp_lifetime%'

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

OpenConext\EngineBlockBundle\Service\WayfRenderer:
autowire: true
Expand Down
3 changes: 2 additions & 1 deletion docs/js_testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ parameters can be used to manipulate the behaviour of the wayf that is rendered.
| **Query parameter** | **Default value** | **Explanation** |
|---|----|----|
| displayUnconnectedIdpsWayf | (bool) false | Type: boolean. Display unconnected IdPs on the WAYF. |
| rememberChoiceFeature | (bool) false | Type: boolean. Display the remember choice feature. |
| rememberChoiceFeature | (bool) false | Type: boolean. Display the remember choice feature in global mode. |
| rememberChoicePerIdp | (bool) false | Type: boolean. Display the remember choice feature in per-SP mode. Implies rememberChoiceFeature. |
| cutoffPointForShowingUnfilteredIdps | (int) 100 | Type: integer. The number of IdPs to display on the WAYF before cutting them off. |
| showIdpBanner | (bool) true | Type: boolean. Show the EduId (default IdP) banner on the WAYF or not |
| defaultIdpEntityId | (string) null | Type: string. The entityId of the default IdP (EduId) |
Expand Down
6 changes: 4 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ The functional-testing route renders the WAYF page with synthetic IdP data, cont
| `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 |
| `rememberChoiceFeature` | bool | `false` | Show "Remember my choice" checkbox (global mode: the choice is stored in the `rememberchoice` cookie) |
| `rememberChoicePerIdp` | bool | `false` | Show "Remember my choice" checkbox in per-SP mode (the choice is stored in the `rememberedidps` cookie). Implies `rememberChoiceFeature`, like in production where only one of the two modes can be enabled |
| `cutoffPointForShowingUnfilteredIdps` | int | `100` | Hide the IdP list until the user searches when list length exceeds this value |

#### Baseline
Expand All @@ -38,7 +39,8 @@ The functional-testing route renders the WAYF page with synthetic IdP data, cont

#### 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)
- [Remember my choice (global mode)](https://engine.dev.openconext.local/functional-testing/wayf?rememberChoiceFeature=true)
- [Remember my choice (per-SP mode)](https://engine.dev.openconext.local/functional-testing/wayf?rememberChoicePerIdp=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
Expand Down
5 changes: 5 additions & 0 deletions languages/messages.en.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@

// Remove cookies
'remember_choice' => 'Remember my choice',
'remember_choice_per_idp' => 'Remember my choice for this service for %duration%',
'remember_choice_duration_days' => '%count% days',
'remember_choice_duration_minutes' => '%count% minutes',
'remember_choice_tooltip_screenreader' => 'Why do we remember your choice?',
'remember_choice_per_idp_tooltip' => 'If you enable this option, you won\'t need to choose how you want to log into this service for the next %duration%. The login screen will then be skipped. <strong>Please note</strong>: do you sometimes use a different account to log in? If so, please disable this option. You can reverse this setting later at <a href="http://profile.surfconext.nl/">profile.surfconext.nl</a>.',
'cookie_removal_header' => 'Remove cookies',
'cookie_remove_button' => 'Remove',
'cookie_remove_all_button' => 'Remove all',
Expand Down
5 changes: 5 additions & 0 deletions languages/messages.nl.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@

// Remove cookies
'remember_choice' => 'Onthoud mijn keuze',
'remember_choice_per_idp' => 'Onthoud mijn keuze voor deze dienst voor %duration%',
'remember_choice_duration_days' => '%count% dagen',
'remember_choice_duration_minutes' => '%count% minuten',
'remember_choice_tooltip_screenreader' => 'Waarom onthouden we jouw keuze?',
'remember_choice_per_idp_tooltip' => 'Als je deze optie aanzet, hoef je de komende %duration% voor deze dienst niet meer te kiezen hoe je wilt inloggen. Het inlogscherm wordt dan overgeslagen. <strong>Let op</strong>: gebruik je soms een ander account om in te loggen? Zet de optie dan uit. Je kunt de keuze later weer terugdraaien op <a href="http://profile.surfconext.nl/">profile.surfconext.nl</a>.',
'cookie_removal_header' => 'Cookies verwijderen',
'cookie_remove_button' => 'Verwijderen',
'cookie_remove_all_button' => 'Alles verwijderen',
Expand Down
5 changes: 5 additions & 0 deletions languages/messages.pt.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@

// Remove cookies
'remember_choice' => 'Relembrar a minha escolha',
'remember_choice_per_idp' => 'Relembrar a minha escolha para este serviço durante %duration%',
'remember_choice_duration_days' => '%count% dias',
'remember_choice_duration_minutes' => '%count% minutos',
'remember_choice_tooltip_screenreader' => 'Porque é que relembramos a sua escolha?',
'remember_choice_per_idp_tooltip' => 'Se ativar esta opção, não terá de escolher como pretende iniciar sessão nos próximos %duration%. O ecrã de início de sessão será então ignorado. Atenção: por vezes utiliza uma conta diferente para iniciar sessão? Nesse caso, desative esta opção. Pode reverter esta opção mais tarde em <a href="http://profile.surfconext.nl/">profile.surfconext.nl</a>.',
'cookie_removal_header' => 'Remover cookies',
'cookie_remove_button' => 'Remover',
'cookie_remove_all_button' => 'Remover todos',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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 Symfony\Contracts\Translation\TranslatorInterface;

final class RememberChoiceDurationFormatter
{
private const SECONDS_PER_MINUTE = 60;
private const SECONDS_PER_DAY = 86400;

public function __construct(
private readonly TranslatorInterface $translator,
private readonly int $lifetimeInSeconds,
) {
}

public function format(): string
{
if ($this->lifetimeInSeconds < self::SECONDS_PER_DAY) {
$minutes = intdiv($this->lifetimeInSeconds, self::SECONDS_PER_MINUTE);
return $this->translator->trans('remember_choice_duration_minutes', ['%count%' => $minutes]);
}

$days = intdiv($this->lifetimeInSeconds, self::SECONDS_PER_DAY);
return $this->translator->trans('remember_choice_duration_days', ['%count%' => $days]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class WayfViewModelFactory
{
public function __construct(
private readonly Wayf $wayfExtension,
private readonly RememberChoiceDurationFormatter $durationFormatter,
) {
}

Expand Down Expand Up @@ -70,6 +71,7 @@ public function create(
regularIdpList: $regularIdpList,
preferredIdpList: $preferredIdpList,
rememberChoicePerIdp: $rememberChoicePerIdp,
rememberChoiceDuration: $rememberChoicePerIdp ? $this->durationFormatter->format() : '',
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public function __construct(
/** @var WayfIdp[] */
public array $preferredIdpList,
public bool $rememberChoicePerIdp = false,
public string $rememberChoiceDuration = '',
) {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public function wayfAction(Request $request)
$displayUnconnectedIdpsWayf = $request->query->getBoolean('displayUnconnectedIdpsWayf');
$addDiscoveries = $request->query->getBoolean('addDiscoveries', true);
$rememberChoiceFeature = $request->query->getBoolean('rememberChoiceFeature');
$rememberChoicePerIdp = $request->query->getBoolean('rememberChoicePerIdp');
$cutoffPointForShowingUnfilteredIdps = $request->query->getInt('cutoffPointForShowingUnfilteredIdps', 100);
$showIdPBanner = $request->query->getBoolean('showIdPBanner', true);
$defaultIdpEntityId = $request->query->get('defaultIdpEntityId', '');
Expand All @@ -66,10 +67,11 @@ public function wayfAction(Request $request)
shouldDisplayBanner: $showIdPBanner,
backLink: $backLink,
cutoffPoint: $cutoffPointForShowingUnfilteredIdps,
rememberChoice: $rememberChoiceFeature,
rememberChoice: $rememberChoiceFeature || $rememberChoicePerIdp,
showRequestAccess: $displayUnconnectedIdpsWayf,
requestId: 'bogus-request-id',
serviceProvider: TestEntitySeeder::buildSp(),
rememberChoicePerIdp: $rememberChoicePerIdp,
);

return new Response($output);
Expand Down
42 changes: 42 additions & 0 deletions tests/e2e/cypress/integration/skeune/wayf/wayf.general.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
matchSelector,
noResultSectionSelector,
remainingIdpSelector,
rememberChoicePerIdpClass,
rememberChoiceTooltipToggleSelector,
rememberChoiceTooltipValueSelector,
searchFieldSelector,
searchResetSelector,
searchSubmitSelector,
Expand Down Expand Up @@ -202,6 +205,45 @@ context('WAYF behaviour not tied to mouse / keyboard navigation', () => {
});
});

describe('Should show the per-SP remember my choice option with a tooltip', () => {
it('Renders the per-SP checkbox with a tooltip toggle', () => {
cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=5&rememberChoiceFeature=true&rememberChoicePerIdp=true');
cy.get(`.${rememberChoicePerIdpClass}`).should('exist');
cy.get(rememberChoiceTooltipToggleSelector).should('exist');
});

it('Hides the tooltip content until the toggle is activated', () => {
cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=5&rememberChoiceFeature=true&rememberChoicePerIdp=true');
cy.get(rememberChoiceTooltipValueSelector).should('not.be.visible');
cy.get(rememberChoiceTooltipToggleSelector).click({force: true});
cy.get(rememberChoiceTooltipValueSelector).should('be.visible');
cy.get(rememberChoiceTooltipToggleSelector).click({force: true});
cy.get(rememberChoiceTooltipValueSelector).should('not.be.visible');
});

it('Toggles aria-hidden and aria-expanded on the tooltip when the label is clicked', () => {
cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=5&rememberChoiceFeature=true&rememberChoicePerIdp=true');
cy.get(rememberChoiceTooltipValueSelector).should('have.attr', 'aria-hidden', 'true');
cy.get(rememberChoiceTooltipToggleSelector).should('have.attr', 'aria-expanded', 'false');
cy.get('.wayf__rememberChoice label.tooltip').click();
cy.get(rememberChoiceTooltipValueSelector).should('not.have.attr', 'aria-hidden');
cy.get(rememberChoiceTooltipToggleSelector).should('have.attr', 'aria-expanded', 'true');
});

it('Opens the tooltip when the label is focused and Enter is pressed', () => {
cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=5&rememberChoiceFeature=true&rememberChoicePerIdp=true');
cy.get('.wayf__rememberChoice label.tooltip').focus().type('{enter}');
cy.get(rememberChoiceTooltipValueSelector).should('not.have.attr', 'aria-hidden');
});

it('Does not show the tooltip toggle for the global (non per-SP) variant', () => {
cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?connectedIdps=5&rememberChoiceFeature=true&rememberChoicePerIdp=false');
cy.get(`.${rememberChoicePerIdpClass}`).should('not.exist');
cy.get(rememberChoiceTooltipToggleSelector).should('not.exist');
cy.onPage('Remember my choice');
});
});

describe('Preferred IdPs section heading', () => {
it('Should show the preferred IdPs section with the correct heading when preferred IdPs are configured', () => {
cy.visit('https://engine.dev.openconext.local/functional-testing/wayf?preferredIdpEntityIds%5B%5D=https%3A%2F%2Fexample.com%2FentityId%2F1');
Expand Down
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.
*/

declare(strict_types=1);

namespace Tests\OpenConext\EngineBlockBundle\Service;

use Mockery as m;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use OpenConext\EngineBlockBundle\Service\RememberChoiceDurationFormatter;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Translation\TranslatorInterface;

class RememberChoiceDurationFormatterTest extends TestCase
{
use MockeryPHPUnitIntegration;

#[DataProvider('lifetimeProvider')]
public function testFormat(int $lifetimeInSeconds, string $expectedKey, array $expectedParameters): void
{
$translator = m::mock(TranslatorInterface::class);
$translator->shouldReceive('trans')
->once()
->with($expectedKey, $expectedParameters)
->andReturn('formatted-duration');

$formatter = new RememberChoiceDurationFormatter($translator, $lifetimeInSeconds);

$this->assertSame('formatted-duration', $formatter->format());
}

public static function lifetimeProvider(): array
{
return [
'ninety days (default lifetime)' => [7776000, 'remember_choice_duration_days', ['%count%' => 90]],
'exactly one day' => [86400, 'remember_choice_duration_days', ['%count%' => 1]],
'one second under a day' => [86399, 'remember_choice_duration_minutes', ['%count%' => 1439]],
'forty-five minutes' => [2700, 'remember_choice_duration_minutes', ['%count%' => 45]],
'zero seconds' => [0, 'remember_choice_duration_minutes', ['%count%' => 0]],
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?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 Tests\OpenConext\EngineBlockBundle\Service;

use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider;
use OpenConext\EngineBlockBundle\Service\RememberChoiceDurationFormatter;
use OpenConext\EngineBlockBundle\Service\WayfViewModelFactory;
use OpenConext\EngineBlockBundle\Twig\Extensions\Extension\ConnectedIdps;
use OpenConext\EngineBlockBundle\Twig\Extensions\Extension\Wayf;
use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Translation\TranslatorInterface;

class WayfViewModelFactoryTest extends TestCase
{
public function testRememberChoiceDurationIsTakenFromTheFormatter(): void
{
$wayfExtension = $this->createMock(Wayf::class);
$wayfExtension->method('getConnectedIdps')->willReturn(new ConnectedIdps([], []));

$translator = $this->createStub(TranslatorInterface::class);
$translator->method('trans')->willReturn('90 days');
$durationFormatter = new RememberChoiceDurationFormatter($translator, 7776000);

$factory = new WayfViewModelFactory($wayfExtension, $durationFormatter);

$viewModel = $factory->create(
idpList: [],
regularIdpList: [],
preferredIdpList: [],
showPreferredIdps: false,
action: '/sso',
greenHeader: 'SP',
helpLink: '/help',
backLink: false,
cutoffPointForShowingUnfilteredIdps: 100,
showIdPBanner: false,
rememberChoiceFeature: true,
showRequestAccess: false,
requestId: 'req-1',
serviceProvider: $this->createStub(ServiceProvider::class),
rememberChoicePerIdp: true,
);

$this->assertSame('90 days', $viewModel->rememberChoiceDuration);
}

public function testRememberChoiceDurationIsEmptyWhenNotPerIdp(): void
{
$wayfExtension = $this->createMock(Wayf::class);
$wayfExtension->method('getConnectedIdps')->willReturn(new ConnectedIdps([], []));

$translator = $this->createStub(TranslatorInterface::class);
$translator->method('trans')->willReturn('90 days');
$durationFormatter = new RememberChoiceDurationFormatter($translator, 7776000);

$factory = new WayfViewModelFactory($wayfExtension, $durationFormatter);

$viewModel = $factory->create(
idpList: [],
regularIdpList: [],
preferredIdpList: [],
showPreferredIdps: false,
action: '/sso',
greenHeader: 'SP',
helpLink: '/help',
backLink: false,
cutoffPointForShowingUnfilteredIdps: 100,
showIdPBanner: false,
rememberChoiceFeature: true,
showRequestAccess: false,
requestId: 'req-1',
serviceProvider: $this->createStub(ServiceProvider::class),
rememberChoicePerIdp: false,
);

$this->assertSame('', $viewModel->rememberChoiceDuration);
}
}
Loading
Loading