Skip to content

Add personal data exporter - #997

Open
faisalahammad wants to merge 7 commits into
WordPress:masterfrom
faisalahammad:feature/954-privacy-exporter-eraser
Open

faisalahammad wants to merge 7 commits into
WordPress:masterfrom
faisalahammad:feature/954-privacy-exporter-eraser

Conversation

@faisalahammad

@faisalahammad faisalahammad commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

What?

Registers the plugin with the WordPress personal data export tool (Tools > Export Personal Data). Users can request a copy of their two-factor data.

Fixes #954

The eraser was split out of this PR, as asked in review. It will follow in a separate PR that builds on the provider methods added here.

Why?

The plugin stores per-user authentication data (enabled methods, failed login counters, one-time tokens, TOTP replay markers, per-session 2FA details) but does not hook into the export tool. None of it shows up in a personal data export.

How?

  • Two_Factor_Core registers the wp_privacy_personal_data_exporters filter and implements the callback. The "Two Factor Authentication" group lists the enabled methods, primary method, failed login attempts, last failed login time, and the password reset flag when it is set.
  • Methods are shown by their label from get_label() ("Authenticator App", "Email", "Recovery Codes"). The raw key is used for providers that no longer exist.
  • Per-session 2FA data from session_tokens (two-factor-login, two-factor-provider) is exported with core's session-tokens group ID and session-tokens-{$user_id}-{$token_key} item ID, so core merges it into the matching session. Each session shows when 2FA was verified and with which method.
  • Providers add their own rows through an optional privacy_export_data( $user ) method on Two_Factor_Provider, mirroring the uninstall_user_meta_keys() pattern: TOTP shows "Configured" plus the last successful login, Email shows when a code was last sent, Backup Codes shows the remaining code count.
  • The exporter goes through all registered providers, so data from a provider disabled in the site-wide settings is still exported.
  • Exports never include secret material: the TOTP key, hashed email tokens, and backup codes are verified absent from the payload by dedicated tests.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus
Used for: Implementation and test help. I reviewed and edited all generated code and tests.

Testing Instructions

Automated: npm test runs the new PHPUnit tests covering the export contents, provider labels and the fallback, per-session data, and that no secret material appears in the export payload.

Manual:

  1. Build and start the dev environment: npm install && npm run build && npm run env start.
  2. Edit a test user and enable Authenticator App, Email, and Recovery Codes. Set Authenticator App as primary and generate recovery codes.
  3. Log out and log in as the test user, passing the Authenticator App prompt.
  4. Go to Tools > Export Personal Data, enter the test user's email, and run the export.
  5. The "Two Factor Authentication" group shows Authenticator App, Email, Recovery Codes as enabled methods and Authenticator App as primary. No Two_Factor_* class names appear.
  6. In the "Session Tokens" group, the session from step 3 also shows "Two Factor verified" with a time and "Two Factor method: Authenticator App".
  7. No TOTP secret, tokens, or codes appear anywhere in the file.
  8. Disable Authenticator App in Settings > Two-Factor and export again. The TOTP rows still appear.

Screenshots or screencast

Before After
No Two Factor data in the personal data export. Export has a "Two Factor Authentication" group, and 2FA details are added to each matching session in "Session Tokens". No admin UI change.

Changelog Entry

Added - Register a personal data exporter with the WordPress privacy tools.

Open WordPress Playground Preview

Register the plugin with the WordPress personal data export and
erasure tools.

- Two_Factor_Core registers both privacy filters and implements the
  callbacks, keeping the data provider-agnostic.
- Providers contribute via two optional base methods on
  Two_Factor_Provider: privacy_export_data() and
  privacy_eraser_user_meta_keys(), mirroring the existing
  uninstall_user_meta_keys() pattern.
- The eraser removes short-lived records (login nonce, rate limit
  counters, email tokens, TOTP replay marker) and keeps credentials
  (TOTP secret, backup codes), reporting them as retained so the
  second factor stays active on the account.
- Exports never include secret material: the TOTP key, hashed email
  tokens and backup codes stay out of the payload.
- Tests cover the export contents, the erasure behavior, and that no
  secret material appears in the export payload.

Fixes WordPress#954
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: faisalahammad <faisalahammad@git.wordpress.org>
Co-authored-by: masteradhoc <masteradhoc@git.wordpress.org>
Co-authored-by: dknauss <dpknauss@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@masteradhoc

Copy link
Copy Markdown
Collaborator

Before we start reviewing. @faisalahammad would you mind changing all versions to 0.18.0 as 0.17.0 is already finalized?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Address the incomplete export of disabled providers and clarify the email timestamp’s accuracy.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Adds WordPress personal-data export and erasure support for Two-Factor authentication data.

