Skip to content
Draft
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
14 changes: 14 additions & 0 deletions docs/6-oidc-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ per-field metadata policy (honored / validated / rejected) is documented in
future major version may switch the getters to fall back to the spec defaults.)
DCR is also opt-in (disabled by default), so unless you enable it, nothing changes
for your deployment.
- Support for the `ui_locales` parameter on the authorization and end session
endpoints (previously ignored). The parameter carries the End-User's preferred
UI languages as a space-separated list of BCP47 language tags, ordered by
preference. The most preferred requested language which is also available in
SimpleSAMLphp (per the `language.available` config option) is applied using the
standard SimpleSAMLphp language cookie — the same mechanism as when the user
picks a language on any SimpleSAMLphp page — so subsequent screens shown during
the flow (login page, consent, logout page...) are rendered in the requested
language. Matching includes a fallback to the primary language subtag (for
example, requested `fr-CA` matches available `fr`). Per specification this is
best-effort: if none of the requested languages are available, the parameter is
ignored without raising an error. The available languages are also advertised
in the OP discovery metadata via the `ui_locales_supported` claim (as BCP47
language tags).
- Logging has been improved for authentication flows. It should now be easier
to find information about what went wrong by looking at the relevant log entries.

Expand Down
1 change: 1 addition & 0 deletions routing/services/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ services:
SimpleSAML\Module\oidc\Utils\Routes: ~
SimpleSAML\Module\oidc\Utils\RequestParamsResolver: ~
SimpleSAML\Module\oidc\Utils\UserIdentifierResolver: ~
SimpleSAML\Module\oidc\Utils\UiLocalesResolver: ~
SimpleSAML\Module\oidc\Utils\ClassInstanceBuilder: ~
SimpleSAML\Module\oidc\Utils\JwksResolver: ~
SimpleSAML\Module\oidc\Utils\AuthenticatedOAuth2ClientResolver: ~
Expand Down
7 changes: 7 additions & 0 deletions src/Bridges/SspBridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace SimpleSAML\Module\oidc\Bridges;

use SimpleSAML\Module\oidc\Bridges\SspBridge\Auth;
use SimpleSAML\Module\oidc\Bridges\SspBridge\Locale;
use SimpleSAML\Module\oidc\Bridges\SspBridge\Module;
use SimpleSAML\Module\oidc\Bridges\SspBridge\Utils;

Expand All @@ -17,6 +18,7 @@ class SspBridge
protected static ?Auth $auth = null;
protected static ?Utils $utils = null;
protected static ?Module $module = null;
protected static ?Locale $locale = null;

public function utils(): Utils
{
Expand All @@ -32,4 +34,9 @@ public function auth(): Auth
{
return self::$auth ??= new Auth();
}

public function locale(): Locale
{
return self::$locale ??= new Locale();
}
}
17 changes: 17 additions & 0 deletions src/Bridges/SspBridge/Locale.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\oidc\Bridges\SspBridge;

use SimpleSAML\Module\oidc\Bridges\SspBridge\Locale\Language;

class Locale
{
protected static ?Language $language = null;

public function language(): Language
{
return self::$language ??= new Language();
}
}
15 changes: 15 additions & 0 deletions src/Bridges/SspBridge/Locale/Language.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\oidc\Bridges\SspBridge\Locale;

use SimpleSAML\Locale\Language as SspLanguage;

class Language
{
public function setLanguageCookie(string $language): void
{
SspLanguage::setLanguageCookie($language);
}
}
34 changes: 34 additions & 0 deletions src/Controllers/AuthorizationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,20 @@
namespace SimpleSAML\Module\oidc\Controllers;

use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\RequestTypes\AuthorizationRequestInterface as OAuth2AuthorizationRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use SimpleSAML\Auth\ProcessingChain;
use SimpleSAML\Module\oidc\Bridges\PsrHttpBridge;
use SimpleSAML\Module\oidc\Bridges\SspBridge;
use SimpleSAML\Module\oidc\ModuleConfig;
use SimpleSAML\Module\oidc\Server\AuthorizationServer;
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
use SimpleSAML\Module\oidc\Server\RequestTypes\AuthorizationRequest;
use SimpleSAML\Module\oidc\Services\AuthenticationService;
use SimpleSAML\Module\oidc\Services\ErrorResponder;
use SimpleSAML\Module\oidc\Services\LoggerService;
use SimpleSAML\Module\oidc\Utils\UiLocalesResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

