Add REST provider management and server-owned TOTP enrollment - #935
Add REST provider management and server-owned TOTP enrollment#935chubes4 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. |
There was a problem hiding this comment.
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
/totpconfirmation 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.
External concurrency fuzz follow-upA dedicated same-user writer campaign ran against head
Both runs passed all invariants:
This brings the external campaign total to 160,473 operations. No additional PR-specific defect was found. |
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_usercapability 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>/providersreturns:user_idproviders: stable providerkeyandlabel, plussupported,configured,enabled,primary, and nullable recovery-coderemainingstaterevalidation: structuredrequired,action, andurlvalues targeting the existingrevalidate_2faflowIt never returns configured TOTP secrets, email tokens, recovery-code hashes, or raw user meta.
POST|PUT|PATCH /two-factor/1.0/users/<user_id>/providersaccepts 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/enrollmentacceptsuser_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/totproute withuser_idandcode, omittingkey. 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-suppliedkeybehavior remains available for backward compatibility.Security decisions
edit_userfor the target.Backward compatibility
/totpPOST/DELETE behavior is retained./generate-backup-codesbehavior is retained.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:
Checks run:
npm run buildpassesnpm run lint:jspassesnpm run lint:csspassesnpm run lint:phpstanpassescomposer lint-compatpasses for PHP 7.2+git diff --checkpassnpm testcould not run locally because the required Docker-backedwp-envcould not connect to Docker on the execution host. CI should run the PHPUnit suite. Repository-widenpm run lintreaches the documented pre-existing PHPCS failures (#437); no new violations are reported in the modified provider or REST tests.Changelog Entry
External Fuzz Evidence
An isolated Homeboy rig fuzzed this REST surface without adding campaign harnesses or artifacts to the PR:
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
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.