Changes:

  • Registers privacy exporter and eraser callbacks.
  • Adds provider-specific privacy methods.
  • Exports non-secret metadata and removes short-lived records.
  • Adds comprehensive PHPUnit coverage.
File Reviewed changes
tests/​providers/​class-two-factor-totp.php Tests TOTP privacy behavior.
tests/​providers/​class-two-factor-provider.php Tests base provider privacy methods.
tests/​providers/​class-two-factor-email.php Tests email privacy behavior.
tests/​providers/​class-two-factor-backup-codes.php Tests recovery-code export behavior.
tests/​class-two-factor-core.php Tests core privacy behavior and secret exclusion.
providers/​class-two-factor-totp.php Handles TOTP export and replay-marker erasure.
providers/​class-two-factor-provider.php Adds provider privacy extension methods.
providers/​class-two-factor-email.php Handles email metadata export and token erasure.
providers/​class-two-factor-backup-codes.php Exports remaining recovery-code counts.
class-two-factor-core.php Implements privacy registration, export, and erasure.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread class-two-factor-core.php Outdated
@dknauss

dknauss commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Review comment, requested by @masteradhoc:

  • 🟢 Retaining 2FA credentials during erasure. This is good because WordPress’s erasure tool only erases user data; it doesn’t delete the user account, and removing 2FA credentials could leave the account protected only by its password.
  • 🟢 Provider-specific tests alongside each provider are good.
  • 🔴 The privacy callbacks skip providers that are disabled site-wide. Both use get_providers(), which applies the site-wide provider filter. If a provider is disabled there, its provider-specific data is omitted from exports and its erasure keys are skipped, leaving that metadata on the account. (Confirmed with runtime testing.)
  • 🔴 Erasure removes TOTP replay protection. A code that was accepted once and rejected when reused will be accepted again after erasure if this all happens quickly enough. Extremely unlikely as an achievable exploit, but it exposes an avoidable weakening of replay protection in the original design in Privacy: register a personal data exporter and eraser #954. The eraser deletes the record that prevents reuse while keeping the secret. That record should remain, as it can still block replay. Fix: keep the replay marker alongside the secret and disclose both as retained security data. (Discovered and confirmed with adversarial testing.)

@faisalahammad

Copy link
Copy Markdown
Contributor Author

@masteradhoc Done. All new @SInCE tags in this PR now say 0.18.0. The older 0.17.0 tags from the released version stay untouched.

@faisalahammad

Copy link
Copy Markdown
Contributor Author

@dknauss Thanks for the review. Both red points are fixed in commit 18ff905.

  1. The exporter and eraser now use a new get_privacy_providers() helper. It collects all registered providers like uninstall does. A provider disabled in the site settings is no longer skipped, so its data is still exported and erased.

  2. The TOTP last successful login time is no longer erased. It keeps blocking reuse of the most recent code after an erasure. The eraser now reports it as retained and shows a message. Tests cover both cases.

@masteradhoc masteradhoc added this to the 0.18.0 milestone Sep 28, 2026

@masteradhoc masteradhoc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @faisalahammad for the work on this PR!

Could we split the exporter and the eraser into two separate PRs? That would help speed up review and testing :) The exporter could go first, and the eraser could build on its provider methods.

Early feedback on what I'd do differently:

Eraser

  • Please don't delete USER_RATE_LIMIT_KEY and USER_FAILED_LOGIN_ATTEMPTS_KEY. They drive the brute-force backoff and the automatic password reset after 30 failed attempts, and today only a successful 2FA login or wp two-factor unlock clears them. An erasure request shouldn't reset them; report them as retained instead.
  • I'd also keep USER_PASSWORD_WAS_RESET_KEY rather than erase it. The CLI disable command preserves it on purpose, and it's cleared automatically once the user sets a new password. Since it stays, please add it to the export.

Exporter

  • The export shows PHP class names (Two_Factor_Totp, Two_Factor_Email, Two_Factor_Backup_Codes) for both the enabled and the primary method. Could you use each provider's get_label() ("Authenticator App", "Email", "Recovery Codes") and fall back to the raw key for providers that no longer exist?
  • Per-session 2FA data is missing. two-factor-login and two-factor-provider are stored in session_tokens, and core's exporter drops them. If you emit items with group_id => 'session-tokens' and core's item ID session-tokens-{$user_id}-{$token_key} (the array key of the session_tokens meta), core merges them into the matching session, so each session shows when 2FA was verified and with which method.

- Remove the personal data eraser, it moves to a separate PR
- Show provider labels instead of class names, fall back to the key
- Export the password reset flag
- Add per-session Two Factor data to the core session tokens export

Addresses PR feedback.

Refs WordPress#997
@faisalahammad

Copy link
Copy Markdown
Contributor Author

@masteradhoc Thanks for the review. I updated the PR in aa621db.

