twofa_settings = get_option( Two_Factor_Authentication::OPTION_NAME, [] ); add_action( 'show_user_profile', [ $this, 'add_user_2fa_section' ] ); add_action( 'edit_user_profile', [ $this, 'add_user_2fa_section' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); add_action( 'wp_ajax_adminoptim-setup-2fa', [ $this, 'setup_2fa' ] ); add_action( 'wp_ajax_adminoptim-reset-2fa', [ $this, 'reset_2fa' ] ); add_action( 'wp_ajax_adminoptim-generate-recovery-code', [ $this, 'setup_recovery_code' ] ); } /** * Function to add user 2FA setup section * * @param \WP_User $user Current user class. * * @return void */ public function add_user_2fa_section( $user ) { $current_user = wp_get_current_user(); $can_setup_2fa = false; if ( $current_user->ID === $user->ID ) { $can_setup_2fa = true; } $user_2fa = get_user_meta( $user->ID, Two_Factor_Authentication::OPTION_NAME, true ); ?>

validate_user_2fa_permission( $user_id ); $response = []; $secret = isset( $_REQUEST['secret'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['secret'] ) ) : ''; if ( ! isset( $_REQUEST['code'] ) ) { $response = new \WP_Error( 'adminoptim-no-code-found', ao_get_admin_notice( __( 'No 2FA code is found', 'admin-optimizer' ), [ 'type' => 'error', ] ) ); wp_send_json_error( $response ); } elseif ( ! is_numeric( $_REQUEST['code'] ) ) { $response = new \WP_Error( 'adminoptim-code-not-digits', ao_get_admin_notice( __( 'The 2FA code is not digits', 'admin-optimizer' ), [ 'type' => 'error', ] ) ); wp_send_json_error( $response ); } elseif ( mb_strlen( sanitize_text_field( wp_unslash( $_REQUEST['code'] ) ) ) !== 6 ) { $response = new \WP_Error( 'adminoptim-code-must-be-6-digits', ao_get_admin_notice( __( 'The 2FA code must be 6 digits', 'admin-optimizer' ), [ 'type' => 'error', ] ) ); wp_send_json_error( $response ); } $code = trim( sanitize_text_field( wp_unslash( $_REQUEST['code'] ) ) ); if ( ! empty( $user_id ) && ! empty( $secret ) && ! empty( $code ) ) { $totp = new ToTP(); [ $is_used_fn, $mark_used_fn ] = Two_Factor_Authentication::get_totp_replay_callbacks( $user_id ); // Allow discrepancy of 1 time slice. if ( $totp->verify_code( $secret, $code, 30, 6, 1, 'sha1', $is_used_fn, $mark_used_fn ) ) { $user_2fa_settings = []; $user_2fa_settings['secret'] = Two_Factor_Authentication::store_secret( $user_id, $secret ); $user_2fa_settings['enabled'] = 1; update_user_meta( $user_id, Two_Factor_Authentication::OPTION_NAME, $user_2fa_settings ); $response['message'] = ao_get_admin_notice( __( 'Two-factor authentication validated and enabled successfully.', 'admin-optimizer' ), [ 'type' => 'success', ] ); wp_send_json_success( $response ); } else { $response = new \WP_Error( 'adminoptim-invalid-2facode', ao_get_admin_notice( __( 'Invalid 2FA code', 'admin-optimizer' ), [ 'type' => 'error', ] ) ); wp_send_json_error( $response ); } } else { $response = new \WP_Error( 'adminoptim-invalid-2fa-info', __( 'Invalid 2FA information', 'admin-optimizer' ) ); wp_send_json_error( $response ); } } /** * Ajax action to disable user 2FA * * @param int $user_id user ID. * * @return void */ public function reset_2fa( $user_id ) { $user_id = ! empty( $_REQUEST['user_id'] ) ? absint( $_REQUEST['user_id'] ) : 0; check_ajax_referer( $user_id . 'user_2fa_info', 'nonce' ); $this->validate_user_2fa_permission( $user_id ); $response = []; if ( $user_id < 1 ) { $response = new \WP_Error( 'adminoptim-invalid-user', ao_get_admin_notice( __( 'Invalid user.', 'admin-optimizer' ), [ 'type' => 'error', ] ) ); wp_send_json_error( $response ); } else { Two_Factor_Authentication::delete_secret( $user_id ); delete_user_meta( $user_id, Two_Factor_Authentication::OPTION_NAME ); $response['message'] = ao_get_admin_notice( __( 'The authenticator app has been disabled successfully. Please reload the page to set it up again.', 'admin-optimizer' ), [ 'type' => 'success', ] ); wp_send_json_success( $response ); } } /** * Setting up recovery codes settings on User Settings section * * @return void */ public function setup_recovery_code() { $user_id = isset( $_REQUEST['user_id'] ) ? absint( $_REQUEST['user_id'] ) : get_current_user_id(); check_ajax_referer( $user_id . 'user_2fa_info', 'nonce' ); $this->validate_user_2fa_permission( $user_id ); $response = []; if ( $user_id < 1 ) { $response = new \WP_Error( 'adminoptim-invalid-user', ao_get_admin_notice( __( 'Invalid user.', 'admin-optimizer' ), [ 'type' => 'error', ] ) ); wp_send_json_error( $response ); } else { $user_2fa = get_user_meta( $user_id, Two_Factor_Authentication::OPTION_NAME, true ); if ( ! Two_Factor_Authentication::is_user_enrolled( $user_2fa ) ) { wp_send_json_error( new \WP_Error( 'adminoptim-2fa-not-enrolled', ao_get_admin_notice( __( 'Set up the authenticator app before generating recovery codes.', 'admin-optimizer' ), [ 'type' => 'error', ] ) ) ); } $recovery_codes = $this->generate_recovery_code(); $user_2fa['recovery_codes'] = $recovery_codes['hashed_codes']; update_user_meta( $user_id, Two_Factor_Authentication::OPTION_NAME, $user_2fa ); $user = get_user_by( 'id', $user_id ); $filename = sanitize_title_with_dashes( get_bloginfo( 'name' ) ) . '-2FA-recovery-codes-' . $user->user_login . '.txt'; $message = '

' . __( 'Write these down! Once you navigate away from this page, you will not be able to view these codes again.', 'admin-optimizer' ) . '

'; $message .= ''; $message .= '

' . __( 'Download the codes here. Once you navigate away from this page, you will not be able to download the codes again.', 'admin-optimizer' ) . '

'; $message .= '

' . __( 'Download Codes', 'admin-optimizer' ) . '

'; $response['message'] = $message; wp_send_json_success( $response ); } } /** * Function to generate recovery codes * * @return array * @throws \Random\RandomException Throws error if generation fails. */ private function generate_recovery_code() { $code_length = 10; $codes = []; $hashed_codes = []; for ( $i = 0; $i < 10; $i++ ) { $codes[] = strtoupper( bin2hex( random_bytes( $code_length / 2 ) ) ); } for ( $i = 0; $i < 10; $i++ ) { $hashed_codes[] = wp_hash_password( $codes[ $i ] ); } $download_link = $this->download_rc_link( $codes ); return [ 'codes' => $codes, 'hashed_codes' => $hashed_codes, 'download_link' => $download_link, ]; } /** * Generate link for user to download recovery codes. * * @param string[] $codes A list of generated recovery codes. * * @return string */ public function download_rc_link( $codes ) { $download_link = ''; if ( ! empty( $codes ) && is_array( $codes ) ) { $blogname = get_bloginfo( 'name' ); $current_date = wp_date( 'Y-m-d H:i:s' ); $timezone = wp_timezone_string(); $message = rawurlencode( "Recovery codes for {$blogname}\n" ); $message .= rawurlencode( "Keep these backup codes somewhere safe but accessible.\n\n" ); foreach ( $codes as $code ) { $message .= rawurlencode( "{$code}\n" ); } $message .= rawurlencode( "\n" ); $message .= rawurlencode( "* You can only use each backup code once.\n" ); $message .= rawurlencode( "* These codes were generated on {$current_date} ({$timezone})" ); $download_link = 'data:application/text;charset=utf-8,' . $message; } return $download_link; } /** * Check whether the current user can modify 2FA settings for a target user. * * @param int $user_id Target user ID. * * @return void */ private function validate_user_2fa_permission( int $user_id ) { $current_user_id = get_current_user_id(); if ( $user_id < 1 || ( $current_user_id !== $user_id && ! current_user_can( 'edit_user', $user_id ) ) ) { wp_send_json_error( new \WP_Error( 'adminoptim-unauthorized-user', ao_get_admin_notice( __( 'Unauthorized action.', 'admin-optimizer' ), [ 'type' => 'error', ] ) ) ); } } }