Make WordPress Core

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)

canonical.diff​ (476 bytes ) - added by romainmrhenry 6 years ago.

Download all attachments as: .zip

Change History (28)

#1 @SergeyBiryukov
6 years ago

  • Component General → Login and Registration

#2 @johnbillion
6 years ago

  • Version trunk

Note: Some hosts handle routing for the .well-known path 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.

#3 @romainmrhenry
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.

Last edited 6 years ago by romainmrhenry (previous) (diff)

#4 @ayeshrajans
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 @romainmrhenry
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 @ayeshrajans
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 @pfefferle
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.

See: ​https://adactio.com/journal/17794

Version 0, edited 6 years ago by pfefferle (next)

#8 @johnbillion
6 years ago

Great point @pfefferle , can you open a separate ticket for this please?

#10 @pfefferle
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"?

Last edited 6 years ago by pfefferle (previous) (diff)

#11 @swissspidy
5 years ago

  • Keywords needs-patch added
  • Milestone Awaiting Review → Future Release

#12 @romainmrhenry
5 years ago

This is now also supported in 1Password and Chrome.
Would be great if we could move forward with this :)

#13 @romainmrhenry
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-password detection to wp_redirect_admin_locations(), mirroring the existing pattern for /login and /admin aliases.
  • Redirects to admin_url( 'profile.php' ) — the profile page handles unauthenticated users by redirecting them to the login page with redirect_to set back to the profile page.
  • A wp_change_password_url filter 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 untrailingslashit behavior elsewhere)
  • No redirect when pretty permalinks are disabled
  • wp_change_password_url filter is respected

#15 @hasnainashfaq
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_redirect filter throws a RuntimeException caught in try/catch/finally, so exit is 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_url does not need explicit parameter acceptance.
  • Trailing slash — untrailingslashit() strips it before comparison, so /.well-known/change-password/ correctly redirects (consistent with the /login and /admin behavior); tests updated to reflect this.

#18 @masteradhoc
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 @audrasjb
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 @peterwilsoncc
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 @jns141191
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 @sajib1223
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

  1. Requested /.well-known/change-password while 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, HEAD and query-string variants also return 404.
  2. ✅ Issue reproduced.

With patch applied

  1. Applied the PR (applies cleanly on current trunk) and re-ran the same script:
    • Logged out: /.well-known/change-password → 302 to /wp-admin/profile.php → wp-login.php?redirect_to=…profile.php&reauth=1
    • Logged in: 302 to /wp-admin/profile.php → 200 on the profile page
    • /.well-known/change-password/ (trailing slash) and HEAD → 302 to profile.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)
  2. wp_change_password_url filter:
    • Internal URL (home_url( '/my-account/edit-password/' )) → 302 to that URL
    • External URL not in allowed_redirect_hosts → 302 to /wp-admin/ (the wp_safe_redirect() fallback)
    • External URL added via allowed_redirect_hosts → 302 to https://example.org/change-password
    • false → no redirect, normal 404
  3. Plain permalinks: no redirect from the patch. Same as trunk: 301 to /.well-known/change-password/, which then serves the front page with a 200 (body class home blog). comment:24 saw a 404 here; that is probably because the PHP built-in server handles the path differently. Either way the patch does not redirect, as intended.
  4. No regressions: /login → wp-login.php, /admin and /dashboard → /wp-admin/, other 404s are unchanged.
  5. 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, because REQUEST_URI is 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/ (the wp_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 common location ~ /\. { 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.

This ticket was mentioned in ​Slack in #core-test by sajib1223. ​View the logs.


3 days ago

#27 @peterwilsoncc
2 days ago

  • Owner set to peterwilsoncc
  • Resolution → fixed
  • Status new → closed

In 64348:

Login and Registration: Support /.well-known/change-password.

Introduce support for the ​Well-Known URL for Changing Passwords, a standardized URL for tools such as password managers to make change of password forms discoverable.

Per the specification this is only available to WordPress sites installed in the root directory of a domain. The URL example.com/.well-known/change-password will redirect a user to their profile page.

Introduces the filter wp_change_password_url to allow sites using a non-standard location for managing user passwords. If this is hosted on an another host, the host will need to be added to the allow list via the allowed_redirect_hosts filter in order to pass the wp_safe_redirect() validation process.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/12621

Props audrasjb, ayeshrajans, hasnainashfaq, jns141191, johnbillion, masteradhoc, pfefferle, romainmrhenry, sajib1223, sergeybiryukov, swissspidy.
Fixes #51173.

Note: See TracTickets for help on using tickets.