Add email verification before activation - #788
faisalahammad wants to merge 4 commits into
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.
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! |
|
@faisalahammad could you please:
this would make it a lot easier to review the PR properly :) |
4bab1d9 to
412d2fe
Compare
|
Hi @masteradhoc, I’ve updated the branch based on your feedback:
Could you please check and let me know if everything good to merge?
|
412d2fe to
a0f6973
Compare
|
Hi @masteradhoc, Thanks for the feedback! I've updated the PR with the following changes:
Everything should be green now. Could you please re-review when you have a chance? |
masteradhoc
left a comment
There was a problem hiding this comment.
early feedback. @nimesh-xecurify could you check the tests added in this PR if they are fine?
|
Hi @masteradhoc, Thanks for the thorough review! I've pushed a fix addressing all your feedback: @ since tag fixes:
CI test failures fixed:
Could you please re-review when you get a chance? |
|
@faisalahammad What do you see the flow being on this if the user changes their email address? Should the user have to re-verify the new one through two-factor, or should it silently change the email it sends to to the new one (ignoring if the new email inbox has some spam filtering or deliverability problems) |
1bca12d to
2506dba
Compare
|
@georgestephanis Right now it silently follows the account email. The provider does not store the address that was verified. It reads the account email at send time. So when the user changes their account email, the token goes to the new address. The verified flag stays set. WordPress core already confirms a new profile email before it applies the change. So the new address belongs to the user. But the token then goes to an address this provider never verified on its own. If that address has spam filtering or deliverability problems, the user can get locked out. I see two options:
I lean toward option 2 for safety. But I did not want to expand scope without your call. Which one do you prefer? |
faisalahammad
left a comment
There was a problem hiding this comment.
Rebased the branch on the latest master and pushed the changes.
What changed since the last review:
- New @SInCE tags use 0.17.0.
- The email REST routes check the user ID and return 400 for an invalid user.
- The delete route disables the provider first, then deletes the verified meta. This matches the TOTP provider.
- is_available_for_user and pre_user_options_update now use get_enabled_providers_for_user for the legacy enabled check.
- The token email subject and message filters get a new action arg.
- user_options returns early for non WP_User values.
- The delete tests no longer enable the provider in setup. Master now needs session revalidation for that, and the tests match the TOTP tests now.
- login_html checks is_wp_error before it splits off the backup providers.
One behavior change to note: if a user has only removed providers enabled and Email is not verified for them, login now shows an error instead of the email fallback. It fails closed.
PHPCS, PHPStan and the full test suite pass locally.
|
@masteradhoc @nimesh-xecurify The branch is rebased on the latest master and all feedback is addressed. The @SInCE tags now use 0.17.0 and the earlier CI test failures are fixed. Details in my review above. GitHub does not let me add you as reviewers, so pinging here. Could you please re-review when you have a chance? |
The "Lint JS & CSS" check fails on master since dependabot bumped @wordpress/scripts to 35.0.0, which ships ESLint v10. Gruntfile.js has 19 errors there, and this PR inherited them: a /* eslint-env */ comment that ESLint v10 no longer supports, plus prettier formatting differences. Remove the eslint-env comment. Node globals now come from the wp-scripts flat config. Reformat the file to match the current prettier rules. Build config only. No plugin behavior changes. Refs WordPress#788.
CI Fix SummaryProblem: The "Lint JS & CSS" check failed with 19 errors in Gruntfile.js. This PR did not touch that file. Dependabot bumped @wordpress/scripts to 35.0.0 on master. That version ships ESLint v10 with stricter rules. Fix (55ae3ed):
Result:
No plugin behavior changes. Build config only. |
Implements a verification step for the Email provider. Users must verify their email address before the Email 2FA method can be enabled. Legacy users who already have Email 2FA enabled are unaffected. Changes: - Add REST API endpoints for email verification (POST/DELETE /two-factor/1.0/email) - Add VERIFIED_META_KEY to track verified email addresses - Update is_available_for_user() to require verification (with legacy fallback) - Add pre_user_options_update() to prevent enabling without verification - Add email-admin.js for verification UI interactions - Add is_wp_error() guards for get_available_providers_for_user() calls - Add comprehensive REST API and unit tests Fixes WordPress#778
- Fix enqueue_assets() @SInCE version: 0.10.0 → 0.16.0 - Add missing @SInCE 0.16.0 to register_rest_routes() - Add missing @SInCE 0.16.0 to rest_setup_email() - Add missing @SInCE 0.16.0 to rest_delete_email() - Add missing @SInCE 0.16.0 to pre_user_options_update() - Fix test_user_two_factor_rest_setup_email_valid_code: replace undefined is_provider_enabled_for_user() with in_array check - Fix test_user_can_delete_email_verification: set verified meta before enabling provider - Fix test_admin_can_delete_email_for_others: add enable_provider_for_user call - Fix test_generate_and_email_token_login_context_correct_args: match assertions to actual email body text - Fix test_other_sessions_destroyed_when_enabling_2fa: add verified meta before enabling Email 2FA - Fix test_user_options (backup codes): update assertion for wp_scripts data
- Bump new method since tags to 0.17.0 - Validate user ID in email REST routes and return 400 - Disable provider before deleting verified meta - Use core API for legacy enabled provider checks - Add action arg to token email subject and message filters - Guard user_options against non WP_User values - Fix delete tests for the revalidation gate and strict assertions - Move is_wp_error check before backup providers in login_html
- email-admin.js: drop the redundant `wp` global declaration and call `window.alert()` so the file passes the new flat ESLint config, which now lints `providers/js` and does not define a bare `alert` global. - email provider UI: use the HTML5 void-element closing style that the other providers settled on, and spell "email" the way the rest of the plugin does, including this PR's own message body. - Restore the `uninstall_user_meta_keys()` docblock wording used by the sibling providers, which an earlier commit on this branch had garbled. - tests: mark the email provider as verified in the missing-provider fallback test added on master, which now goes through the new availability gate. Refs WordPress#778
55ae3ed to
cd43167
Compare
|
I rebased this branch onto current Rebase resultThe branch is now 4 commits on top of
Two things were dropped because
Tests pass (272 tests, 851 assertions) and all linters pass. The question: fallback lockout
This PR makes the Email provider require prior setup (verification). Combine the two and a specific user gets locked out:
Before this PR the same user was emailed a code and could log in. The legacy exemption in Two ways to resolve it:
I did not decide this on my own because it changes auth behavior beyond the original review feedback. Please tell me which way to go. @masteradhoc @georgestephanis The build zip and manual test steps are ready. I will keep this as a draft-level change until we settle the question above. |

