Skip to content

Commit c5faa09

Browse files
committed
Don't override already set language
1 parent 770f6b7 commit c5faa09

6 files changed

Lines changed: 110 additions & 40 deletions

File tree

docs/6-oidc-upgrade.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,16 @@ per-field metadata policy (honored / validated / rejected) is documented in
185185
endpoints (previously ignored). The parameter carries the End-User's preferred
186186
UI languages as a space-separated list of BCP47 language tags, ordered by
187187
preference. The most preferred requested language which is also available in
188-
SimpleSAMLphp (per the `language.available` config option) is applied using the
189-
standard SimpleSAMLphp language cookie — the same mechanism as when the user
190-
picks a language on any SimpleSAMLphp page — so subsequent screens shown during
191-
the flow (login page, consent, logout page...) are rendered in the requested
192-
language. Matching includes a fallback to the primary language subtag (for
188+
SimpleSAMLphp (per the `language.available` config option) is used to render
189+
the screens shown during the flow. On the authorization endpoint, where the
190+
login page and consent are rendered by SimpleSAMLphp in subsequent requests,
191+
this is done via the standard SimpleSAMLphp language cookie (the same mechanism
192+
as when the user picks a language on any SimpleSAMLphp page); to avoid a
193+
client-supplied parameter silently overwriting a persistent, instance-wide
194+
preference, an already present language cookie (an explicit choice the user
195+
made) is not overridden. On the end session endpoint, the logout page is
196+
rendered directly in the requested language, without setting the language
197+
cookie. Matching includes a fallback to the primary language subtag (for
193198
example, requested `fr-CA` matches available `fr`). Per specification this is
194199
best-effort: if none of the requested languages are available, the parameter is
195200
ignored without raising an error. The available languages are also advertised

src/Bridges/SspBridge/Locale/Language.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public function setLanguageCookie(string $language): void
1414
SspLanguage::setLanguageCookie($language);
1515
}
1616

17+
public function getLanguageCookie(): ?string
18+
{
19+
return SspLanguage::getLanguageCookie();
20+
}
21+
1722
/**
1823
* Get the languages available in SimpleSAMLphp (configured in language.available and known to the
1924
* translation system), as computed by SimpleSAMLphp itself. The Language instance is created without

src/Controllers/AuthorizationController.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ public function authorization(Request $request): Response
136136
* page), so subsequent screens shown during the authentication flow (login page, consent...) are
137137
* rendered in the requested language. Per specification this is best-effort, so no error is raised
138138
* if none of the requested languages are available.
139+
*
140+
* The SimpleSAMLphp language cookie is a persistent, instance-wide preference, so an already present
141+
* cookie (an explicit language choice the user, or a previous interaction, has made) is not
142+
* overridden by the client-supplied ui_locales value.
139143
*/
140144
protected function setUiLanguage(OAuth2AuthorizationRequestInterface $authorizationRequest): void
141145
{
@@ -149,6 +153,14 @@ protected function setUiLanguage(OAuth2AuthorizationRequestInterface $authorizat
149153
return;
150154
}
151155

156+
if ($this->sspBridge->locale()->language()->getLanguageCookie() !== null) {
157+
$this->loggerService->debug(
158+
'AuthorizationController: not overriding existing language cookie with ui_locales parameter.',
159+
['uiLocales' => $authorizationRequest->getUiLocales(), 'language' => $language],
160+
);
161+
return;
162+
}
163+
152164
$this->loggerService->debug(
153165
'AuthorizationController: setting UI language based on ui_locales parameter.',
154166
['uiLocales' => $authorizationRequest->getUiLocales(), 'language' => $language],

src/Controllers/EndSessionController.php

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use League\OAuth2\Server\Exception\OAuthServerException;
88
use Psr\Http\Message\ServerRequestInterface;
99
use SimpleSAML\Module\oidc\Bridges\PsrHttpBridge;
10-
use SimpleSAML\Module\oidc\Bridges\SspBridge;
1110
use SimpleSAML\Module\oidc\Factories\TemplateFactory;
1211
use SimpleSAML\Module\oidc\Server\AuthorizationServer;
1312
use SimpleSAML\Module\oidc\Server\LogoutHandlers\BackChannelLogoutHandler;
@@ -35,7 +34,6 @@ public function __construct(
3534
protected PsrHttpBridge $psrHttpBridge,
3635
protected ErrorResponder $errorResponder,
3736
protected UiLocalesResolver $uiLocalesResolver,
38-
protected SspBridge $sspBridge,
3937
) {
4038
}
4139

@@ -55,7 +53,7 @@ public function __invoke(ServerRequestInterface $request): Response
5553

5654
$logoutRequest = $this->authorizationServer->validateLogoutRequest($request);
5755

58-
$uiLanguage = $this->setUiLanguage($logoutRequest);
56+
$uiLanguage = $this->resolveUiLanguage($logoutRequest);
5957

6058
// Set indication that the logout is initiated using OIDC protocol. This will be checked in the
6159
// logoutHandler() method.
@@ -144,14 +142,16 @@ public function __invoke(ServerRequestInterface $request): Response
144142
}
145143

146144
/**
147-
* Set the UI language for the current user agent based on the ui_locales logout request parameter, if any of
148-
* the requested languages are available in SimpleSAMLphp. This is done using the standard SimpleSAMLphp
149-
* language cookie (same mechanism as when the user picks a language on any SimpleSAMLphp page). Per
150-
* specification this is best-effort, so no error is raised if none of the requested languages are
151-
* available. Returns the resolved language, so it can also be applied when rendering the logout
152-
* page in the current request (the language cookie only affects subsequent requests).
145+
* Resolve the UI language to use for the logout page based on the ui_locales logout request parameter, if
146+
* any of the requested languages are available in SimpleSAMLphp. The resolved language is applied only when
147+
* rendering the logout page in the current request (see resolveResponse()); unlike the authorization flow,
148+
* the logout flow renders at most a single page and has no subsequent SimpleSAMLphp screens, so the
149+
* persistent, instance-wide language cookie is intentionally not set here (it would be a side effect
150+
* affecting later unrelated screens, including when a post_logout_redirect_uri is used and no page is
151+
* rendered at all). Per specification this is best-effort, so no error is raised if none of the
152+
* requested languages are available.
153153
*/
154-
protected function setUiLanguage(LogoutRequest $logoutRequest): ?string
154+
protected function resolveUiLanguage(LogoutRequest $logoutRequest): ?string
155155
{
156156
$language = $this->uiLocalesResolver->resolve($logoutRequest->getUiLocales());
157157

@@ -160,12 +160,10 @@ protected function setUiLanguage(LogoutRequest $logoutRequest): ?string
160160
}
161161

162162
$this->loggerService->debug(
163-
'EndSessionController: setting UI language based on ui_locales parameter.',
163+
'EndSessionController: resolved UI language based on ui_locales parameter.',
164164
['uiLocales' => $logoutRequest->getUiLocales(), 'language' => $language],
165165
);
166166

167-
$this->sspBridge->locale()->language()->setLanguageCookie($language);
168-
169167
return $language;
170168
}
171169

tests/unit/src/Controllers/AuthorizationControllerTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,4 +563,35 @@ public function testDoesNotSetUiLanguageWhenNoRequestedLanguageIsAvailable(): vo
563563

564564
($this->mock())($this->serverRequestStub);
565565
}
566+
567+
/**
568+
* @throws \Throwable
569+
*/
570+
public function testDoesNotOverrideExistingLanguageCookieWithUiLocales(): void
571+
{
572+
$this->authorizationRequestMock->method('getUiLocales')->willReturn('hr en');
573+
$this->uiLocalesResolverStub->method('resolve')->willReturn('hr');
574+
// An explicit language choice is already stored in the language cookie.
575+
$this->sspBridgeLocaleLanguageMock->method('getLanguageCookie')->willReturn('de');
576+
577+
$this->authorizationServerStub
578+
->method('validateAuthorizationRequest')
579+
->willReturn($this->authorizationRequestMock);
580+
$this->authorizationServerStub
581+
->method('completeAuthorizationRequest')
582+
->willReturn($this->responseStub);
583+
584+
$this->serverRequestStub->method('getQueryParams')->willReturn([]);
585+
586+
$this->authenticationServiceStub->method('manageState')->willReturn($this->state);
587+
$this->authenticationServiceStub->method('getAuthenticateUser')->willReturn($this->userEntityStub);
588+
$this->authenticationServiceStub
589+
->method('getAuthorizationRequestFromState')
590+
->willReturn($this->authorizationRequestMock);
591+
592+
$this->sspBridgeLocaleLanguageMock->expects($this->never())
593+
->method('setLanguageCookie');
594+
595+
($this->mock())($this->serverRequestStub);
596+
}
566597
}

tests/unit/src/Controllers/EndSessionControllerTest.php

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use PHPUnit\Framework\TestCase;
1212
use SimpleSAML\Error\BadRequest;
1313
use SimpleSAML\Module\oidc\Bridges\PsrHttpBridge;
14-
use SimpleSAML\Module\oidc\Bridges\SspBridge;
1514
use SimpleSAML\Module\oidc\Controllers\EndSessionController;
1615
use SimpleSAML\Module\oidc\Factories\TemplateFactory;
1716
use SimpleSAML\Module\oidc\Server\AuthorizationServer;
@@ -25,6 +24,7 @@
2524
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
2625
use SimpleSAML\OpenID\Core\IdToken;
2726
use SimpleSAML\Session;
27+
use SimpleSAML\XHTML\Template;
2828
use Symfony\Component\HttpFoundation\RedirectResponse;
2929
use Symfony\Component\HttpFoundation\Response;
3030

@@ -51,9 +51,6 @@ class EndSessionControllerTest extends TestCase
5151
protected MockObject $psrHttpBridgeMock;
5252
protected MockObject $errorResponderMock;
5353
protected Stub $uiLocalesResolverStub;
54-
protected MockObject $sspBridgeMock;
55-
protected MockObject $sspBridgeLocaleMock;
56-
protected MockObject $sspBridgeLocaleLanguageMock;
5754

