Skip to content
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 custom password strength meter #41485

Open
wants to merge 37 commits into
base: add/packages/account-protection-password-validation
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
96e3fd1
Add foundation for the custom password strength meter
dkmyta Jan 31, 2025
1ce02ff
Merge branch 'add/packages/account-protection-password-validation' in…
dkmyta Jan 31, 2025
46a77ce
Merge branch 'add/packages/account-protection-password-validation' in…
dkmyta Jan 31, 2025
78e272b
Add ajax request for password validation
dkmyta Feb 1, 2025
924c68b
Merge branch 'add/packages/account-protection-password-validation' in…
dkmyta Feb 2, 2025
1721af3
Merge branch 'add/packages/account-protection-password-validation' in…
dkmyta Feb 2, 2025
b1fe35e
Merge branch 'add/packages/account-protection-password-validation' in…
dkmyta Feb 2, 2025
eafecc0
Updates and improvements
dkmyta Feb 3, 2025
f83f86c
Rebase
dkmyta Feb 3, 2025
e7cf410
Merge branch 'add/packages/account-protection-password-validation' in…
dkmyta Feb 4, 2025
108e3e1
Add password validation status handling and hook up ajax callback
dkmyta Feb 4, 2025
529005b
Update variables names
dkmyta Feb 4, 2025
d156dcf
Add loading state
dkmyta Feb 4, 2025
84c5ecb
Remove todos
dkmyta Feb 4, 2025
523b195
Add nonce to ajax request
dkmyta Feb 5, 2025
1157e6c
Improve logic
dkmyta Feb 6, 2025
9e0d65f
Rebase
dkmyta Feb 6, 2025
e093fab
Improvements and reorg
dkmyta Feb 6, 2025
f0448ef
Add info popovers
dkmyta Feb 6, 2025
8381d5d
Add core req to initial validation state
dkmyta Feb 6, 2025
73c3db2
Generalize core info popover message
dkmyta Feb 6, 2025
b76ee4b
Fix core strength meter status
dkmyta Feb 6, 2025
ab15e8d
Remove testing code
dkmyta Feb 6, 2025
20ac033
Ensure save enabled when appropriate
dkmyta Feb 6, 2025
70b18a6
Update todos
dkmyta Feb 6, 2025
e82172c
Center validation items
dkmyta Feb 6, 2025
fe525c7
Fix tests
dkmyta Feb 6, 2025
e96a8dd
Save alt approach
dkmyta Feb 6, 2025
9d4da96
Fix styling, centralize core references
dkmyta Feb 7, 2025
ec88c39
Reorg
dkmyta Feb 7, 2025
79997c6
Use global pagenow for context, restrict user specific check to profi…
dkmyta Feb 7, 2025
6723799
Compartmentalize generating and appending validation meter and status…
dkmyta Feb 7, 2025
5db6af0
Optimization and reorg improvements
dkmyta Feb 7, 2025
f3d5c46
Remove todos
dkmyta Feb 7, 2025
33fcc0b
Remove unneeded comments
dkmyta Feb 7, 2025
e0c08b8
Ensure info popover fits in all form views
dkmyta Feb 7, 2025
c55e5d2
Fix test
dkmyta Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions projects/packages/account-protection/src/assets/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions projects/packages/account-protection/src/assets/cross.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions projects/packages/account-protection/src/assets/info.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions projects/packages/account-protection/src/assets/loading.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,26 @@ class Account_Protection {
*/
private $password_manager;

/**
* Password_Strength_Meter instance
*
* @var Password_Strength_Meter
*/
private $password_strength_meter;

/**
* Account_Protection constructor.
*
* @param ?Modules $modules Modules instance.
* @param ?Password_Detection $password_detection Password detection instance.
* @param ?Password_Manager $password_manager Validation service instance.
* @param ?Modules $modules Modules instance.
* @param ?Password_Detection $password_detection Password detection instance.
* @param ?Password_Manager $password_manager Password manager instance.
* @param ?Password_Strength_Meter $password_strength_meter Password strength meter instance.
*/
public function __construct( ?Modules $modules = null, ?Password_Detection $password_detection = null, ?Password_Manager $password_manager = null ) {
$this->modules = $modules ?? new Modules();
$this->password_detection = $password_detection ?? new Password_Detection();
$this->password_manager = $password_manager ?? new Password_Manager();
public function __construct( ?Modules $modules = null, ?Password_Detection $password_detection = null, ?Password_Manager $password_manager = null, ?Password_Strength_Meter $password_strength_meter = null ) {
$this->modules = $modules ?? new Modules();
$this->password_detection = $password_detection ?? new Password_Detection();
$this->password_manager = $password_manager ?? new Password_Manager();
$this->password_strength_meter = $password_strength_meter ?? new Password_Strength_Meter();
}

/**
Expand Down Expand Up @@ -115,6 +124,14 @@ protected function register_runtime_hooks(): void {
// Update recent passwords list
add_action( 'profile_update', array( $this->password_manager, 'on_profile_update' ), 10, 3 );
add_action( 'after_password_reset', array( $this->password_manager, 'on_password_reset' ), 10, 2 );

// Enqueue password strength meter scripts
add_action( 'admin_enqueue_scripts', array( $this->password_strength_meter, 'enqueue_jetpack_password_strength_meter_profile_script' ) );
add_action( 'login_enqueue_scripts', array( $this->password_strength_meter, 'enqueue_jetpack_password_strength_meter_reset_script' ) );

// AJAX endpoint for password validation
add_action( 'wp_ajax_validate_password_ajax', array( $this->password_strength_meter, 'validate_password_ajax' ) );
add_action( 'wp_ajax_nopriv_validate_password_ajax', array( $this->password_strength_meter, 'validate_password_ajax' ) );
}

/**
Expand Down
9 changes: 8 additions & 1 deletion projects/packages/account-protection/src/class-config.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@
* Class Config
*/
class Config {
// Password Detection Constants
public const PASSWORD_DETECTION_TRANSIENT_PREFIX = 'password_detection';
public const PASSWORD_DETECTION_ERROR_CODE = 'password_detection_validation_error';
public const PASSWORD_DETECTION_EMAIL_SENT_EXPIRATION = 600; // 10 minutes
public const PASSWORD_DETECTION_MAX_RESEND_ATTEMPTS = 3;

public const VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY = 'jetpack_account_protection_recent_password_hashes';
// Password Manager Constants
public const PASSWORD_MANAGER_RECENT_PASSWORD_HASHES_USER_META_KEY = 'jetpack_account_protection_recent_password_hashes';
public const PASSWORD_MANAGER_RECENT_PASSWORDS_LIMIT = 10;

// Validation Service Constants
public const VALIDATION_SERVICE_MIN_LENGTH = 6;
public const VALIDATION_SERVICE_MAX_LENGTH = 150;
}
Original file line number Diff line number Diff line change
Expand Up @@ -356,15 +356,19 @@ private function set_transient_error( int $user_id, string $message, int $expira
* @return void
*/
public function enqueue_styles(): void {
global $pagenow;
if ( ! isset( $pagenow ) || $pagenow !== 'wp-login.php' ) {
return;
}
// No nonce verification necessary - reading only
// phpcs:disable WordPress.Security.NonceVerification
if ( ( isset( $GLOBALS['pagenow'] ) && $GLOBALS['pagenow'] === 'wp-login.php' ) && ( isset( $_GET['action'] ) && $_GET['action'] === 'password-detection' ) ) {
wp_enqueue_style(
'password-detection-styles',
plugin_dir_url( __FILE__ ) . 'css/password-detection.css',
array(),
Account_Protection::PACKAGE_VERSION
);
if ( isset( $_GET['action'] ) && $_GET['action'] === 'password-detection' ) {
wp_enqueue_style(
'password-detection-styles',
plugin_dir_url( __FILE__ ) . 'css/password-detection.css',
array(),
Account_Protection::PACKAGE_VERSION
);
}
}
}
45 changes: 12 additions & 33 deletions projects/packages/account-protection/src/class-password-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,42 +72,26 @@ public function validate_profile_update( \WP_Error $errors, bool $update, \stdCl
return;
}

if ( ( ! $update && ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_create-user'] ) ), 'create-user' ) ) )
|| ( $update && ! $this->verify_profile_update_nonce( $user->ID ) ) ) {
$errors->add( 'nonce_error', __( '<strong>Error:</strong> Nonce verification failed.', 'jetpack-account-protection' ) );
if ( ! $update && ( ! isset( $_POST['_wpnonce_create-user'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_create-user'] ) ), 'create-user' ) ) ) {
$errors->add( 'nonce_error', __( '<strong>Error:</strong> Create user nonce verification failed.', 'jetpack-account-protection' ) );
return;
}

$password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) );

if ( $update ) {
$old_user_data = $this->get_old_user_data( $user->ID );
if ( $this->validation_service->is_current_password( $old_user_data, $password ) ) {
$errors->add( 'password_error', __( '<strong>Error:</strong> The password was used recently.', 'jetpack-account-protection' ) );
return;
}
if ( $update && ! $this->verify_profile_update_nonce( $user->ID ) ) {
$errors->add( 'nonce_error', __( '<strong>Error:</strong> Update user nonce verification failed.', 'jetpack-account-protection' ) );
return;
}

$context = $update ? 'update' : 'create-user';
$error = $this->validation_service->return_first_validation_error( $user, $password, $context );
$password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) );

$error = $this->validation_service->get_first_validation_error( $password, true, $user );

if ( ! empty( $error ) ) {
$errors->add( 'password_error', $error );
return;
}
}

/**
* Get the old user data.
*
* @param int $user_id The user ID.
*
* @return \WP_User|false The old user data, or false if the user does not exist.
*/
public function get_old_user_data( $user_id ) {
return get_userdata( $user_id );
}

/**
* Validate the password reset.
*
Expand Down Expand Up @@ -135,12 +119,7 @@ public function validate_password_reset( \WP_Error $errors, $user ): void {
}

$password = sanitize_text_field( wp_unslash( $_POST['pass1'] ) );
if ( $this->validation_service->is_current_password( $user, $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' );
$error = $this->validation_service->get_first_validation_error( $password );
if ( ! empty( $error ) ) {
$errors->add( 'password_error', $error );
return;
Expand Down Expand Up @@ -187,7 +166,7 @@ public function on_password_reset( $user, $new_password ): void { // phpcs:ignor
* @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 );
$recent_passwords = get_user_meta( $user_id, Config::PASSWORD_MANAGER_RECENT_PASSWORD_HASHES_USER_META_KEY, true );

if ( ! is_array( $recent_passwords ) ) {
$recent_passwords = array();
Expand All @@ -199,8 +178,8 @@ public function save_recent_password( int $user_id, string $password_hash ): voi

// 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 );
$recent_passwords = array_slice( $recent_passwords, 0, Config::PASSWORD_MANAGER_RECENT_PASSWORDS_LIMIT );

update_user_meta( $user_id, Config::VALIDATION_SERVICE_RECENT_PASSWORD_HASHES_USER_META_KEY, $recent_passwords );
update_user_meta( $user_id, Config::PASSWORD_MANAGER_RECENT_PASSWORD_HASHES_USER_META_KEY, $recent_passwords );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php
/**
* Class used to define Password Strength Meter.
*
* @package automattic/jetpack-account-protection
*/

namespace Automattic\Jetpack\Account_Protection;

/**
* Class Password_Strength_Meter
*/
class Password_Strength_Meter {
/**
* 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();
}

/**
* AJAX endpoint for password validation.
*
* @return void
*/
public function validate_password_ajax(): void {
if ( ! isset( $_POST['password'] ) ) {
wp_send_json_error( array( 'message' => __( 'No password provided.', 'jetpack-account-protection' ) ) );
}

if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'validate_password_nonce' ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'jetpack-account-protection' ) ) );
}

$user_specific = false;
if ( isset( $_POST['user_specific'] ) ) {
$user_specific = filter_var( sanitize_text_field( wp_unslash( $_POST['user_specific'] ) ), FILTER_VALIDATE_BOOLEAN );
}

$password = sanitize_text_field( wp_unslash( $_POST['password'] ) );
$state = $this->validation_service->get_validation_state( $password, $user_specific );

wp_send_json_success( array( 'state' => $state ) );
}

/**
* Enqueue the password strength meter script on the profile page.
*
* @return void
*/
public function enqueue_jetpack_password_strength_meter_profile_script(): void {
global $pagenow;

if ( ! isset( $pagenow ) || ! in_array( $pagenow, array( 'profile.php', 'user-new.php', 'user-edit.php' ), true ) ) {
return;
}

$this->enqueue_script();
$this->enqueue_styles();

// Only profile page should run user specific checks.
$this->localize_jetpack_data( 'profile.php' === $pagenow );
}

/**
* Enqueue the password strength meter script on the reset password page.
*
* @return void
*/
public function enqueue_jetpack_password_strength_meter_reset_script(): void {
// No nonce verification necessary as the action includes a robust verification process
// phpcs:disable WordPress.Security.NonceVerification
if ( isset( $_GET['action'] ) && ( 'rp' === $_GET['action'] || 'resetpass' === $_GET['action'] ) ) {
$this->enqueue_script();
$this->enqueue_styles();
$this->localize_jetpack_data();
}
}

/**
* Localize the Jetpack data for the password strength meter.
*
* @param bool $user_specific Whether or not to run user specific checks.
*
* @return void
*/
public function localize_jetpack_data( bool $user_specific = false ): void {
wp_localize_script(
'jetpack-password-strength-meter',
'jetpackData',
array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'validate_password_nonce' ),
'userSpecific' => $user_specific,
'logo' => plugin_dir_url( __FILE__ ) . 'assets/jetpack-logo.svg',
'infoIcon' => plugin_dir_url( __FILE__ ) . 'assets/info.svg',
'checkIcon' => plugin_dir_url( __FILE__ ) . 'assets/check.svg',
'crossIcon' => plugin_dir_url( __FILE__ ) . 'assets/cross.svg',
'loadingIcon' => plugin_dir_url( __FILE__ ) . 'assets/loading.svg',
'validationInitialState' => $this->validation_service->get_validation_initial_state( $user_specific ),
)
);
}

/**
* Enqueue the password strength meter script.
*
* @return void
*/
public function enqueue_script(): void {
wp_enqueue_script(
'jetpack-password-strength-meter',
plugin_dir_url( __FILE__ ) . 'js/jetpack-password-strength-meter.js',
array( 'jquery' ),
Account_Protection::PACKAGE_VERSION,
true
);
}

/**
* Enqueue the password strength meter styles.
*
* @return void
*/
public function enqueue_styles(): void {
wp_enqueue_style(
'strength-meter-styles',
plugin_dir_url( __FILE__ ) . 'css/strength-meter.css',
array(),
Account_Protection::PACKAGE_VERSION
);
}
}
Loading
Loading