Skip to content

Add REST provider management and server-owned TOTP enrollment - #935

Open
chubes4 wants to merge 4 commits into
WordPress:masterfrom
chubes4:feat/rest-provider-management
Open

Add REST provider management and server-owned TOTP enrollment#935
chubes4 wants to merge 4 commits into
WordPress:masterfrom
chubes4:feat/rest-provider-management

Conversation

@chubes4

@chubes4 chubes4 commented Jul 22, 2026

Copy link
Copy Markdown

What?

Add an authenticated, provider-agnostic REST API for reading and updating a user's Two-Factor settings, plus a server-owned TOTP enrollment flow.

Fixes #934
Fixes #936
Fixes #937

Why?

The existing REST surface is provider-specific and incomplete: TOTP setup requires a caller-supplied secret, recovery codes expose generation only, Email has no management route, and clients cannot read configured/enabled/primary state or discover that recent Two-Factor revalidation is required.

External frontend settings therefore have to duplicate plugin security ownership or read user meta. This API lets those clients consume the upstream plugin directly without local wrappers.

A previous proposal in #672 would have added provider data to public user responses. This PR instead uses a dedicated route protected by the existing edit_user capability boundary, so anonymous users and users who cannot edit the target cannot enumerate provider state.

How?

Provider settings

GET /two-factor/1.0/users/<user_id>/providers returns:

  • user_id
  • providers: stable provider key and label, plus supported, configured, enabled, primary, and nullable recovery-code remaining state
  • revalidation: structured required, action, and url values targeting the existing revalidate_2fa flow

It never returns configured TOTP secrets, email tokens, recovery-code hashes, or raw user meta.

POST|PUT|PATCH /two-factor/1.0/users/<user_id>/providers accepts the complete desired settings:

{
  "enabled_providers": ["Two_Factor_Email", "Two_Factor_Backup_Codes"],
  "primary_provider": "Two_Factor_Email"
}

Both fields are required so omission cannot silently reset the selected primary provider. Every enabled provider must be supported and configured, and the primary must be included in the enabled set. The persistence path is shared with profile saves so first-time enablement, provider removal, current-session metadata, and other-session invalidation retain existing behavior. Empty arrays intentionally preserve the wp-admin ability to disable the final provider.

TOTP enrollment

POST /two-factor/1.0/totp/enrollment accepts user_id, generates the secret on the server, replaces any prior pending enrollment for that user, and returns the secret, otpauth:// URI, and expiration timestamp once for setup display.

The client confirms through the existing POST /two-factor/1.0/totp route with user_id and code, omitting key. Pending enrollment is user-bound, expires after ten minutes, is deleted on successful confirmation or expiry, and cannot be replayed after confirmation. The existing caller-supplied key behavior remains available for backward compatibility.

Security decisions

  • Read and write routes require the authenticated caller to have edit_user for the target.
  • Sensitive writes and TOTP enrollment reuse the existing recent-2FA permission helper.
  • Revalidation failures are explicit HTTP 403 responses containing the same structured revalidation target returned by status reads.
  • Cookie-authenticated REST requests continue to rely on WordPress REST nonce/CSRF handling; application-password behavior is unchanged.
  • Provider configuration is checked through provider methods rather than direct meta inspection.
  • Existing profile-save session invalidation semantics are reused for self-service and administrator edits.
  • TOTP enrollment creates only minimal expiring pending state; confirmed secrets continue through the existing validation and storage methods.
  • Recovery-code plaintext remains one-time output from the existing generation endpoint; this endpoint exposes only the remaining count.
  • Site provider filters and per-user provider filters continue to define support, including on multisite.

Backward compatibility

  • Existing /totp POST/DELETE behavior is retained.
  • Existing /generate-backup-codes behavior is retained.
  • Existing wp-admin profile UI uses the same provider persistence behavior after the internal extraction.
  • No public WordPress user REST fields are added.

Use of AI Tools

AI assistance: Yes
Tool(s): OpenCode
Model(s): GPT-5.6 Sol
Used for: Repository investigation, implementation, security review, tests, and PR drafting. The implementation and generated tests were reviewed against the existing provider, profile-save, permission, and session behavior.

Testing Instructions

Automated coverage was added for:

  • anonymous, self, cross-user, and administrator permissions
  • status fields, recovery-code counts, and TOTP secret redaction
  • Email and recovery-code enablement and primary selection
  • unsupported, unconfigured, malformed, and invalid-primary requests
  • structured recent-revalidation failures
  • self/admin session and primary-provider behavior
  • server-owned TOTP begin/confirm, replacement, expiry, replay prevention, and cross-user denial
  • compatibility with existing caller-supplied TOTP setup tests