Split

  • This PR now has only the exporter. I removed the eraser code and its tests. The eraser will come in a new PR that builds on the provider methods here.

Exporter

  • Enabled and primary methods now use each provider's get_label(), so the export shows "Authenticator App, Email, Recovery Codes". If a provider no longer exists, the raw key is shown.
  • Per-session data is added. For each session with two-factor-login, the exporter sends an item with group_id => 'session-tokens' and the core item ID session-tokens-{$user_id}-{$token_key}, so core merges it into the same session. Each session shows when 2FA was verified and with which method. When 2FA was enabled during a session and no code was entered, the method says so.
  • USER_PASSWORD_WAS_RESET_KEY is now in the export.

Eraser (next PR)

  • I will keep USER_RATE_LIMIT_KEY, USER_FAILED_LOGIN_ATTEMPTS_KEY and USER_PASSWORD_WAS_RESET_KEY, and report them as retained.

I also updated the PR description and added the AI disclosure section.

- Use the matching wordpress-develop PHPUnit library for -branch
  matrix legs in .github/workflows/test.yml
- Return "Unknown" for non-string provider keys in the exporter
  label helper in class-two-factor-core.php
- Label setup-only sessions "Two Factor enabled" in the session export
- Cast the failed attempts count to int in the export
- Rename the TOTP export row to "Last successful TOTP verification"
  in providers/class-two-factor-totp.php
- Add a test for malformed provider meta and update label assertions

Errors fixed:
- Interface "WP_Secrets_Store" not found: wp-env loaded the trunk
  PHPUnit library for 7.1.4-alpha, which needs the Secrets API.
- TypeError from isset() with array provider meta in the exporter.

PHP 7.4+ compatible. PHPCS, PHPStan and PHPUnit (single site and
multisite) pass locally.

Refs WordPress#997

@faisalahammad faisalahammad left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI fix summary: 5 failures fixed

All 5 failing jobs were the wordpress/wordpress#7.1-branch legs. They failed with Interface "WP_Secrets_Store" not found in the PHPUnit bootstrap. The same error also shows on master. The core version on that branch is 7.1.4-alpha, so wp-env loads the PHPUnit library from wordpress-develop trunk. Trunk needs the Secrets API, and 7.1 does not have it.

# File Error Fix
1 .github/workflows/test.yml WP_Secrets_Store not found For -branch legs, check out the matching wordpress-develop branch for the PHPUnit library
2 class-two-factor-core.php TypeError from isset() when provider meta is an array Return "Unknown" for provider keys that are not strings
3 class-two-factor-core.php Sessions set up without a code were labeled "verified" Label them "Two Factor enabled"
4 class-two-factor-core.php Raw failed attempts meta in the export Cast to int
5 providers/class-two-factor-totp.php Label said "login" but the time also updates on revalidation Rename to "Last successful TOTP verification"

Fix 1 is the same change as in #1026. Items 2 to 5 came from a CodeRabbit review.

PHPCS, PHPStan and PHPUnit pass locally for single site and multisite (324 tests each).

@masteradhoc

Copy link
Copy Markdown
Collaborator

Thank you @faisalahammad ! Can you change the subject of this PR to "exporter" only?
And the fix for the CI can you add this outside of this PR so we can review that as well? would help all PRs

The workflow step that matches the PHPUnit library to the
WordPress branch now lives in its own PR, as asked in review.

Refs WordPress#997
@faisalahammad faisalahammad changed the title Add personal data exporter and eraser Add personal data exporter Oct 18, 2026
@faisalahammad

Copy link
Copy Markdown
Contributor Author

@masteradhoc Thanks. Both done.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved export correctness issues and a critical test-isolation issue remain.

3 open issues
1 resolved since last review

Review effort: Lite

Comment thread tests/class-two-factor-core.php
Comment thread class-two-factor-core.php
Comment thread providers/class-two-factor-backup-codes.php Outdated
- Report recovery codes with 0 left after all codes are used.
- Skip the site-wide provider filter for the exporter, so disabled
  third-party providers are still exported.
- Skip provider export data that is not an array.

Addresses PR feedback.
Refs WordPress#997

@faisalahammad faisalahammad left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 2b90abc after the Copilot review.

  • Recovery codes: a user who used all codes now exports "0 unused codes". Before, the row was missing.
  • Third-party providers that are off in Settings > Two-Factor are now in the export. The site-wide setting still hides them on login and profile.
  • Provider export data that is not an array is skipped, so one bad provider does not break the export.
  • The test option comment needs no change. The WP test case rolls back the DB and flushes the cache after each test.

Two new tests. PHPCS, PHPStan, and PHPUnit pass on single site and multisite (326 tests each).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Privacy: register a personal data exporter and eraser

4 participants