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
@@ -1,4 +1,6 @@
# UNRELEASED
* Add a link on My Profile to reset remembered per-SP WAYF login-method choices #353
* Deployments must set the new `wayf_reset_url` parameter in `parameters.yaml` (see `config/openconext/parameters.yaml.dist`) to the EngineBlock `/reset-remember-wayf` endpoint.
* Upgrade the runtime and build toolchain to PHP 8.5 and Node.js 24.
* Refresh the CI Docker base image plus Composer and npm dependencies for the PHP 8.5 build.
* Preserve the originally requested deeplink after SAML login instead of redirecting to the homepage #325
Expand Down
7 changes: 4 additions & 3 deletions assets/css/components/modal-window.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
width: unset;
}

&--form {
width: 100%;
}
}

.modalWindow__button--form {
display: contents;
}
40 changes: 40 additions & 0 deletions assets/css/pages/myProfile/myProfile.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@import 'attributesTable';
@import '../../helpers/breakpoints';
@import '../../helpers/functions';
@import '../../helpers/variables';

.content__myProfile {
padding-right: calculateRem(70px);
Expand All @@ -21,3 +22,42 @@
.myProfile__note {
margin-bottom: calculateRem(80px);
}

.myProfile__wayfResetExplanation {
color: $titleBlack;
display: flex;
font-size: $f-normal;

@include screen('mobile') {
flex-direction: column;
}
}

.myProfile__wayfResetButtonWrapper {
flex: 0 0 max-content;
height: max-content;
min-width: max-content;
text-align: center;

@include screen('mobile') {
text-align: left;
}
}

.myProfile__wayfResetButton {
height: 100%;
width: 100%;
}

.myProfile__wayfResetText {
margin-top: 0;
}

@include ie11Only('.myProfile__wayfResetButtonWrapper, .myProfile__wayfResetButton, .myProfile__wayfResetText') {
display: inline-block;
}

@include ie11Only('.myProfile__wayfResetButton') {
height: auto;
min-width: 14rem;
}
Comment thread
johanib marked this conversation as resolved.
5 changes: 5 additions & 0 deletions config/openconext/parameters.yaml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ parameters:
engineblock_api_password: secret
engineblock_api_verify_ssl: true

# The EngineBlock endpoint that clears a user's remembered per-SP WAYF IdP
# choices. Profile links to this with a `redirect` query parameter so the
# user is sent back here afterwards. See OpenConext-engineblock#2085/#345.
wayf_reset_url: "https://engine.dev.openconext.local/reset-remember-wayf"
Comment thread
johanib marked this conversation as resolved.

user_lifecycle_enabled: true
user_lifecycle_api_base_url: "https://userlifecycle.dev.openconext.local/"
user_lifecycle_api_username: userlifecycle
Expand Down
5 changes: 5 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ services:
$removeConsentEnabled: '%remove_consent_enabled%'
tags: [ 'controller.service_arguments' ]

OpenConext\ProfileBundle\Controller\MyProfileController:
arguments:
$wayfResetUrl: '%wayf_reset_url%'
tags: [ 'controller.service_arguments' ]

OpenConext\ProfileBundle\Controller\InviteRolesController:
arguments:
$enabled: '%invite_roles_enabled%'
Expand Down
40 changes: 39 additions & 1 deletion src/OpenConext/ProfileBundle/Controller/MyProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,28 @@
namespace OpenConext\ProfileBundle\Controller;

use OpenConext\ProfileBundle\Service\UserService;
use OpenConext\ProfileBundle\Service\WayfResetLinkBuilder;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

class MyProfileController extends AbstractController
{

public function __construct(
private readonly UserService $userService,
private readonly LoggerInterface $logger,
private readonly WayfResetLinkBuilder $wayfResetLinkBuilder,
private readonly UrlGeneratorInterface $urlGenerator,
private readonly string $wayfResetUrl,
private readonly CsrfTokenManagerInterface $csrfTokenManager,
) {
}

Expand All @@ -49,7 +60,34 @@ public function overview(): Response

return $this->render(
'@OpenConextProfile/MyProfile/overview.html.twig',
['user' => $user],
[
'user' => $user,
],
);
}

#[Route(
path: "/my-profile/wayf-reset",
name: "profile.my_profile_wayf_reset",
methods: ["POST"],
schemes: "https",
)]
public function resetWayfChoice(
Request $request,
): RedirectResponse {
$token = new CsrfToken('wayf_reset', (string) $request->request->get('_token'));

if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new AccessDeniedHttpException('Invalid CSRF token for the WAYF reset action');
}