Checks run:

  • npm run build passes
  • npm run lint:js passes
  • npm run lint:css passes
  • npm run lint:phpstan passes
  • composer lint-compat passes for PHP 7.2+
  • targeted PHPCS passes for the modified provider and REST test files
  • PHP syntax and git diff --check pass

npm test could not run locally because the required Docker-backed wp-env could not connect to Docker on the execution host. CI should run the PHPUnit suite. Repository-wide npm run lint reaches the documented pre-existing PHPCS failures (#437); no new violations are reported in the modified provider or REST tests.

Changelog Entry

Added - Authenticated REST provider management and server-owned TOTP enrollment.

Open WordPress Playground Preview

External Fuzz Evidence

An isolated Homeboy rig fuzzed this REST surface without adding campaign harnesses or artifacts to the PR:

  • Seed: 935202607
  • Operations: 14,044 REST requests across malformed schemas, provider transitions, permissions, TOTP lifecycle, and recovery-code interactions
  • Environment: WordPress 6.9, PHP 8.4.21
  • Finding: Fixes Recovery-code REST route accepts nonexistent users #937. The campaign reproduced a nonexistent-user recovery-code generation path that returned plaintext codes/warnings or a 500 error
    404 before provider callbacks/- Fix: commit 1940948 now performs capability-first target-user existence validation in the shared core REST permission boundary and returns rest_user_invalid_id (HTTP 404) before provider callbacks/404 before provider callbacks
  • Regression coverage: both minimized recovery-code payloads assert no codes, warnings, or state mutation; TOTP legacy/new mutations and generic provider settings assert the same missing-user response contract

All other campaign operations found no permission bypass, secret leakage, provider invariant failure, replay acceptance, or invalid-state mutation. The fuzz campaign remains external; only the minimized production fix and deterministic regression tests are included here.

@github-actions

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: chubes4 <extrachill@git.wordpress.org>

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

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.

Pull request overview

Adds a new authenticated REST surface to manage a user’s Two-Factor provider settings (read + full-state update), and introduces a server-owned TOTP enrollment flow where the server generates and returns the secret once and confirmation reuses the existing /totp endpoint.

Changes:

  • Adds core REST routes to read/update provider state (supported/configured/enabled/primary) plus structured “recent revalidation required” targeting.
  • Adds server-owned TOTP enrollment endpoint that persists short-lived pending enrollment state and allows /totp confirmation without resubmitting the secret.
  • Expands PHPUnit coverage for permissions, invariant enforcement, revalidation responses, missing-user handling, and TOTP enrollment lifecycle.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
class-two-factor-core.php Registers provider-settings REST routes, adds shared REST permission helpers, and extracts provider-settings persistence into reusable helpers (including session invalidation semantics).
providers/class-two-factor-totp.php Adds /totp/enrollment REST endpoint and pending enrollment state handling; allows /totp setup to confirm server-owned enrollment without a client-supplied key.
tests/class-two-factor-rest-api.php New test suite covering provider-settings REST API behavior, invariants, redaction, permissions, revalidation targeting, and session semantics.
tests/providers/class-two-factor-totp-rest-api.php Adds tests for server-owned TOTP enrollment begin/confirm, replacement, expiry, replay prevention, and consistent missing-user behavior.
tests/providers/class-two-factor-backup-codes-rest-api.php Adds regression tests ensuring nonexistent users are rejected before recovery-code generation/mutation.
TESTS.md Documents the new REST API test class and expanded TOTP REST coverage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread providers/class-two-factor-totp.php Outdated
@chubes4

chubes4 commented Jul 24, 2026

Copy link
Copy Markdown
Author

External concurrency fuzz follow-up

A dedicated same-user writer campaign ran against head a3f28d0 after the WP Codebox concurrent-wrapper fix landed. The harness and artifacts remain external to this PR.

  • WordPress 7.0.2 multisite runtime
  • PHP 8.5.8 and PHP 8.4.23
  • Two independent runs of 224 mutations at concurrency 16 (448 concurrent operations total)
  • Per run: 80 provider-state updates, 48 TOTP enrollment starts, 48 confirmations of one pending enrollment, and 48 recovery-code replacements
  • Mutations targeted one user on the primary site; the earlier deterministic matrix covers cross-site multisite behavior

Both runs passed all invariants:

  • final provider state matched one complete accepted request
  • final pending enrollment matched exactly one complete returned secret
  • exactly one concurrent confirmation succeeded; 47 lost races returned bounded 400 responses, with no 500s
  • confirmed TOTP state was configured and pending enrollment was removed
  • final recovery-code hashes matched exactly one complete returned plaintext set, with no torn or mixed write
  • no command failures, PHP fatals, wrapper collisions, or state-integrity failures occurred

This brings the external campaign total to 160,473 operations. No additional PR-specific defect was found.

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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

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

Labels

None yet

Projects

None yet

2 participants