Skip to content

Implement per-SP WAYF remember-choice core (#2065) - #2091

Open
kayjoosten wants to merge 1 commit into
mainfrom
wayf-remember-choice-core
Open

kayjoosten wants to merge 1 commit into
mainfrom
wayf-remember-choice-core

Conversation

@kayjoosten

Copy link
Copy Markdown
Contributor

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 rememberchoice cookie feature) and adds a new, disabled-by-default, opt-in per-SP mode.

What this PR adds

  • Configuration: wayf.remember_choice_per_idp (default false), wayf.remember_choice_per_idp_lifetime (default 7776000s / 90 days), wayf.remember_choice_per_idp_max (default 16).
  • Metadata gate: new SP metadata coin coin:wayf_remember_choice (Manage metadata:coin:wayf_remember_choice), defaulting to false, with strict boolean parsing so only true/"1" opt an SP in.
  • RememberedIdpCookie service (src/OpenConext/EngineBlock/Service/Wayf/): a deterministic codec/policy service for the new rememberedidps cookie — 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).
  • Runtime bridge: the cookie service and per-SP feature flag are exposed via DiContainerRuntime (not DiContainer), matching the legacy/modern bridge convention for new dependencies.
  • Read-side bypass (SingleSignOn): when the global wayf.remember_choice, the new per-SP flag, and the SP's metadata coin are all enabled (and the request is not ForceAuthn/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 global rememberchoice cookie path is now only used when per-SP mode is disabled.
  • Write-side persistence (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.
  • Minimal frontend signal: a hidden rememberChoice field 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.
  • Cookie attributes: the new cookie is 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 (both src and legacy library rulesets), docheader: clean
  • PHPUnit eb4: 244/244 passed
  • PHPUnit unit: 998/998 passed
  • PHPUnit integration: 105/105 passed
  • PHPUnit functional (APP_ENV=test): 119/119 passed
  • Behat default suite: 300/300 scenarios, 5557/5557 steps passed
  • lint:twig theme/: 114/114 valid
  • theme && yarn lint: passed

New 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 helpers
  • WayfScriptConfigRenderTest — 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 decision
  • Updated DiContainerRuntimeTest, WayfRendererTest, WayfTest, PushMetadataAssemblerTest, ServiceProviderTest, BindingsTest

Behavior preserved

  • One-candidate-IdP shortcut and SSO-notification shortcut remain first, unconditionally
  • Existing global rememberchoice behavior is byte-for-byte unchanged when wayf.remember_choice_per_idp is false
  • An SP without the new metadata coin never uses the new per-SP cookie and never silently falls back to the old global cookie once per-SP mode is globally enabled

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant