-
Notifications
You must be signed in to change notification settings - Fork 810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Account Protection: Add password validation #41401
Merged
dkmyta
merged 146 commits into
add/account-protection
from
add/packages/account-protection-password-validation
Feb 12, 2025
Merged
Changes from all commits
Commits
Show all changes
146 commits
Select commit
Hold shift + click to select a range
3f56ee7
Add Account Protection toggle to Jetpack security settings
dkmyta 719f950
Import package and run activation/deactivation on module toggle
dkmyta c83c604
changelog
dkmyta ab4f99a
Add Protect Settings page and hook up Account Protection toggle
dkmyta c2e8f1e
changelog
dkmyta b64fdaf
Update changelog
dkmyta 13949a6
Merge branch 'add/jetpack/account-protection-security-settings' into …
dkmyta 3f68536
Register modules on plugin activation
dkmyta 3628b02
Ensure package is initialized on plugin activation
dkmyta 3f90fe4
Make account protection class init static
dkmyta 911e1db
Merge branch 'add/jetpack/account-protection-security-settings' into …
dkmyta 3bfbcb3
Add auth hooks, redirect and a custom login action template
dkmyta 289dbdb
Reorg, add Password_Detection class
dkmyta 7eabdd3
Remove user cxn req and banner
dkmyta 5a1af0b
Do not enabled module by default
dkmyta 3b35efe
Add strict mode option and settings toggle
dkmyta 4ddb644
changelog
dkmyta 2a0e811
Merge branch 'add/jetpack/account-protection-security-settings' into …
dkmyta b62811b
Add strict mode toggle
dkmyta 28f5820
Add strict mode toggle and endpoints
dkmyta da260ea
Rebase
dkmyta b72e93a
Reorg and add kill switch and is supported check
dkmyta 7fad7f9
Add testing infrastructure
dkmyta 39a28d5
Add email handlings, resend AJAX action, and attempt limitations
dkmyta da85a18
Add nonces, checks and template error handling
dkmyta 6a43ec0
Use method over template to avoid lint errors
dkmyta 7557056
Improve render_password_detection_template, update SVG file ext
dkmyta 16845a7
Remove template file and include
dkmyta 5f959f1
Prep for validation endpoints
dkmyta a4ba959
Update classes to be dynamic
dkmyta 992f288
Add constructors
dkmyta 43d4cd0
Reorg user meta methods
dkmyta 3cec891
Add type declarations and hinting
dkmyta c61877b
Simplify method naming
dkmyta 92d0ea6
Rebase, fix conflicts
dkmyta 7634ed2
Use dynamic classes
dkmyta 692db33
Update class dependencies
dkmyta 22d2678
Fix copy
dkmyta 0fd3e41
Revert unrelated changes
dkmyta 805b367
Rebase, fix conflicts
dkmyta 4383b5e
Revert unrelated changes
dkmyta 9a70647
Fix method calls
dkmyta 969102f
Do not activate by default
dkmyta 8356bd4
Fix phan errors
dkmyta dad19c7
Merge branch 'add/jetpack/account-protection-security-settings' into …
dkmyta a5f1467
Rebase, fix conflicts
dkmyta 32f3ef6
Changelog
dkmyta b02d511
Update composer deps
dkmyta 7c255ac
Update lock files, add constructor method
dkmyta cdb0ac8
Fix php warning
dkmyta 19efaea
Merge branch 'add/jetpack/account-protection-security-settings' into …
dkmyta 1ce68b9
Update lock file
dkmyta 7a06508
Changelog
dkmyta ddfa535
Rebase
dkmyta c128cf5
Fix Password_Detection constructor
dkmyta 7a56b48
Changelog
dkmyta bc7aa77
More changelogs
dkmyta b28c8cf
Remove comments
dkmyta 4bb5401
Fix static analysis errors
dkmyta 20dec01
Remove top level phpunit.xml.dist
dkmyta 2bdbf8e
Remove never return type
dkmyta a80c024
Revert tests dir changes in favour of a dedicated task
dkmyta f07e52e
Add tests dir
dkmyta 80d0e92
Reapply default test infrastructure
dkmyta c03d626
Reorg and rename
dkmyta ae3b6b6
Update @package
dkmyta 30f2329
Use never phpdoc return type as per static analysis error
dkmyta 55ccadb
Merge branch 'add/account-protection' into add/jetpack/account-protec…
dkmyta 3fed240
Merge branch 'add/jetpack/account-protection-security-settings' into …
dkmyta 2beaca5
Merge branch 'add/protect/account-protection-settings' into add/packa…
dkmyta aad7ff6
Enable module by default
dkmyta de4fc75
Merge branch 'add/jetpack/account-protection-security-settings' into …
dkmyta 448079b
Enable module by default
dkmyta bc263e0
Merge branch 'add/protect/account-protection-settings' into add/packa…
dkmyta 4b18375
Remove all reference to and functionality of strict mode
dkmyta bbec51a
Rebase, fix conflicts
dkmyta 7d72fd9
Remove unneeded strict mode code, update Protect settings UI
dkmyta 36f0945
Updates/fixes
dkmyta 37e0aa2
Fix import
dkmyta d51016d
Update placeholder content
dkmyta 30b86d5
Revert unrelated changes
dkmyta 6b34d25
Remove missed code
dkmyta 40a6edf
Update reset email to two factor auth email
dkmyta 528ee1d
Updates and improvements
dkmyta 612f655
Reorg
dkmyta 9b2bb3e
Optimizations and reorganizations
dkmyta 664558b
Hook up email service
dkmyta 915504d
Update error handling todos, fix weak password check
dkmyta 87445c2
Test
dkmyta 9ef7e9d
Localize text content
dkmyta 4c794c5
Fix lint warnings/errors
dkmyta 0b493b8
Update todos
dkmyta 82d9ff2
Add error handling, enforce input restrictions
dkmyta b466475
Move main constants back entry file
dkmyta 7f7b57d
Fix package version check
dkmyta fe79de3
Optimize setting error transient
dkmyta 6743841
Add nonce check for resend email action
dkmyta 88eed6e
Fix spacing
dkmyta 4e0be98
Fix resend nonce handling
dkmyta 8f79bab
Merge branch 'add/account-protection' into update/packages/account-pr…
dkmyta 490e50b
Email service fixes
dkmyta d47a220
Fixes, improvements to doc consistency
dkmyta 7e87875
Add remaining password validation
dkmyta 38a2d15
Update weak password check returns
dkmyta 9838e09
Fix phan errors
dkmyta 7d4b46f
Revert prior change
dkmyta 6c52261
Fix meta key
dkmyta c9acdcb
Rebase
dkmyta 06f6008
Add process for add/updating recent pass list
dkmyta 730407a
Send auth code via wpcom only
dkmyta 39cd995
Update method name
dkmyta 022f9ab
Rebase, fix weak password method returns
dkmyta abfe635
Rebase, fix conflicts
dkmyta 56ee7aa
Optimize validation
dkmyta c5e658e
Fix key, remove testing code
dkmyta 9637bd3
Fix docs
dkmyta fd002db
Rebase, fix conflicts
dkmyta 81a2325
Fix tests
dkmyta b173e09
Merge branch 'add/account-protection' into add/packages/account-prote…
dkmyta 61ca3ed
Merge branch 'add/account-protection' into add/packages/account-prote…
dkmyta 8159db7
Merge branch 'add/account-protection' into add/packages/account-prote…
dkmyta 5e96389
Improve matches user data logic
dkmyta c5c8acd
Remove password reset nonce verification code
dkmyta 8f8f934
Updates and fixes
dkmyta 8388caa
Include tests for new validation methods
dkmyta 2c59a84
Include tests for new validation methods
dkmyta 7fb0b5a
Add password manager class tests
dkmyta b651b2b
Remove custom nonce, add core create-user nonce check
dkmyta 16d54a6
Remove todos - always run server side validation
dkmyta c9f5d32
Update constant naming
dkmyta 5559bce
Translate error message
dkmyta b716b6f
Ensure styles are enqueued when viewing the password detection page
dkmyta d296b26
Use global page now and action check to enqueue styles
dkmyta 5515183
Skip recent password checks during create user action
dkmyta a8e6b8d
Additional skips, and comment clarification
dkmyta b5699a6
Revert skips of user specific reset form validation, hook provides ac…
dkmyta 556fc84
Revert unintended additions
dkmyta 6e725cb
Return early if update is irrelevant
dkmyta afb4621
Only verify nonce if pass is set
dkmyta 62d30c2
Skip validation if bypass enabled
dkmyta 6c6eea4
Merge branch 'add/account-protection' into add/packages/account-prote…
dkmyta 55f5617
Merge branch 'add/account-protection' into add/packages/account-prote…
dkmyta 3a2993c
Fix test
dkmyta 7c5733c
Update methods, removes nonce checks, fix tests
dkmyta bd3e654
Fix test
dkmyta 647be75
Remove comment
dkmyta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
This file contains 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
155 changes: 155 additions & 0 deletions
155
projects/packages/account-protection/src/class-password-manager.php
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
<?php | ||
/** | ||
* Class used to define Password Manager. | ||
* | ||
* @package automattic/jetpack-account-protection | ||
*/ | ||
|
||
namespace Automattic\Jetpack\Account_Protection; | ||
|
||
/** | ||
* Class Password_Manager | ||
*/ | ||
class Password_Manager { | ||
/** | ||
* Validaton service instance | ||
* | ||
* @var Validation_Service | ||
*/ | ||
private $validation_service; | ||
|
||
/** | ||
* Validation_Service constructor. | ||
* | ||
* @param ?Validation_Service $validation_service Password manager instance. | ||
*/ | ||
public function __construct( ?Validation_Service $validation_service = null ) { | ||
$this->validation_service = $validation_service ?? new Validation_Service(); | ||
} | ||
|
||
/** | ||
* Validate the profile update. | ||
* | ||
* @param \WP_Error $errors The error object. | ||
* @param bool $update Whether the user is being updated. | ||
* @param \stdClass $user A copy of the new user object. | ||
* | ||
* @return void | ||
*/ | ||
public function validate_profile_update( \WP_Error $errors, bool $update, \stdClass $user ): void { | ||
if ( empty( $user->user_pass ) ) { | ||
return; | ||
} | ||
|
||
// If bypass is enabled, do not validate the password | ||
// phpcs:ignore WordPress.Security.NonceVerification | ||
if ( isset( $_POST['pw_weak'] ) && 'on' === $_POST['pw_weak'] ) { | ||
return; | ||
} | ||
|
||
if ( $update ) { | ||
if ( $this->validation_service->is_current_password( $user->ID, $user->user_pass ) ) { | ||
$errors->add( 'password_error', __( '<strong>Error:</strong> The password was used recently.', 'jetpack-account-protection' ) ); | ||
return; | ||
} | ||
} | ||
|
||
$context = $update ? 'update' : 'create-user'; | ||
$error = $this->validation_service->return_first_validation_error( $user, $user->user_pass, $context ); | ||
|
||
if ( ! empty( $error ) ) { | ||
$errors->add( 'password_error', $error ); | ||
return; | ||
} | ||
} | ||
|
||
/** | ||
* Validate the password reset. | ||
* | ||
* @param \WP_Error $errors The error object. | ||
* @param \WP_User|\WP_Error $user The user object. | ||
* | ||
* @return void | ||
*/ | ||
public function validate_password_reset( \WP_Error $errors, $user ): void { | ||
if ( is_wp_error( $user ) ) { | ||
return; | ||
} | ||
|
||
// phpcs:ignore WordPress.Security.NonceVerification | ||
if ( empty( $_POST['pass1'] ) ) { | ||
return; | ||
} | ||
|
||
// If bypass is enabled, do not validate the password | ||
// phpcs:ignore WordPress.Security.NonceVerification | ||
if ( isset( $_POST['pw_weak'] ) && 'on' === $_POST['pw_weak'] ) { | ||
return; | ||
} | ||
|
||
// phpcs:ignore WordPress.Security.NonceVerification | ||
$password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) ); | ||
if ( $this->validation_service->is_current_password( $user->ID, $password ) ) { | ||
$errors->add( 'password_error', __( '<strong>Error:</strong> The password was used recently.', 'jetpack-account-protection' ) ); | ||
return; | ||
} | ||
|
||
$error = $this->validation_service->return_first_validation_error( $user, $password, 'reset' ); | ||
if ( ! empty( $error ) ) { | ||
$errors->add( 'password_error', $error ); | ||
return; | ||
} | ||
} | ||
|
||
/** | ||
* Handle the profile update. | ||
* | ||
* @param int $user_id The user ID. | ||
* @param \WP_User $old_user_data Object containing user data prior to update. | ||
* | ||
* @return void | ||
*/ | ||
public function on_profile_update( int $user_id, \WP_User $old_user_data ): void { | ||
// phpcs:ignore WordPress.Security.NonceVerification | ||
if ( isset( $_POST['action'] ) && $_POST['action'] === 'update' ) { | ||
$this->save_recent_password( $user_id, $old_user_data->user_pass ); | ||
} | ||
} | ||
|
||
/** | ||
* Handle the password reset. | ||
* | ||
* @param \WP_User $user The user. | ||
* | ||
* @return void | ||
*/ | ||
public function on_password_reset( $user ): void { | ||
$this->save_recent_password( $user->ID, $user->user_pass ); | ||
} | ||
|
||
/** | ||
* Save the new password hash to the user's recent passwords list. | ||
* | ||
* @param int $user_id The user ID. | ||
* @param string $password_hash The password hash to store. | ||
* | ||
* @return void | ||
*/ | ||
public function save_recent_password( int $user_id, string $password_hash ): void { | ||
$recent_passwords = get_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, true ); | ||
|
||
if ( ! is_array( $recent_passwords ) ) { | ||
$recent_passwords = array(); | ||
} | ||
|
||
if ( in_array( $password_hash, $recent_passwords, true ) ) { | ||
return; | ||
} | ||
|
||
// Add the new hashed password and keep only the last 10 | ||
array_unshift( $recent_passwords, $password_hash ); | ||
$recent_passwords = array_slice( $recent_passwords, 0, 10 ); | ||
|
||
update_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, $recent_passwords ); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand the flow correctly, will using the "wp_enqueue_scripts" action here load these styles on every request?
If so, could we use login_enqueue_scripts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your understanding is correct - this does fire on every request but for some odd reason
login_enqueue_scripts
was not working to limit it to login requests. It seems to enqueue the styles on the basewp-login.php
page but the minute we are redirected towp-login.php?action=account-protection...
(after failed password validation) the styles are missing. I suspect it might have to do with the order in which these actions are fired - at the time the custom password detectionwp-login.php
page is generated this action isn't able to attach the styles?As an alternative, we can continue to use
wp_enqueue_scripts
but add some logic to the callback so that we are only enqueuing the styles if$_GET[ 'action' ]
is set and equals'account-protection'
- we could take it one step further and also check! wp_style_is( $handle, 'enqueued' )
. Thoughts/opinions?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Conditionally enqueuing works for me 👍
We could use
$_GLOBALS['pagenow'] === 'wp-login.php'
to narrow it down to the specific page, plus theaction
check.IIRC we shouldn't need to worry about checking if the style was enqueued or not already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in d296b26 and b716b6f