Implement per-SP WAYF remember-choice core (#2065) - #2091
Open
kayjoosten wants to merge 1 commit into
Open
kayjoosten wants to merge 1 commit into
kayjoosten wants to merge 1 commit into
Conversation
Introduce the backend core for remembering a user's chosen IdP on a
per-SP basis, gated behind two independent opt-ins: an EngineBlock-wide
config flag and the SP's own Manage metadata coin. This replaces, for
opted-in SPs, the existing single global "remember choice" cookie with
a signed-per-entry cookie keyed by SP entityId, so different SPs can
have independent (and independently expiring) remembered IdPs.
Only true/"1" values for the SP metadata coin enable the feature for
that SP; any other value (including "false" or "-1") leaves it
disabled, so a malformed Manage value can never silently turn on
choice-remembering behaviour.
- Coins/ServiceProvider/PushMetadataAssembler gain the
wayf_remember_choice coin (metadata:coin:wayf_remember_choice),
defaulting to false and parsed with strict true-only semantics.
- RememberedIdpCookie encodes/decodes the "rememberedidps" cookie
(deflate + base64 JSON, one {idp, expires} entry per SP entityId),
validates entry shape defensively, drops expired entries on every
read, and prunes down to a configurable maximum (keeping the
highest-expiry entries) only when a new entry is added.
- DiContainerRuntime/EngineBlockBootstrapper bridge the new cookie
service and the per-SP feature flag into the legacy Corto layer.
- SingleSignOn bypasses WAYF using a remembered IdP when the SP has
opted in and the choice is still valid for one of the candidate
IdPs, preserving the existing zero/one-candidate and SSO-notification
shortcuts; the legacy global "rememberchoice" cookie path is now
gated off for SPs using the new per-SP mode instead of double-firing.
- ContinueToIdp persists a checked "remember this choice" selection
into the cookie and normalizes/prunes it on every request regardless
of whether this SP is enrolled, so stale entries age out naturally.
- The WAYF view model, Twig templates and rememberChoice.js are wired
so the per-SP checkbox submits a hidden form field instead of writing
a client-side cookie, leaving the legacy global-cookie behaviour
untouched for SPs that haven't opted in.
Refs: #2065
kayjoosten
force-pushed
the
wayf-remember-choice-core
branch
from
September 15, 2026 08:03
bc590c8 to
5ab851e
Compare
This was referenced Sep 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the EngineBlock backend core for per-SP WAYF remembered IdP choices, the first vertical slice of #2065 (part of epic #1956 — "Remember my Choice for this Service" feature in WAYF).
This PR is intentionally scoped to a safe, independently testable backend slice. It preserves all existing behavior (one-IdP shortcut, SSO-notification shortcut, and the existing global
rememberchoicecookie feature) and adds a new, disabled-by-default, opt-in per-SP mode.What this PR adds
wayf.remember_choice_per_idp(defaultfalse),wayf.remember_choice_per_idp_lifetime(default7776000s / 90 days),wayf.remember_choice_per_idp_max(default16).coin:wayf_remember_choice(Managemetadata:coin:wayf_remember_choice), defaulting tofalse, with strict boolean parsing so onlytrue/"1"opt an SP in.RememberedIdpCookieservice (src/OpenConext/EngineBlock/Service/Wayf/): a deterministic codec/policy service for the newrememberedidpscookie — JSON entries keyed by SP entity ID, deflated + base64 encoded, with strict validation (invalid data is always treated as absent and cleared, never used for IdP selection), expiry cleanup on every read, and over-limit pruning only when a new choice is written (never during passive reads).DiContainerRuntime(notDiContainer), matching the legacy/modern bridge convention for new dependencies.SingleSignOn): when the globalwayf.remember_choice, the new per-SP flag, and the SP's metadata coin are all enabled (and the request is notForceAuthn/debug), a valid remembered IdP that is still a candidate IdP skips the WAYF automatically. The existing one-IdP shortcut and SSO-notification shortcut remain unconditionally first. The legacy globalrememberchoicecookie path is now only used when per-SP mode is disabled.ContinueToIdp): when a user has explicitly checked "remember my choice" and all three per-SP gates are enabled, the(SP, IdP)pair is persisted in the cookie with a 90-day expiry. Cookie hygiene (invalid/expired cleanup) always runs regardless of whether the checkbox was checked.rememberChoicefield is added to the WAYF's per-IdP form. In per-SP mode, checking the existing checkbox sets this field instead of creating the legacy client-side cookie; the checkbox itself is visually unchanged.Secure,HttpOnly,SameSite=None, scoped to the EngineBlock host only (not the wildcard locale-cookie domain), with the configured lifetime.What this PR does not include (tracked as follow-up work)
Recommended merge order after this PR: #2085 → #2066 → Profile #345/#350 → #2084.
Testing
All run inside the project's PHP 8.5 Docker container as
www-data:phpmd,phpcs(bothsrcand legacylibraryrulesets),docheader: cleaneb4: 244/244 passedunit: 998/998 passedintegration: 105/105 passedfunctional(APP_ENV=test): 119/119 passeddefaultsuite: 300/300 scenarios, 5557/5557 steps passedlint:twig theme/: 114/114 validtheme && yarn lint: passedNew focused test coverage includes:
RememberedIdpCookieTest— codec round-trip, all invalid-input cases, expiry cleanup, pruning-only-on-add, candidate-safe lookup, cookie write/clear, and the shared cookie-hygiene/gating helpersWayfScriptConfigRenderTest— a real-Twig-render regression test ensuring the per-SP flag actually reaches the rendered WAYF page (this caught and fixed a template-context bug during development)ContinueToIdpTest— write-gate coverage for the persistence decisionDiContainerRuntimeTest,WayfRendererTest,WayfTest,PushMetadataAssemblerTest,ServiceProviderTest,BindingsTestBehavior preserved
rememberchoicebehavior is byte-for-byte unchanged whenwayf.remember_choice_per_idpisfalse