$this->logger->info('Redirecting to EngineBlock to reset the remembered WAYF choice');

$wayfResetLink = $this->wayfResetLinkBuilder->build(
$this->wayfResetUrl,
$this->urlGenerator->generate('profile.my_profile_overview', [], UrlGeneratorInterface::ABSOLUTE_URL),
);

return new RedirectResponse($wayfResetLink);
}
}
33 changes: 33 additions & 0 deletions src/OpenConext/ProfileBundle/Service/WayfResetLinkBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types = 1);

/**
* 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\ProfileBundle\Service;

final readonly class WayfResetLinkBuilder
{
public function build(
string $wayfResetUrl,
string $returnUrl,
): string {
$separator = str_contains($wayfResetUrl, '?') ? '&' : '?';

return $wayfResetUrl . $separator . 'redirect=' . rawurlencode($returnUrl);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

declare(strict_types = 1);

/**
* 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\ProfileBundle\Tests\Controller;

use OpenConext\Profile\Api\AuthenticatedUserProviderInterface;
use OpenConext\Profile\Repository\ContactPersonRepositoryInterface;
use OpenConext\Profile\Repository\LocaleRepositoryInterface;
use OpenConext\Profile\Repository\UserRepositoryInterface;
use OpenConext\Profile\Value\EntityId;
use OpenConext\Profile\Value\Locale;
use OpenConext\Profile\Value\LocaleSet;
use OpenConext\ProfileBundle\Controller\MyProfileController;
use OpenConext\ProfileBundle\Service\LocaleService;
use OpenConext\ProfileBundle\Service\SupportContactEmailService;
use OpenConext\ProfileBundle\Service\UserService;
use OpenConext\ProfileBundle\Service\WayfResetLinkBuilder;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

class MyProfileControllerTest extends TestCase
{
/**
* @test
* @group Controller
*/
public function it_redirects_to_the_wayf_reset_url_when_the_csrf_token_is_valid(): void
{
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
$csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with($this->equalTo(new CsrfToken('wayf_reset', 'valid-token')))
->willReturn(true);

$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->expects($this->once())
->method('generate')
->with('profile.my_profile_overview', [], UrlGeneratorInterface::ABSOLUTE_URL)
->willReturn('https://profile.example.org/my-profile');

$controller = new MyProfileController(
$this->createUserService(),
$this->createMock(LoggerInterface::class),
new WayfResetLinkBuilder(),
$urlGenerator,
'https://engine.example.org/reset-remember-wayf',
$csrfTokenManager,
);

$response = $controller->resetWayfChoice(new Request([], ['_token' => 'valid-token']));

$this->assertSame(
'https://engine.example.org/reset-remember-wayf?redirect=https%3A%2F%2Fprofile.example.org%2Fmy-profile',
$response->getTargetUrl(),
);
}

/**
* @test
* @group Controller
*/
public function it_denies_access_when_the_csrf_token_is_invalid(): void
{
$csrfTokenManager = $this->createMock(CsrfTokenManagerInterface::class);
$csrfTokenManager->expects($this->once())
->method('isTokenValid')
->with($this->equalTo(new CsrfToken('wayf_reset', 'invalid-token')))
->willReturn(false);

$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$urlGenerator->expects($this->never())->method('generate');

$controller = new MyProfileController(
$this->createUserService(),
$this->createMock(LoggerInterface::class),
new WayfResetLinkBuilder(),
$urlGenerator,
'https://engine.example.org/reset-remember-wayf',
$csrfTokenManager,
);

$this->expectException(AccessDeniedHttpException::class);
$this->expectExceptionMessage('Invalid CSRF token for the WAYF reset action');

$controller->resetWayfChoice(new Request([], ['_token' => 'invalid-token']));
}

private function createUserService(): UserService
{
return new UserService(
new SupportContactEmailService($this->createMock(ContactPersonRepositoryInterface::class)),
$this->createMock(UserRepositoryInterface::class),
$this->createMock(AuthenticatedUserProviderInterface::class),
new LocaleService(
$this->createMock(LocaleRepositoryInterface::class),
LocaleSet::create([new Locale('en')]),
new Locale('en'),
),
new EntityId('https://engine.example.org/authentication/sp/metadata'),
);
}
}
Loading
Loading