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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ on the EngineBlock wiki.

## Unreleased

Features:

* Added a per-SP "remember my choice" mode for the WAYF, enabled with
`feature_enable_wayf_remember_choice_per_idp` and the SP metadata coin `coin:wayf_remember_choice`. See
`docs/wayf_remember_choice.md`.
* The per-SP mode and the existing global mode (`wayf.remember_choice`) are mutually exclusive. EngineBlock refuses to
build its container (for example during `cache:clear` or `cache:warmup`) when both are `true`. Enable only one.
* The per-SP mode does not require `wayf.remember_choice`. Leave `wayf.remember_choice` set to `false` when enabling it.
* The cookie removal page `/authentication/idp/remove-cookies` is now also available in the per-SP mode and lists the
`rememberedidps` cookie. Before, it was only available when `wayf.remember_choice` was `true`.

Updated:

* Updated Composer and Yarn (theme and Cypress) dependencies within their current version constraints to resolve open Dependabot alerts. (#2121)
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"ext-mbstring": "*",
"ext-openssl": "*",
"ext-pdo": "*",
"ext-zlib": "*",
"beberlei/assert": "^3.3.3",
"composer/package-versions-deprecated": "^1.11",
"doctrine/dbal": "^4.3.3",
Expand Down Expand Up @@ -54,7 +55,6 @@
"twig/twig": "^3.22.0"
},
"require-dev": {
"ext-zlib": "*",
"behat/behat": "^3.26",
"behat/mink": "^1.12",
"behat/mink-browserkit-driver": "^2.2",
Expand Down
7 changes: 3 additions & 4 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions config/packages/engineblock_features.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ parameters:
eb.stepup.send_user_attributes: "%feature_stepup_send_user_attributes%"
eb.feature_enable_sram_interrupt: "%feature_enable_sram_interrupt%"
eb.hide_bookmarkable_url: "%feature_hide_bookmarkable_url%"
eb.feature_enable_wayf_remember_choice_per_idp: "%feature_enable_wayf_remember_choice_per_idp%"
13 changes: 13 additions & 0 deletions config/packages/parameters.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,16 @@ parameters:
wayf.cutoff_point_for_showing_unfiltered_idps: 50

## Allow users to save their selected IdP and then auto-select it on returning visits.
## This is the global mode: one remembered IdP for all SPs, stored in a client-side cookie.
## Mutually exclusive with feature_enable_wayf_remember_choice_per_idp: enabling both fails the
## container build. See docs/wayf_remember_choice.md.
wayf.remember_choice: false
## Lifetime for remembered IdP choices scoped per SP, in seconds (7776000 seconds = 90 days).
wayf.remember_choice_per_idp_lifetime: 7776000
Comment thread
johanib marked this conversation as resolved.
Resolved
## Maximum number of remembered IdP choice entries to retain per SP. Once exceeded, the
## oldest-expiring entries are evicted first; the newly added choice is always kept and no
## error is raised (see RememberedIdpCookie::pruneOverLimit()).
wayf.remember_choice_per_idp_max: 16
Comment thread
johanib marked this conversation as resolved.
Resolved

## Toggle the default IdP quick link banner on the WAYF.
wayf.display_default_idp_banner_on_wayf: true
Expand Down Expand Up @@ -235,6 +244,10 @@ parameters:
feature_stepup_send_user_attributes: false
feature_enable_sram_interrupt: false
feature_hide_bookmarkable_url: false
## Allow SPs to opt in to remembered IdP choices scoped per SP through Manage metadata.
## This is the per-SP mode and replaces the global mode: it does not require wayf.remember_choice.
## Mutually exclusive with wayf.remember_choice: enabling both fails the container build.
feature_enable_wayf_remember_choice_per_idp: false

##########################################################################################
## PROFILE SETTINGS
Expand Down
10 changes: 10 additions & 0 deletions config/services/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
autowire: true
autoconfigure: true
arguments:
$featureConfiguration: '@OpenConext\EngineBlockBundle\Configuration\FeatureConfiguration'
$preferredIdpEntityIds: '%wayf.preferred_idp_entity_ids%'
tags:
- { name: kernel.event_subscriber }
Expand Down Expand Up @@ -173,6 +174,15 @@ services:

OpenConext\EngineBlock\Service\CookieService:

OpenConext\EngineBlock\Service\Wayf\RememberedIdpCookie:
autowire: true
arguments:
$lifetime: '%wayf.remember_choice_per_idp_lifetime%'
$maxEntries: '%wayf.remember_choice_per_idp_max%'
$cookieDomain: ''
$cookiePath: '%cookie.path%'
$secure: '%cookie.secure%'

OpenConext\EngineBlock\Metadata\Factory\Factory\ServiceProviderFactory:
arguments:
- '@engineblock.compat.metadata.definitions'
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
1. [PHP testing][php-testing]
1. [Javscript testing][js-testing]
1. [Logging][logging]
1. [WAYF remember my choice](wayf_remember_choice.md)

[license]: https://github.com/OpenConext/OpenConext-engineblock/tree/master/docs/LICENSE
[release]: https://github.com/OpenConext/OpenConext-engineblock/tree/master/docs/release_procedure.md
Expand Down
5 changes: 3 additions & 2 deletions docs/sso_session_cookie.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ An authentication with the Identity Provider is successful when the SAML respons
The value of the cookie contains an array of the entity ID('s) of the successfully authenticated Identity Providers.
This cookie will be deleted at `/logout` or will expire according to the set `sso_session_cookie_max_age`.
If the SSO Cookie Session feature is enabled it is also possible to manually delete this cookie at
`/authentication/idp/remove-cookies`. Do note however, that the remove cookies endpoint is only available if
`wayf.remember_choice=true`
`/authentication/idp/remove-cookies`. Do note however, that the remove cookies endpoint is only available if one of the
WAYF remember my choice modes is enabled (`wayf.remember_choice` or
`feature_enable_wayf_remember_choice_per_idp`), see [WAYF remember my choice](wayf_remember_choice.md).

## Configuration of SSO Session Cookie in Engineblock

Expand Down
98 changes: 98 additions & 0 deletions docs/wayf_remember_choice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# WAYF remember my choice

EngineBlock can remember which Identity Provider (IdP) a user picked on the WAYF, so that the user skips the WAYF on a
returning visit. There are two ways to do this. They are **mutually exclusive**: you enable one or the other, never
both.

| | Global mode | Per-SP mode |
|---|---|---|
| Parameter | `wayf.remember_choice` | `feature_enable_wayf_remember_choice_per_idp` |
| Scope of the remembered choice | One IdP for all SPs | One IdP per SP |
| SP opt-in required | No, applies to all SPs | Yes, SP metadata coin `coin:wayf_remember_choice` |
| Cookie name | `rememberchoice` | `rememberedidps` |
| Written by | Browser (JavaScript) | EngineBlock (server side) |
| Cookie lifetime | 365 days | `wayf.remember_choice_per_idp_lifetime` (default 90 days) |
| Cookie attributes | Secure, SameSite=None | Secure, HttpOnly, SameSite=None, scoped to the EngineBlock host |

## Choosing a mode

Enable at most one of the following in `config/packages/parameters.yml`:

# Global mode
wayf.remember_choice: true
feature_enable_wayf_remember_choice_per_idp: false

or

# Per-SP mode
wayf.remember_choice: false
feature_enable_wayf_remember_choice_per_idp: true

If both are `false`, the WAYF has no remember my choice checkbox and EngineBlock never skips the WAYF based on an
earlier choice.

### Both modes enabled is an error

If both parameters are `true`, EngineBlock refuses to build its container and fails with:

The WAYF remember-my-choice modes are mutually exclusive: "wayf.remember_choice" and
"feature_enable_wayf_remember_choice_per_idp" cannot both be true. Enable only one of them.

The check runs when the Symfony container is compiled, so a wrong configuration is caught by `cache:clear` or
`cache:warmup` during deployment, before any user is affected. Fix the configuration by setting one of the two
parameters to `false`.

The per-SP mode does not depend on `wayf.remember_choice`. Earlier development versions required both to be `true`;
that combination is now rejected.

## Global mode

With `wayf.remember_choice: true` the WAYF shows a "remember my choice" checkbox. When the user checks it and picks an
IdP, the browser stores the IdP entity ID in the `rememberchoice` cookie. On the next authentication request,
EngineBlock skips the WAYF and sends the user to that IdP if it is a candidate IdP for the requesting SP. The choice is
shared by all SPs.

The remembered IdP is ignored for `ForceAuthn` and debug requests.

The cookie can be removed on `/authentication/idp/remove-cookies`, see [Removing the cookies](#removing-the-cookies).

## Per-SP mode

With `feature_enable_wayf_remember_choice_per_idp: true` the remembered choice is stored per SP. The feature is
opt-in per SP through Manage metadata:

metadata:coin:wayf_remember_choice: true

Only the boolean `true` (or `"1"`) opts an SP in. An SP without the coin never uses the per-SP cookie, and never falls
back to the global cookie.

When a user checks the checkbox and picks an IdP for an opted-in SP, EngineBlock stores the `(SP, IdP)` pair in the
`rememberedidps` cookie. On the next authentication request from that SP, EngineBlock skips the WAYF if the remembered
IdP is still a candidate IdP. The one-IdP shortcut and the SSO notification shortcut always take precedence, and
`ForceAuthn` and debug requests ignore the remembered IdP.

Related parameters:

# Lifetime of a remembered choice, in seconds (7776000 seconds = 90 days)
wayf.remember_choice_per_idp_lifetime: 7776000

# Maximum number of remembered SPs per cookie. The entries that expire soonest are evicted first.
wayf.remember_choice_per_idp_max: 16

The cookie content is validated strictly. Invalid or expired entries are never used to select an IdP and are removed
from the cookie.

The cookie can be removed on `/authentication/idp/remove-cookies`, see [Removing the cookies](#removing-the-cookies).

## Removing the cookies

The page `/authentication/idp/remove-cookies` lets a user inspect and remove the cookies EngineBlock has set,
including `rememberchoice` and `rememberedidps`. The page is available when either mode is enabled, and returns a 404
when both are disabled.

## Switching modes

Cookies written in one mode are not read in the other. After switching, users have to make their choice once more.
Switching from global to per-SP mode requires that you first set `wayf.remember_choice` to `false`, and then enable
`feature_enable_wayf_remember_choice_per_idp` and the `coin:wayf_remember_choice` coin on the SPs that should
use it.
65 changes: 65 additions & 0 deletions library/EngineBlock/Corto/Module/Service/ContinueToIdp.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider;
use OpenConext\EngineBlock\Metadata\Factory\Factory\ServiceProviderFactory;
use OpenConext\EngineBlock\Metadata\X509\KeyPairFactory;
use OpenConext\EngineBlock\Service\Wayf\RememberedIdpCookie;
use Symfony\Component\HttpFoundation\Request;
use Twig\Environment;

Expand Down Expand Up @@ -113,9 +114,73 @@ public function serve($serviceName, Request $httpRequest)
$log->info('Raw HTTP request', array('http_request' => (string) $application->getHttpRequest()));
}

$this->_persistRememberedChoice($sp, $idp, $httpRequest->request->get('rememberChoice'));

$this->_server->sendAuthenticationRequest($request, $selectedIdp);
}

/**
* @param ServiceProvider $sp
* @param mixed $idp
* @param string|null $rememberChoice
*/
private function _persistRememberedChoice(ServiceProvider $sp, $idp, $rememberChoice)
{
$application = EngineBlock_ApplicationSingleton::getInstance();
$container = $application->getDiContainer();
$runtime = $application->getDiContainerRuntime();
$log = $this->_server->getLogger();

$rememberedIdpCookie = $runtime->rememberedIdpCookie;

$entries = $rememberedIdpCookie->loadValidEntriesFromRequest($container->getSymfonyRequest(), $log);

if (!$this->_isRememberChoiceChecked($rememberChoice)) {
return;
}

if (!RememberedIdpCookie::isEnabledForServiceProvider(
$runtime->rememberChoicePerIdp,
$sp
)) {
return;
}

$result = $rememberedIdpCookie->add($entries, $sp->entityId, $idp->entityId);
if (!$rememberedIdpCookie->write($result['entries'], $result['expires'])) {
$log->error(sprintf(
'WAYF-remember-my-choice cookie write failed for SP %s and IdP %s',
$sp->entityId,
$idp->entityId
));

return;
}

$log->info(sprintf(
'WAYF-remember-my-choice set for SP %s and IdP %s and expiry %d',
$sp->entityId,
$idp->entityId,
$result['expires']
));

if ($result['dropped'] > 0) {
$log->info(sprintf(
'WAYF-remember-my-choice %d entities dropped from cookie',
$result['dropped']
));
}
}

/**
* @param string|null $rememberChoice
* @return bool
*/
private function _isRememberChoiceChecked($rememberChoice)
{
return $rememberChoice === '1';
}
Comment thread
johanib marked this conversation as resolved.
Resolved

/**
* @param EngineBlock_Corto_ProxyServer $proxyServer
* @return ServiceProvider
Expand Down
Loading
Loading