What?
Add an email verification step before the Email two-factor provider can be activated, aligning the Email provider's activation flow with the TOTP provider.
Fixes #778
Why?
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. Requiring a successful code verification before the provider can be enabled closes that gap.
How?
user_options(),providers/js/email-admin.js)POST /two-factor/1.0/email(send/validate verification codes) andDELETE /two-factor/1.0/email(reset verification status) viaregister_rest_routes(),rest_setup_email(), andrest_delete_email(), with the same permission check used by other providers.Two_Factor_Email::is_available_for_user()returnstrueonly if the user is verified (or already has the provider enabled), checked via the_two_factor_email_verifieduser meta (VERIFIED_META_KEY). No core changes are needed on top of currentmaster:Two_Factor_Core::get_available_providers_for_user()already callsis_available_for_user()for the fallback provider, so this gate is respected there.pre_user_options_update()hook prevents enabling the Email provider via the standard profile form save unless the user is verified.generate_and_email_token()accepts an$actionargument (loginvsverification_setup) to send context-appropriate subject and body.Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): GLM
Used for: Initial implementation, unit/REST tests, and merge conflict resolution on the rebase. The final code and tests were reviewed, tested, and edited by me.
Testing Instructions
New flow (fresh setup):
No regression (legacy user):
No regression (login flow):
loginaction path ingenerate_and_email_token()).Screenshots or screencast
Changelog Entry