Repository navigation
Support a caller-supplied redirect on /reset-remember-wayf - #2097
Conversation
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
3081177 to
a362211
Compare
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
924a15c to
d64ba78
Compare
johanib
left a comment
There was a problem hiding this comment.
Looks good.
I think AI still found a valid looking case that seems to reproduce.
Seems good to close that hole and apply the basic sanitization.
a362211 to
0f9a300
Compare
d64ba78 to
51cb8a0
Compare
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
0f9a300 to
a7aacbc
Compare
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
51cb8a0 to
357be6c
Compare
|
Thanks, the feedback is addressed. One non-blocking suggestion: since we require PHP 8.5, we could use the native WHATWG URL parser ( private function isAllowedRedirect(string $redirect): ?string
{
$url = \Uri\WhatWg\Url::parse($redirect);
if ($url === null
|| $url->getScheme() !== 'https'
|| $url->getUsername() !== null
|| $url->getPassword() !== null
|| !in_array($url->getAsciiHost(), $this->allowedRedirectHosts, true)
) {
return null;
}
return $url->toAsciiString();
}Quick check against the earlier cases:
Returning I didn't find a Symfony component for this. The Validator Fine to merge as is. Your call whether to switch. |
johanib
left a comment
There was a problem hiding this comment.
2 small issues, if fixed, good to merge! 👍
a7aacbc to
1e00df1
Compare
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
# If applied, this commit will... Reword the CHANGELOG entry for the `redirect` query parameter so it explains the purpose of the parameter rather than leading with its https-only validation detail. # Why is this change needed? Prior to this change, the entry opened with the fact that only https URLs with an allowed host are accepted, without saying what the parameter is actually for. A system operator reading the CHANGELOG had no indication of why they might want to configure wayf.reset_choice_allowed_redirect_hosts. # How does it address the issue? This change leads with the purpose: callers such as Profile can send the user back to the page they came from after clearing their remembered per-SP WAYF choices, instead of always landing on the operator-configured wayf.reset_choice_per_idp_redirect. The validation rule and the wayfReset query parameter are kept as supporting detail. # Provide links to any relevant tickets, articles or other resources Addresses review comment on PR #2097.
ff0404b to
cb5b277
Compare
Let /reset-remember-wayf accept an optional `redirect` query parameter so callers such as Profile can send the user back to the page they came from once their remembered per-SP WAYF choices are cleared, instead of always sending everyone to the single operator-configured URL from #2085. Every redirect (whether the caller-supplied target or the configured fallback) now also carries a `wayfReset=removed|none` query parameter, so the caller can tell whether a cookie actually existed and was cleared. - New config wayf.reset_choice_allowed_redirect_hosts is a host allowlist (defaulting to profile.dev.openconext.local in dev/CI): the `redirect` parameter is only honoured when it parses to an http(s) URL whose host appears in this list, otherwise the endpoint silently falls back to the existing wayf.reset_choice_per_idp_redirect target. This mirrors the "parse and check an allowed value" shape of the existing AllowedSchemeValidator rather than introducing a new general-purpose URL validation abstraction for a single call site. - Resolving the redirect target and appending the wayfReset signal are both extracted into small private methods so the __invoke method reads as a straight sequence: clear/log the cookie if present, resolve where to send the user, tell them what happened. - The entry-count/log-message behaviour from #2085 is unchanged: a present-but-unparseable cookie is still treated the same as a valid one for logging and for the wayfReset signal (both are cleared and reported), since from the caller's perspective the cookie no longer exists either way. - Existing unit/functional tests were updated for the new `?wayfReset=` suffix on every redirect, and new cases cover an allowed redirect host, a redirect with its own query string, a disallowed host, a malformed URL, and a disallowed scheme. Needed by OpenConext/OpenConext-profile#345, which links back to Profile's "my personal data" page after resetting a user's remembered WAYF choices.
# If applied, this commit will Fix redirects whose target URL contains a fragment so wayfReset actually reaches the destination app as a real query parameter. # Why is this change needed? Prior to this change, appendQueryParameter() always concatenated the wayfReset parameter onto the end of the URL string. For a target URL with a fragment, that put the query parameter after the '#', where it is treated as part of the fragment by browsers and never seen as a real query parameter. # How does it address the issue? This change splits off any fragment before appending the query parameter, then re-appends the fragment at the very end, producing the conventional ...?wayfReset=...#fragment shape. # Provide links to any relevant tickets, articles or other resources Found during review of PR #2097.
# Why is this change needed? Prior to this change, the redirect query parameter, the allowed redirect hosts and the wayfReset result parameter of /reset-remember-wayf were only described in parameters.yml.dist. Operators need to know about the open redirect protection and integrators about the result parameter. # How does it address the issue? This change extends the reset section in docs/wayf_remember_choice.md and adds an entry to the CHANGELOG.
Only accept https redirects and reject backslashes, control characters, whitespace and userinfo in the redirect query parameter. Prior to this change, the redirect value was only checked with parse_url() against the host allowlist. Browsers normalise a value such as https://evil.example.org\@profile.example.org/ differently from parse_url(), so the host check could pass while the browser navigated to another host (open redirect). Plain http was also accepted without a need for it. This change rejects values containing a backslash, control character or whitespace, values with credentials, and any scheme other than https. Unit tests cover each of these cases and the docs and changelog describe the stricter rules. Review feedback on the redirect parameter pull request.
# If applied, this commit will... Reword the CHANGELOG entry for the `redirect` query parameter so it explains the purpose of the parameter rather than leading with its https-only validation detail. # Why is this change needed? Prior to this change, the entry opened with the fact that only https URLs with an allowed host are accepted, without saying what the parameter is actually for. A system operator reading the CHANGELOG had no indication of why they might want to configure wayf.reset_choice_allowed_redirect_hosts. # How does it address the issue? This change leads with the purpose: callers such as Profile can send the user back to the page they came from after clearing their remembered per-SP WAYF choices, instead of always landing on the operator-configured wayf.reset_choice_per_idp_redirect. The validation rule and the wayfReset query parameter are kept as supporting detail. # Provide links to any relevant tickets, articles or other resources Addresses review comment on PR #2097.
cb5b277 to
c6e65c7
Compare
Prior to this change, the redirect query parameter of /reset-remember-wayf was validated with a regular expression and parse_url(). That meant anticipating every difference between parse_url() and the way browsers interpret a URL. This change parses the value with Uri\WhatWg\Url, the same specification browsers follow, so the host that is checked is the host the browser navigates to. The validated URL is returned in its normalised ASCII form and used as the redirect target. Inputs a browser would also accept, such as a leading space or a tab, are now accepted and normalised instead of rejected.
johanib
left a comment
There was a problem hiding this comment.
thanks, looks good now with the native php 8.5 url validation
* Add link to reset remembered WAYF login-method choices
Add a "Login method choices" section to the bottom of the "my personal
data" page that lets a user reset every per-application login-method
choice they've made in the WAYF, behind a confirmation popup. Confirming
sends the user to EngineBlock's /reset-remember-wayf endpoint (#2085),
which clears the cookie and redirects back here.
- WayfResetLinkBuilder is a small, pure service that appends a `redirect`
query parameter (the absolute URL of this same page) to the configured
wayf_reset_url. It's factored out of MyProfileController and unit
tested in isolation because this repo has no controller/functional
test infrastructure yet, and the interesting logic (building the
link) doesn't need a kernel to test.
- New wayf_reset_url parameter follows the existing pattern of simple
string config values in parameters.yaml(.dist), pointing at
EngineBlock's reset endpoint.
- The new section reuses the shared modal.html.twig confirmation
component (the same one used for deleting a connection), wrapped in a
single-iteration `for` loop: the helper unconditionally derives its
element ids from `loop.index`, which only exists inside a loop, and
its one existing caller is always used from within a real loop over
services.
- SCSS mirrors the structure/naming of the existing "Download" section
on the SURFconext dashboard (mySurfConext__download*), including its
IE11 fallback rules, since both are a text explanation next to a
single button.
- New translation keys were added under the existing
profile.my_profile key in all three locales (en/nl/pt). Since exact
copy from design isn't available, the wording is a best-effort draft
that should be reviewed against the actual mockups; the confirm
button text ("Reset all") is the one piece of copy specified by the
ticket itself.
This depends on OpenConext/OpenConext-engineblock#2097 adding `redirect`
support to /reset-remember-wayf; without it the endpoint still works but
always redirects to its own configured default instead of back here.
Refs: #345
* Require CSRF protection for the WAYF-reset action
# If applied, this commit will
Stop the 'reset login method choices' action from being triggerable
by a plain, unauthenticated cross-site GET request.
# Why is this change needed?
Prior to this change, the confirm button in the reset modal was a
plain <a href> pointing directly at EngineBlock's cross-origin
/reset-remember-wayf endpoint. The confirmation modal was a
client-side-only gate; the underlying action was a GET with no CSRF
token, so it could be triggered by cross-site navigation or an
auto-submitting link, silently clearing a user's remembered WAYF
choices without their intent.
# How does it address the issue?
This change replaces the link with a same-origin POST form carrying
a Symfony CSRF token. A new route validates the token and only then
redirects the browser on to EngineBlock's existing reset endpoint,
preserving the same redirect-back behavior for legitimate
same-origin submissions.
# Provide links to any relevant tickets, articles or other resources
Found during review of PR #353.
* Fix modal button layout and document wayf_reset_url param
# If applied, this commit will...
Fix the reset-choice confirmation button's layout in the modal and add
changelog guidance for the new required parameter.
# Why is this change needed?
Prior to this change, the modal button used a width:100% rule on the
form wrapper that broke the button's intended layout, and the
CHANGELOG did not mention that the new wayf_reset_url parameter must
be set by deployments.
# How does it address the issue?
This change replaces the width override with display:contents on the
form wrapper so the button retains its natural sizing, and adds
CHANGELOG entries describing the feature and the required parameter.
# Provide links to any relevant tickets, articles or other resources
#353
* Add link to reset remembered WAYF login-method choices
Add a "Login method choices" section to the bottom of the "my personal
data" page that lets a user reset every per-application login-method
choice they've made in the WAYF, behind a confirmation popup. Confirming
sends the user to EngineBlock's /reset-remember-wayf endpoint (#2085),
which clears the cookie and redirects back here.
- WayfResetLinkBuilder is a small, pure service that appends a `redirect`
query parameter (the absolute URL of this same page) to the configured
wayf_reset_url. It's factored out of MyProfileController and unit
tested in isolation because this repo has no controller/functional
test infrastructure yet, and the interesting logic (building the
link) doesn't need a kernel to test.
- New wayf_reset_url parameter follows the existing pattern of simple
string config values in parameters.yaml(.dist), pointing at
EngineBlock's reset endpoint.
- The new section reuses the shared modal.html.twig confirmation
component (the same one used for deleting a connection), wrapped in a
single-iteration `for` loop: the helper unconditionally derives its
element ids from `loop.index`, which only exists inside a loop, and
its one existing caller is always used from within a real loop over
services.
- SCSS mirrors the structure/naming of the existing "Download" section
on the SURFconext dashboard (mySurfConext__download*), including its
IE11 fallback rules, since both are a text explanation next to a
single button.
- New translation keys were added under the existing
profile.my_profile key in all three locales (en/nl/pt). Since exact
copy from design isn't available, the wording is a best-effort draft
that should be reviewed against the actual mockups; the confirm
button text ("Reset all") is the one piece of copy specified by the
ticket itself.
This depends on OpenConext/OpenConext-engineblock#2097 adding `redirect`
support to /reset-remember-wayf; without it the endpoint still works but
always redirects to its own configured default instead of back here.
Refs: #345
* Require CSRF protection for the WAYF-reset action
# If applied, this commit will
Stop the 'reset login method choices' action from being triggerable
by a plain, unauthenticated cross-site GET request.
# Why is this change needed?
Prior to this change, the confirm button in the reset modal was a
plain <a href> pointing directly at EngineBlock's cross-origin
/reset-remember-wayf endpoint. The confirmation modal was a
client-side-only gate; the underlying action was a GET with no CSRF
token, so it could be triggered by cross-site navigation or an
auto-submitting link, silently clearing a user's remembered WAYF
choices without their intent.
# How does it address the issue?
This change replaces the link with a same-origin POST form carrying
a Symfony CSRF token. A new route validates the token and only then
redirects the browser on to EngineBlock's existing reset endpoint,
preserving the same redirect-back behavior for legitimate
same-origin submissions.
# Provide links to any relevant tickets, articles or other resources
Found during review of PR #353.
* Fix modal button layout and document wayf_reset_url param
# If applied, this commit will...
Fix the reset-choice confirmation button's layout in the modal and add
changelog guidance for the new required parameter.
# Why is this change needed?
Prior to this change, the modal button used a width:100% rule on the
form wrapper that broke the button's intended layout, and the
CHANGELOG did not mention that the new wayf_reset_url parameter must
be set by deployments.
# How does it address the issue?
This change replaces the width override with display:contents on the
form wrapper so the button retains its natural sizing, and adds
CHANGELOG entries describing the feature and the required parameter.
# Provide links to any relevant tickets, articles or other resources
#353
* Show feedback modal after resetting WAYF remembered choice
# If applied, this commit will
Show the user a modal confirming the outcome of resetting their
remembered WAYF login-method choice.
# Why is this change needed?
Prior to this change, resetting the "remember my choice" cookie via
the profile page redirected back silently. The user had no feedback
on whether a cookie was actually removed or whether there was none
to begin with.
# How does it address the issue?
This change reads the wayfReset query parameter set by EngineBlock's
reset endpoint (removed or none) through a new WayfResetResultResolver
service, and passes it to the overview template. The template renders
an auto-opening modal, reusing the existing modal helper, with copy
that matches the removed/none outcome. The modal helper is extended
with autoOpen and cleanParam options so the modal shows itself on
load and the query parameter is stripped from the URL afterwards,
and its header id is now derived from the modal's own linkId rather
than loop.index so multiple modals on one page never collide.
# Provide links to any relevant tickets, articles or other resources
#350
What
Extends
GET /reset-remember-wayf(introduced in #2085 / #2094) so itaccepts an optional
redirectquery parameter, and always appends awayfReset=removed|nonequery parameter to whichever redirect target isused. This is a prerequisite for
OpenConext/OpenConext-profile#345,
which wants to link from Profile's "my personal data" page to this
endpoint and land the user back on that same page afterwards, knowing
whether a cookie was actually cleared.
Stacked on #2094
This branches from
wayf-reset-endpoint(#2094, not yet merged), since itdirectly extends
ResetRememberedWayfControlleradded there. The diffshown here is the incremental change only; once #2094 merges to
main,this PR's base should be retargeted to
main.Details
wayf.reset_choice_allowed_redirect_hostsis a hostallowlist (defaulting to
profile.dev.openconext.localin dev/CI). Theredirectparameter is only honoured when it parses to anhttp(s)URLwhose host appears in this list; otherwise the endpoint silently falls
back to the existing
wayf.reset_choice_per_idp_redirecttarget. Thismirrors the "parse and check an allowed value" shape of the existing
AllowedSchemeValidatorrather than introducing a new general-purposeURL validation abstraction for a single call site.
wayfResetsignal areextracted into small private methods, so
__invokereads as a straightsequence: clear/log the cookie if present, resolve where to send the
user, tell them what happened.
present-but-unparseable cookie is still treated the same as a valid one
for logging and for the
wayfResetsignal (both are cleared andreported), since from the caller's perspective the cookie no longer
exists either way.
?wayfReset=suffix on every redirect, and new cases cover an allowed redirect host,
a redirect with its own query string, a disallowed host, a malformed
URL, and a disallowed scheme.
Testing
All run inside the Docker dev container (PHP 8.5):
phpmd/phpcs/phpcs-legacy/docheader: clean.eb4: 244/244.unit: 1008/1008 (1003 baseline + 5 new).functional(
APP_ENV=test): 123/123 (121 baseline + 2 new).integration:105/105.
defaultsuite: 300/300 scenarios, 5557/5557 steps (unchanged —no new scenarios needed, this is pure PHPUnit-covered logic).
Refs: OpenConext/OpenConext-profile#345