Expand All @@ -40,6 +43,8 @@ public function __construct(
private readonly LoggerService $loggerService,
private readonly PsrHttpBridge $psrHttpBridge,
private readonly ErrorResponder $errorResponder,
private readonly UiLocalesResolver $uiLocalesResolver,
private readonly SspBridge $sspBridge,
) {
}

Expand All @@ -64,6 +69,7 @@ public function __invoke(ServerRequestInterface $request): ResponseInterface
if (!isset($queryParameters[ProcessingChain::AUTHPARAM])) {
$this->loggerService->debug('AuthorizationController::invoke: No AuthProcId query param.');
$authorizationRequest = $this->authorizationServer->validateAuthorizationRequest($request);
$this->setUiLanguage($authorizationRequest);
$state = $this->authenticationService->processRequest($request, $authorizationRequest);
// processState will trigger a redirect
}
Expand Down Expand Up @@ -123,6 +129,34 @@ public function authorization(Request $request): Response
}
}

/**
* Set the UI language for the current user agent based on the ui_locales authorization request parameter,
* if any of the requested languages are available in SimpleSAMLphp. This is done using the standard
* SimpleSAMLphp language cookie (same mechanism as when the user picks a language on any SimpleSAMLphp
* page), so subsequent screens shown during the authentication flow (login page, consent...) are
* rendered in the requested language. Per specification this is best-effort, so no error is raised
* if none of the requested languages are available.
*/
protected function setUiLanguage(OAuth2AuthorizationRequestInterface $authorizationRequest): void
{
if (!$authorizationRequest instanceof AuthorizationRequest) {
return;
}

$language = $this->uiLocalesResolver->resolve($authorizationRequest->getUiLocales());

if ($language === null) {
return;
}

$this->loggerService->debug(
'AuthorizationController: setting UI language based on ui_locales parameter.',
['uiLocales' => $authorizationRequest->getUiLocales(), 'language' => $language],
);

$this->sspBridge->locale()->language()->setLanguageCookie($language);
}

/**
* Validate authorization request after the authn has been performed. For example, check if the
* ACR claim has been requested and that authn performed satisfies it.
Expand Down
42 changes: 39 additions & 3 deletions src/Controllers/EndSessionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use League\OAuth2\Server\Exception\OAuthServerException;
use Psr\Http\Message\ServerRequestInterface;
use SimpleSAML\Module\oidc\Bridges\PsrHttpBridge;
use SimpleSAML\Module\oidc\Bridges\SspBridge;
use SimpleSAML\Module\oidc\Factories\TemplateFactory;
use SimpleSAML\Module\oidc\Server\AuthorizationServer;
use SimpleSAML\Module\oidc\Server\LogoutHandlers\BackChannelLogoutHandler;
Expand All @@ -15,6 +16,7 @@
use SimpleSAML\Module\oidc\Services\LoggerService;
use SimpleSAML\Module\oidc\Services\SessionService;
use SimpleSAML\Module\oidc\Stores\Session\LogoutTicketStoreBuilder;
use SimpleSAML\Module\oidc\Utils\UiLocalesResolver;
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
use SimpleSAML\Session;
use Symfony\Component\HttpFoundation\RedirectResponse;
Expand All @@ -32,6 +34,8 @@ public function __construct(
protected TemplateFactory $templateFactory,
protected PsrHttpBridge $psrHttpBridge,
protected ErrorResponder $errorResponder,
protected UiLocalesResolver $uiLocalesResolver,
protected SspBridge $sspBridge,
) {
}

Expand All @@ -51,6 +55,8 @@ public function __invoke(ServerRequestInterface $request): Response

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

$uiLanguage = $this->setUiLanguage($logoutRequest);

// Set indication that the logout is initiated using OIDC protocol. This will be checked in the
// logoutHandler() method.
$this->sessionService->setIsOidcInitiatedLogout(true);
Expand Down Expand Up @@ -134,7 +140,33 @@ public function __invoke(ServerRequestInterface $request): Response
// run for other logout initiated actions, like (currently) re-authentication...
$this->sessionService->setIsOidcInitiatedLogout(false);

return $this->resolveResponse($logoutRequest, $wasLogoutActionCalled);
return $this->resolveResponse($logoutRequest, $wasLogoutActionCalled, $uiLanguage);
}

/**
* Set the UI language for the current user agent based on the ui_locales logout request parameter, if any of
* the requested languages are available in SimpleSAMLphp. This is done using the standard SimpleSAMLphp
* language cookie (same mechanism as when the user picks a language on any SimpleSAMLphp page). Per
* specification this is best-effort, so no error is raised if none of the requested languages are
* available. Returns the resolved language, so it can also be applied when rendering the logout
* page in the current request (the language cookie only affects subsequent requests).
*/
protected function setUiLanguage(LogoutRequest $logoutRequest): ?string
{
$language = $this->uiLocalesResolver->resolve($logoutRequest->getUiLocales());

if ($language === null) {
return null;
}

$this->loggerService->debug(
'EndSessionController: setting UI language based on ui_locales parameter.',
['uiLocales' => $logoutRequest->getUiLocales(), 'language' => $language],
);

$this->sspBridge->locale()->language()->setLanguageCookie($language);

return $language;
}

