Opened 6 years ago
Closed 2 days ago
#51173 closed feature request (fixed)
Add support for /.well-known/change-password
| Reported by: | romainmrhenry | Owned by: | peterwilsoncc |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.2 |
| Component: | Login and Registration | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests |
| Cc: | Focuses: |
Description
[Chrome Feature](https://www.chromestatus.com/feature/6256768407568384)
[Editors Draft](https://wicg.github.io/change-password-url/)
This already landed in Safari
Would it be possible to add support for /.well-known/change-password into WordPress?
This might redirect to wp_login_url()
Attachments (1)
Change History (28)
#3
@
6 years ago
Yes, also read some anecdotes of either hosts or proxies handling all .well-known paths. I personally think this is a bad practice, but unfortunately the [spec](https://www.rfc-editor.org/rfc/rfc8615.html) does not prohibit this.
#4
@
6 years ago
- Keywords needs-unit-tests added
+1 from me. I think it's a really nice addition. I'm not sure redirecting to the login page is the correct approach though. Shouldn't we send the user to wp-admin/profile.php, where the new password field is located? WordPress will take care to redirect the user to the login page with redirect_to parameter set back to wp-admin/profile.php in case the user is not logged in.
#5
@
6 years ago
I though about redirecting to wp-admin/profile.php and still consider it a good option.
wp_login_url() might offer more options to theme builders, especially those with custom user flows (ecommerce).
It might need to be a separate thing altogether with a dedicated filter.
#6
@
6 years ago
This URL is intended to be used when the user tries to change the password, as in a password manager helping to automatically or semi-automatically change the password. I think a redirect to the profile page is fine, because any custom workflows for user profile would redirect the user from profile page to any custom page anyway.
#7
@
6 years ago
Maybe we should also consider to use the autocomplete attributes:
<input type="password" autocomplete="new-password">
or:
<input type="password" autocomplete="current-password">
For the password field.
#10
@
6 years ago
@johnbillion I found this old ticket: https://core.trac.wordpress.org/ticket/49608
Should we re-open it, or should I write a new one with the fokus on autocomplete="current-password"?
#12
@
5 years ago
This is now also supported in 1Password and Chrome.
Would be great if we could move forward with this :)
#13
@
4 years ago
The specification for this has reached First Public Working Draft : https://www.w3.org/TR/2022/WD-change-password-url-20220927/
Who on the WordPress core team can champion this feature?
This ticket was mentioned in PR #12621 on WordPress/wordpress-develop by @hasnainashfaq.
3 months ago
#14
- Keywords has-patch has-unit-tests added; needs-unit-tests needs-patch removed
Fixes https://core.trac.wordpress.org/ticket/51173
## What
Adds a redirect from /.well-known/change-password to wp-admin/profile.php inside the existing wp_redirect_admin_locations() function in canonical.php. This follows the W3C Well-Known Change Password URL spec.
## Why
Browsers (Chrome, Safari, Firefox) and password managers use /.well-known/change-password to automatically navigate users to the password-change page. Without this redirect, WordPress sites return a 404 for this URL, breaking password manager integrations.
## How
- Adds
/.well-known/change-passworddetection towp_redirect_admin_locations(), mirroring the existing pattern for/loginand/adminaliases. - Redirects to
admin_url( 'profile.php' )— the profile page handles unauthenticated users by redirecting them to the login page withredirect_toset back to the profile page. - A
wp_change_password_urlfilter allows sites with custom user flows (membership plugins, WooCommerce accounts) to override the redirect target. - Only fires when pretty permalinks are enabled (same guard as the rest of the function).
## Tests
Adds tests/phpunit/tests/canonical/changePassword.php covering:
- Redirect fires for
/.well-known/change-password - Trailing-slash variant does not redirect (consistent with
untrailingslashitbehavior elsewhere) - No redirect when pretty permalinks are disabled
wp_change_password_urlfilter is respected
#15
@
3 months ago
I've opened a pull request for this: https://github.com/WordPress/wordpress-develop/pull/12621
The implementation adds the redirect inside wp_redirect_admin_locations() in canonical.php, mirroring the existing pattern for /login and /admin. It redirects to admin_url( 'profile.php' ) with a wp_change_password_url filter for custom user flows, and covers subdirectory installs by also matching the root-relative path. Unit tests included.
@masteradhoc commented on PR #12621:
6 weeks ago
#16
@HasnainAshfaq i've come across the PR. could you please check the open copilot feedback and fix/resolve all open comments - if valid? i'd like to check this and see if it could be helpful to push this further.
@hasnainashfaq commented on PR #12621:
5 weeks ago
#17
Thanks for the review @masteradhoc! I've gone through all the open Copilot comments:
@since 7.2.0— applied per your suggestion.- Subdirectory URL matching — removed the
home_url()/site_url()relative variants; per the W3C spec the well-known URL is always at the origin root (/.well-known/change-password), so only that literal path is matched now. - Exception-based redirect interception — already in place from an earlier revision; the
wp_redirectfilter throws aRuntimeExceptioncaught intry/catch/finally, soexitis never reached in tests. assertNotNull()before string assertions — already added.tear_down()restoring only when set — already handled via$request_uri_was_set.- Targeted filter removal — already using specific callback references, not
remove_all_filters(). - Arrow function for filter callback —
fn() => $custom_urldoes not need explicit parameter acceptance. - Trailing slash —
untrailingslashit()strips it before comparison, so/.well-known/change-password/correctly redirects (consistent with the/loginand/adminbehavior); tests updated to reflect this.
#18
@
5 weeks ago
- Milestone Future Release → 7.2
Adding this to the WP 7.2 milestone so this can be picked up and tested.
This ticket was mentioned in Slack in #core by audrasjb. View the logs.
3 days ago
#20
@
3 days ago
- Keywords needs-testing added
As per today's bug scrub:
I think it's a bit late for this to be committed in 7.2, but let's keep it in the milestone until beta 1.
I'm adding needs-testing workflow keyword.
@peterwilsoncc commented on PR #12621:
3 days ago
#21
I've taken the liberty of pushing the following:
- Updating the redirect to use
wp_safe_redirect()with the docblock updated accordingly - Conditionally exiting only if the redirect succeeds
- Removed a bunch of unnecessary tear_down code from the tests: the base test suite resets hooks, URL structure, the $_SERVER and wp_query
- Improved redirect assertions to check the full URL rather than just for
profile.php - Merged in trunk
#22
@
3 days ago
I pushed a few changes to the linked pull request, the source changes were pretty minor:
- use
wp_safe_redirect() - conditionally exit only if the redirect fires
Otherwise it was test suite changes, mostly to remove tear down code duplicating items handled by the base test suite.
The code is testing well in manual testing so I think it's good to commit. I'll drop a note in the #core-test channel to ask some folks to take a look.
This ticket was mentioned in Slack in #core-test by peterwilsoncc. View the logs.
3 days ago
#24
@
3 days ago
Test Report
Patch tested: https://github.com/WordPress/wordpress-develop/pull/12621 (head 5e819b6)
Environment: WordPress 7.2-alpha-64337 (trunk), PHP 8.2.29, SQLite, PHP built-in server, pretty permalinks (/%postname%/)
Before patch: /.well-known/change-password → 404 (bug reproduced)
After patch:
✅ Logged in → 302 to /wp-admin/profile.php
✅ Logged out → profile.php → wp-login.php?redirect_to=profile.php → back to profile.php after login
✅ Trailing slash variant → 302 to profile.php
✅ Plain permalinks → 404 (no redirect, as intended)
✅ No false matches: /.well-known/change-passwordX, /.well-known/foo, uppercase, /blog/.well-known/change-password → 404
✅ No regressions: /login, /admin, /dashboard, normal 404s, normal posts unchanged
✅ wp_change_password_url filter: custom URL respected; ( return_false ) disables redirect
✅ External host blocked by wp_safe_redirect (falls back to /wp-admin/); allowed via allowed_redirect_hosts
✅ No PHP notices/warnings with WP_DEBUG on
Notes:
- PR description says the trailing-slash variant does not redirect; it does (matches the updated tests).
- A query string (/.well-known/change-password?x=1) does not redirect, because REQUEST_URI is compared as-is.
- The filter is applied on every 404 before the path check; checking the path first would avoid that.
#25
@
3 days ago
- Keywords needs-testing removed
Patch Testing Report
Patch tested: https://github.com/WordPress/wordpress-develop/pull/12621 (head 5e819b6)
Environment
- WordPress: trunk (7.2-alpha-63166-src,
7a47d39442) - PHP: 8.2.29 (php-fpm)
- Server: nginx 1.31.6
- Database: MySQL 8.4.8
- Browser: curl for the scripted checks (with and without a logged-in cookie), plus a manual check in the browser
- OS: Windows (WSL2, Docker)
- Theme: Twenty Twenty-Five
- Permalinks:
/%year%/%monthnum%/%postname%/ - MU Plugins:
- fix-docker-loopback (local env helper)
- test-trac-51173.php (test fixture, only for the filter checks, see Support Content)
- Plugins: none activated
Steps taken
Reproduction on clean trunk
- Requested
/.well-known/change-passwordwhile logged out and logged in:404, rendered by WordPress (the theme's "Page not found" page), so the request does reach WordPress through nginx. The trailing-slash,HEADand query-string variants also return404. - ✅ Issue reproduced.
With patch applied
- Applied the PR (applies cleanly on current trunk) and re-ran the same script:
- Logged out:
/.well-known/change-password→302to/wp-admin/profile.php→wp-login.php?redirect_to=…profile.php&reauth=1 - Logged in:
302to/wp-admin/profile.php→200on the profile page /.well-known/change-password/(trailing slash) andHEAD→302toprofile.php- No false matches:
/.well-known/change-passwordX,/.well-known/CHANGE-PASSWORD,/.well-known/foo→404 /.well-known/change-password?x=1→404(see note below)
- Logged out:
wp_change_password_urlfilter:- Internal URL (
home_url( '/my-account/edit-password/' )) →302to that URL - External URL not in
allowed_redirect_hosts→302to/wp-admin/(thewp_safe_redirect()fallback) - External URL added via
allowed_redirect_hosts→302tohttps://example.org/change-password false→ no redirect, normal404
- Internal URL (
- Plain permalinks: no redirect from the patch. Same as trunk:
301to/.well-known/change-password/, which then serves the front page with a200(body classhome blog). comment:24 saw a404here; that is probably because the PHP built-in server handles the path differently. Either way the patch does not redirect, as intended. - No regressions:
/login→wp-login.php,/adminand/dashboard→/wp-admin/, other 404s are unchanged. - PHPUnit
--filter Tests_Canonical_ChangePassword:OK (4 tests, 6 assertions).--group canonical:OK (219 tests, 1155 assertions). PHPCS on both changed files: no errors or warnings.
✅ Patch is solving the problem
My results agree with comment:24, this time on nginx + MySQL instead of the PHP built-in server + SQLite. Notes for review:
- A query string (
?x=1) stops the match, becauseREQUEST_URIis compared as-is (also noted in comment:24). Browsers and password managers request the bare URL, so I don't think this matters in practice. - The filter docblock already says external hosts must be added via
allowed_redirect_hosts. It could also mention that if they aren't, the user is sent to/wp-admin/(thewp_safe_redirect()fallback) rather than getting a 404. - Not covered: multisite, subdirectory installs, and hosts or nginx configs that handle
/.well-known/or dot-paths themselves (for example the commonlocation ~ /\. { deny all; }rule). In those setups the request never reaches WordPress, as @johnbillion pointed out in comment:2.
AI disclosure
- AI assistance: Yes
- Tool(s): Claude Code (Claude Opus 5.5)
- Used for: writing the repro script, running the curl / PHPUnit / PHPCS checks and drafting this report. I also tested the patch manually in the browser, and I reviewed and edited the report before posting.
Support Content
Repro script, run from the wordpress-develop root with the local env running (repro-51173.sh):
#!/bin/bash set -u BASE=${BASE:-http://localhost:8889} JAR=$(mktemp) cli() { npm run --silent env:cli -- "$@" 2>/dev/null | tail -n 1; } cat > src/wp-content/mu-plugins/test-trac-51173.php <<'PHP' <?php // Throwaway helper for #51173 testing. add_filter( 'wp_change_password_url', function ( $url ) { switch ( get_option( 't51173_mode' ) ) { case 'internal': return home_url( '/my-account/edit-password/' ); case 'external': return 'https://example.org/change-password'; case 'false': return false; } return $url; } ); add_filter( 'allowed_redirect_hosts', function ( $hosts ) { return 'external-allowed' === get_option( 't51173_mode' ) ? array_merge( $hosts, array( 'example.org' ) ) : $hosts; } ); add_filter( 'wp_change_password_url', function ( $url ) { return 'external-allowed' === get_option( 't51173_mode' ) ? 'https://example.org/change-password' : $url; }, 20 ); PHP hit() { local label=$1 path=$2; shift 2 printf '%-48s %s\n' "$label" "$(curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}' "$@" "$BASE$path")"; } follow() { local label=$1 path=$2; shift 2 printf '%-48s %s\n' "$label" "$(curl -s -L -o /dev/null -w '%{http_code} at %{url_effective}' "$@" "$BASE$path")"; } mode() { cli option update t51173_mode "$1" >/dev/null; } cli rewrite structure '/%year%/%monthnum%/%postname%/' >/dev/null mode default echo "== Logged out ==" hit 'GET /.well-known/change-password' /.well-known/change-password hit 'GET /.well-known/change-password/' /.well-known/change-password/ hit 'GET /.well-known/change-password?x=1' '/.well-known/change-password?x=1' hit 'GET /.well-known/change-passwordX' /.well-known/change-passwordX hit 'GET /.well-known/CHANGE-PASSWORD' /.well-known/CHANGE-PASSWORD hit 'GET /.well-known/foo' /.well-known/foo hit 'HEAD /.well-known/change-password' /.well-known/change-password -I follow 'follow redirects: final page' /.well-known/change-password echo "== Logged in (admin) ==" curl -s -c "$JAR" -b 'wordpress_test_cookie=WP%20Cookie%20check' -o /dev/null \ -d 'log=admin&pwd=password&testcookie=1&redirect_to=/wp-admin/' "$BASE/wp-login.php" hit 'GET /.well-known/change-password' /.well-known/change-password -b "$JAR" follow 'follow redirects: final page' /.well-known/change-password -b "$JAR" echo "== wp_change_password_url filter ==" for m in internal external external-allowed false; do mode "$m"; hit "mode=$m" /.well-known/change-password done mode default echo "== Regressions ==" hit 'GET /login' /login hit 'GET /admin' /admin hit 'GET /dashboard' /dashboard hit 'GET /no-such-page' /no-such-page echo "== Plain permalinks ==" cli rewrite structure '' >/dev/null hit 'GET /.well-known/change-password' /.well-known/change-password cli rewrite structure '/%year%/%monthnum%/%postname%/' >/dev/null cli option delete t51173_mode >/dev/null rm -f src/wp-content/mu-plugins/test-trac-51173.php "$JAR"
Output with the patch applied:
== Logged out == GET /.well-known/change-password 302 -> http://localhost:8889/wp-admin/profile.php GET /.well-known/change-password/ 302 -> http://localhost:8889/wp-admin/profile.php GET /.well-known/change-password?x=1 404 -> GET /.well-known/change-passwordX 404 -> GET /.well-known/CHANGE-PASSWORD 404 -> GET /.well-known/foo 404 -> HEAD /.well-known/change-password 302 -> http://localhost:8889/wp-admin/profile.php follow redirects: final page 200 at http://localhost:8889/wp-login.php?redirect_to=http%3A%2F%2Flocalhost%3A8889%2Fwp-admin%2Fprofile.php&reauth=1 == Logged in (admin) == GET /.well-known/change-password 302 -> http://localhost:8889/wp-admin/profile.php follow redirects: final page 200 at http://localhost:8889/wp-admin/profile.php == wp_change_password_url filter == mode=internal 302 -> http://localhost:8889/my-account/edit-password/ mode=external 302 -> http://localhost:8889/wp-admin/ mode=external-allowed 302 -> https://example.org/change-password mode=false 404 -> == Regressions == GET /login 302 -> http://localhost:8889/wp-login.php GET /admin 302 -> http://localhost:8889/wp-admin/ GET /dashboard 302 -> http://localhost:8889/wp-admin/ GET /no-such-page 404 -> == Plain permalinks == GET /.well-known/change-password 301 -> http://localhost:8889/.well-known/change-password/
On clean trunk every /.well-known/… line is 404 and the regression and plain-permalink lines are identical.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Note: Some hosts handle routing for the
.well-knownpath internally, meaning that such requests might not ever reach the web server that WordPress is running on. I know for example that SiteGround does this.