Add email verification before activation#788
Add email verification before activation#788faisalahammad wants to merge 3 commits intoWordPress:masterfrom
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
georgestephanis
left a comment
There was a problem hiding this comment.
I want to see this go in, but I think to avoid merge conflicts it'll need to pause until #814 goes in, or that will need to pause for this. Or this can just start doing the newer external include. Either way.
Like the idea, but there's some extra changes in the PR that I don't think need to be in this PR? I'm looking at the distignore, and I'm not sure why it's changing from protected to public for the constructor ... possibly totally reasonable, I'm just trying to be thorough.
There was a problem hiding this comment.
Pull request overview
Adds an email-verification step for the Email two-factor provider by introducing a “verified” user-meta flag, a REST-driven verification flow in the profile UI, and guardrails to prevent enabling Email 2FA unless verified (with legacy compatibility).
Changes:
- Add
VERIFIED_META_KEYand gateis_available_for_user()on verification (while allowing legacy-enabled users). - Introduce Email provider REST endpoints to send/verify codes and to deactivate/reset verification state.
- Expand unit tests for email contents, availability gating, and profile-save behavior; update
.distignore.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
providers/class-two-factor-email.php |
Adds verification meta key, REST endpoints, updated email content handling, UI changes, and profile-save enforcement. |
tests/providers/class-two-factor-email.php |
Adds/updates tests for verification-context emails, availability rules, and pre_user_options_update() behavior. |
.distignore |
Ignores two-factor.zip from distribution exports. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
providers/class-two-factor-email.php
Outdated
| public function __construct() { | ||
| add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) ); | ||
| add_action( 'two_factor_user_options_' . __CLASS__, array( $this, 'user_options' ) ); | ||
| add_action( 'personal_options_update', array( $this, 'pre_user_options_update' ), 5 ); | ||
| add_action( 'edit_user_profile_update', array( $this, 'pre_user_options_update' ), 5 ); | ||
| parent::__construct(); | ||
| } |
providers/class-two-factor-email.php
Outdated
| ), | ||
| ); | ||
| /* translators: $1$s: IP address of user, %2$s: `user_login` of authenticated user */ | ||
| /* translators: $1$s: IP address of user, %2$s: `user_login` of authenticated user */ |
| public function pre_user_options_update( $user_id ) { | ||
| if ( isset( $_POST[ Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY ] ) && is_array( $_POST[ Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY ] ) ) { | ||
| $enabled_providers = $_POST[ Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY ]; | ||
| if ( in_array( 'Two_Factor_Email', $enabled_providers, true ) ) { | ||
| $is_verified = get_user_meta( $user_id, self::VERIFIED_META_KEY, true ); | ||
| $current_providers = get_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, true ); | ||
|
|
||
| // If not verified, and NOT currently enabled (legacy), disallow enabling. | ||
| if ( ! $is_verified && ( ! is_array( $current_providers ) || ! in_array( 'Two_Factor_Email', $current_providers, true ) ) ) { | ||
| $enabled_providers = array_diff( $enabled_providers, array( 'Two_Factor_Email' ) ); | ||
| $_POST[ Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY ] = $enabled_providers; | ||
| } | ||
| } | ||
| } |
| // Mock REMOTE_ADDR for IP check | ||
| $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; | ||
|
|
||
| $this->provider->generate_and_email_token( $user, 'login' ); | ||
|
|
||
| $content = $GLOBALS['phpmailer']->Body; | ||
|
|
||
| $this->assertStringContainsString( 'Enter', $content ); | ||
| $this->assertStringContainsString( 'log in', $content ); | ||
| // Check that IP is effectively in the message (and not the token key or something else) | ||
| $this->assertStringContainsString( '127.0.0.1', $content ); | ||
| // Check that username is in the message | ||
| $this->assertStringContainsString( $user->user_login, $content ); |
| /** | ||
| * REST API endpoint for setting up Email. | ||
| * | ||
| * @param WP_REST_Request $request The Rest Request object. | ||
| * @return WP_Error|array Array of data on success, WP_Error on error. | ||
| */ | ||
| public function rest_setup_email( $request ) { | ||
| $user_id = $request['user_id']; | ||
| $user = get_user_by( 'id', $user_id ); | ||
|
|
||
| $code = preg_replace( '/\s+/', '', $request['code'] ); | ||
|
|
||
| // If no code, generate and email one. | ||
| if ( empty( $code ) ) { | ||
| if ( $this->generate_and_email_token( $user, 'verification_setup' ) ) { | ||
| return array( 'success' => true ); | ||
| } | ||
| return new WP_Error( 'email_error', __( 'Unable to send email. Please check your server settings.', 'two-factor' ), array( 'status' => 500 ) ); | ||
| } | ||
|
|
||
| // Verify code. | ||
| if ( ! $this->validate_token( $user_id, $code ) ) { | ||
| return new WP_Error( 'invalid_code', __( 'Invalid verification code.', 'two-factor' ), array( 'status' => 400 ) ); | ||
| } | ||
|
|
||
| // Mark as verified. | ||
| update_user_meta( $user_id, self::VERIFIED_META_KEY, true ); | ||
|
|
||
| if ( $request->get_param( 'enable_provider' ) && ! Two_Factor_Core::enable_provider_for_user( $user_id, 'Two_Factor_Email' ) ) { | ||
| return new WP_Error( 'db_error', __( 'Unable to enable Email provider for this user.', 'two-factor' ), array( 'status' => 500 ) ); | ||
| } | ||
|
|
||
| ob_start(); | ||
| $this->user_options( $user ); | ||
| $html = ob_get_clean(); | ||
|
|
||
| return array( | ||
| 'success' => true, | ||
| 'html' => $html, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Rest API endpoint for handling deactivation of Email. | ||
| * | ||
| * @param WP_REST_Request $request The Rest Request object. | ||
| * @return array Success array. | ||
| */ | ||
| public function rest_delete_email( $request ) { | ||
| $user_id = $request['user_id']; | ||
| $user = get_user_by( 'id', $user_id ); | ||
|
|
||
| delete_user_meta( $user_id, self::VERIFIED_META_KEY ); | ||
|
|
||
| if ( ! Two_Factor_Core::disable_provider_for_user( $user_id, 'Two_Factor_Email' ) ) { | ||
| return new WP_Error( 'db_error', __( 'Unable to disable Email provider for this user.', 'two-factor' ), array( 'status' => 500 ) ); | ||
| } | ||
|
|
||
| ob_start(); | ||
| $this->user_options( $user ); | ||
| $html = ob_get_clean(); | ||
|
|
||
| return array( | ||
| 'success' => true, | ||
| 'html' => $html, | ||
| ); | ||
| } |
masteradhoc
left a comment
There was a problem hiding this comment.
Hey @faisalahammad
Thanks for your PR :) as we've just merged #814 would you mind seperating your code as well to the seperate files that the PR added?
abc10ef to
2515e10
Compare
|
I have updated the PR to address all the feedback:
Ready for another review! |
Fixed 12 file(s) based on 14 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Description
This PR implements a verification step for the Email provider in the Two-Factor plugin.
Previously, users could enable Email 2FA without confirming ownership of the email address, which posed a risk of account lockout if the email was incorrect or inaccessible. This change aligns the Email provider's activation flow with the TOTP provider by requiring successful code verification before the provider can be enabled.
Changes
POST /two-factor/1.0/email: Handles sending verification codes and validating them.DELETE /two-factor/1.0/email: Handles resetting the verification status (if needed).Two_Factor_Email::is_available_for_user()now returnstrueonly if the user has verified their email (checked via_two_factor_email_verifieduser meta).pre_user_options_updatehook to prevent the Email provider from being enabled via the standard profile form save unless the user is verified.How to Test
New User (Fresh Setup)
Legacy User (Existing Setup)
Screenshot
Technical Details
Two_Factor_Emailregister_rest_routes()rest_setup_email()rest_delete_email()pre_user_options_update()user_options(): updated to render the verification UI.is_available_for_user(): added verification check (with legacy fallback).generate_and_email_token(): updated to accept an$actionargument ('login' vs 'verification_setup') to send context-appropriate emails.VERIFIED_META_KEY:_two_factor_email_verifiedChecklist
Fixes #778