public function endSession(Request $request): Response
Expand Down Expand Up @@ -211,8 +243,11 @@ public static function logoutHandler(): void
/**
* @throws \SimpleSAML\Error\ConfigurationError
*/
protected function resolveResponse(LogoutRequest $logoutRequest, bool $wasLogoutActionCalled): Response
{
protected function resolveResponse(
LogoutRequest $logoutRequest,
bool $wasLogoutActionCalled,
?string $uiLanguage = null,
): Response {
if (($postLogoutRedirectUri = $logoutRequest->getPostLogoutRedirectUri()) !== null) {
$this->loggerService->debug(
'Logout request includes post-logout redirect URI: ' . $postLogoutRedirectUri,
Expand Down Expand Up @@ -248,6 +283,7 @@ protected function resolveResponse(LogoutRequest $logoutRequest, bool $wasLogout
showMenu: false,
showModuleName: false,
showSubPageTitle: false,
language: $uiLanguage,
);
}
}
7 changes: 7 additions & 0 deletions src/Factories/TemplateFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,16 @@ public function build(
?bool $showMenu = null,
?bool $showModuleName = null,
?bool $showSubPageTitle = null,
?string $language = null,
): Template {
$template = new Template($this->sspConfiguration, $templateName);

if ($language !== null) {
// Render this template in the given language. Note that the language cookie is intentionally not
// set here (callers can do that themselves, for example, using the SSP bridge).
$template->getTranslator()->getLanguage()->setLanguage($language, false);
}

$includeDefaultMenuItems ??= $this->includeDefaultMenuItems;
$showMenu ??= $this->showMenu;
$showModuleName ??= $this->showModuleName;
Expand Down
1 change: 0 additions & 1 deletion src/Server/AuthorizationServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ public function validateLogoutRequest(ServerRequestInterface $request): LogoutRe
$idTokenHint = $resultBag->getOrFail(IdTokenHintRule::class)->getValue();
$postLogoutRedirectUri = $resultBag->getOrFail(PostLogoutRedirectUriRule::class)->getValue();
$state = $resultBag->getOrFail(StateRule::class)->getValue();
/** @var string|null $uiLocales */
$uiLocales = $resultBag->getOrFail(UiLocalesRule::class)->getValue();

return new LogoutRequest($idTokenHint, $postLogoutRedirectUri, $state, $uiLocales);
Expand Down
6 changes: 6 additions & 0 deletions src/Server/Grants/AuthCodeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeOfflineAccessRule;
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeRule;
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\StateRule;
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\UiLocalesRule;
use SimpleSAML\Module\oidc\Server\RequestTypes\AuthorizationRequest;
use SimpleSAML\Module\oidc\Server\ResponseModes\QueryResponseMode;
use SimpleSAML\Module\oidc\Server\ResponseTypes\Interfaces\AcrResponseTypeInterface;
Expand Down Expand Up @@ -867,6 +868,7 @@ public function validateAuthorizationRequestWithRequestRules(
CodeChallengeMethodRule::class,
IssuerStateRule::class,
AuthorizationDetailsRule::class,
UiLocalesRule::class,
];

// Since we have already validated redirect_uri, and we have state, make it available for other checkers.
Expand Down Expand Up @@ -984,6 +986,10 @@ public function validateAuthorizationRequestWithRequestRules(
$this->loggerService->debug('AuthCodeGrant: ACR values: ', ['acrValues' => $acrValues]);
$authorizationRequest->setRequestedAcrValues($acrValues);

$uiLocales = $resultBag->getOrFail(UiLocalesRule::class)->getValue();
$this->loggerService->debug('AuthCodeGrant: UI locales: ', ['uiLocales' => $uiLocales]);
$authorizationRequest->setUiLocales($uiLocales);


$authorizationRequest->setIsVciRequest($isVciAuthorizationCodeRequest);
$flowType = $isVciAuthorizationCodeRequest ?
Expand Down
5 changes: 5 additions & 0 deletions src/Server/Grants/ImplicitGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ResponseTypeRule;
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ScopeRule;
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\StateRule;
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\UiLocalesRule;
use SimpleSAML\Module\oidc\Server\RequestTypes\AuthorizationRequest;
use SimpleSAML\Module\oidc\Server\ResponseModes\FragmentResponseMode;
use SimpleSAML\Module\oidc\Services\IdTokenBuilder;
Expand Down Expand Up @@ -133,6 +134,7 @@ public function validateAuthorizationRequestWithRequestRules(
RequiredNonceRule::class,
RequestedClaimsRule::class,
AcrValuesRule::class,
UiLocalesRule::class,
];

$this->requestRulesManager->predefineResultBag($resultBag);
Expand Down Expand Up @@ -189,6 +191,9 @@ public function validateAuthorizationRequestWithRequestRules(
$acrValues = $resultBag->getOrFail(AcrValuesRule::class)->getValue();
$authorizationRequest->setRequestedAcrValues($acrValues);

$uiLocales = $resultBag->getOrFail(UiLocalesRule::class)->getValue();
$authorizationRequest->setUiLocales($uiLocales);

$responseMode = $resultBag->getOrFail(ResponseModeRule::class)->getValue();
$authorizationRequest->setResponseMode($responseMode);

Expand Down
4 changes: 2 additions & 2 deletions src/Server/RequestRules/Rules/UiLocalesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use SimpleSAML\OpenID\Codebooks\ParamsEnum;

/**
* @extends AbstractRule<mixed>
* @extends AbstractRule<string|null>
*/
class UiLocalesRule extends AbstractRule
{
Expand All @@ -32,7 +32,7 @@ public function checkRule(
ResponseModeInterface $responseMode = new QueryResponseMode(),
array $allowedServerRequestMethods = [HttpMethodsEnum::GET],
): ?Result {
return new Result($this->getKey(), $this->requestParamsResolver->getBasedOnAllowedMethods(
return new Result($this->getKey(), $this->requestParamsResolver->getAsStringBasedOnAllowedMethods(
ParamsEnum::UiLocales->value,
$request,
$allowedServerRequestMethods,
Expand Down
16 changes: 16 additions & 0 deletions src/Server/RequestTypes/AuthorizationRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ class AuthorizationRequest extends OAuth2AuthorizationRequest
*/
protected ?array $requestedAcrValues = null;

/**
* End-User's preferred UI languages, as requested using the ui_locales parameter (space-separated list of
* BCP47 language tags, ordered by preference).
*/
protected ?string $uiLocales = null;

/**
* ACR used during authn.
*/
Expand Down Expand Up @@ -225,6 +231,16 @@ public function setRequestedAcrValues(?array $requestedAcrValues): void
$this->requestedAcrValues = $requestedAcrValues;
}

public function getUiLocales(): ?string
{
return $this->uiLocales;
}

public function setUiLocales(?string $uiLocales): void
{
$this->uiLocales = $uiLocales;
}

public function getAcr(): ?string
{
return $this->acr;
Expand Down
Loading
Loading