5855
/**
5956
* @throws \PHPUnit\Framework\MockObject\Exception
@@ -76,25 +73,19 @@ public function setUp(): void
7673
$this->errorResponderMock = $this->createMock(ErrorResponder::class);
7774

7875
$this->uiLocalesResolverStub = $this->createStub(UiLocalesResolver::class);
79-
$this->sspBridgeMock = $this->createMock(SspBridge::class);
80-
$this->sspBridgeLocaleMock = $this->createMock(SspBridge\Locale::class);
81-
$this->sspBridgeLocaleLanguageMock = $this->createMock(SspBridge\Locale\Language::class);
82-
$this->sspBridgeMock->method('locale')->willReturn($this->sspBridgeLocaleMock);
83-
$this->sspBridgeLocaleMock->method('language')->willReturn($this->sspBridgeLocaleLanguageMock);
8476
}
8577

86-
protected function mock(): EndSessionController
78+
protected function mock(?TemplateFactory $templateFactory = null): EndSessionController
8779
{
8880
return new EndSessionController(
8981
$this->authorizationServerStub,
9082
$this->sessionServiceStub,
9183
$this->sessionLogoutTicketStoreBuilderStub,
9284
$this->loggerServiceMock,
93-
$this->templateFactoryStub,
85+
$templateFactory ?? $this->templateFactoryStub,
9486
$this->psrHttpBridgeMock,
9587
$this->errorResponderMock,
9688
$this->uiLocalesResolverStub,
97-
$this->sspBridgeMock,
9889
);
9990
}
10091

@@ -232,37 +223,65 @@ public function testLogoutHandler(): never
232223
* @throws \SimpleSAML\Error\BadRequest
233224
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
234225
*/
235-
public function testSetsUiLanguageBasedOnUiLocales(): void
226+
public function testRendersLogoutPageInResolvedUiLanguage(): void
236227
{
237228
$this->currentSessionMock->method('getAuthorities')->willReturn([]);
238229
$this->sessionServiceStub->method('getCurrentSession')->willReturn($this->currentSessionMock);
239230
$this->logoutRequestStub->method('getUiLocales')->willReturn('hr en');
240231
$this->authorizationServerStub->method('validateLogoutRequest')->willReturn($this->logoutRequestStub);
241232
$this->uiLocalesResolverStub->method('resolve')->willReturn('hr');
242233

243-
$this->sspBridgeLocaleLanguageMock->expects($this->once())
244-
->method('setLanguageCookie')
245-
->with('hr');
246-
247-
$this->mock()->__invoke($this->serverRequestStub);
234+
$this->assertSame('hr', $this->captureRenderedTemplateLanguage());
248235
}
249236

250237
/**
251238
* @throws \Throwable
252239
* @throws \SimpleSAML\Error\BadRequest
253240
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
254241
*/
255-
public function testDoesNotSetUiLanguageWhenNoRequestedLanguageIsAvailable(): void
242+
public function testRendersLogoutPageWithoutLanguageWhenNoneResolved(): void
256243
{
257244
$this->currentSessionMock->method('getAuthorities')->willReturn([]);
258245
$this->sessionServiceStub->method('getCurrentSession')->willReturn($this->currentSessionMock);
259246
$this->logoutRequestStub->method('getUiLocales')->willReturn('de');
260247
$this->authorizationServerStub->method('validateLogoutRequest')->willReturn($this->logoutRequestStub);
261248
$this->uiLocalesResolverStub->method('resolve')->willReturn(null);
262249

263-
$this->sspBridgeLocaleLanguageMock->expects($this->never())
264-
->method('setLanguageCookie');
250+
$this->assertNull($this->captureRenderedTemplateLanguage());
251+
}
265252

266-
$this->mock()->__invoke($this->serverRequestStub);
253+
/**
254+
* Invoke the controller with a TemplateFactory mock and return the language passed to the rendered
255+
* logout template.
256+
*
257+
* @throws \Throwable
258+
* @throws \SimpleSAML\Error\BadRequest
259+
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException
260+
*/
261+
private function captureRenderedTemplateLanguage(): ?string
262+
{
263+
$templateStub = $this->createStub(Template::class);
264+
$capturedLanguage = null;
265+
266+
$templateFactoryMock = $this->createMock(TemplateFactory::class);
267+
$templateFactoryMock->method('build')->willReturnCallback(
268+
function (
269+
string $templateName,
270+
array $data = [],
271+
?string $activeHrefPath = null,
272+
?bool $includeDefaultMenuItems = null,
273+
?bool $showMenu = null,
274+
?bool $showModuleName = null,
275+
?bool $showSubPageTitle = null,
276+
?string $language = null,
277+
) use (&$capturedLanguage, $templateStub): Template {
278+
$capturedLanguage = $language;
279+
return $templateStub;
280+
},
281+
);
282+
283+
$this->mock($templateFactoryMock)->__invoke($this->serverRequestStub);
284+
285+
return $capturedLanguage;
267286
}
268287
}

0 commit comments

Comments
 (0)