feat: two-factor authentication (TOTP + recovery codes) - #71
Merged
Conversation
Port the Escalated Laravel two-factor implementation to the WordPress plugin backend. TOTP follows RFC 6238 (HMAC-SHA1 over a base32-decoded secret, dynamic truncation) implemented in pure PHP with no external Composer dependency. Storage mirrors the api_tokens conventions in a new escalated_two_factors table (one row per user): - secret: AES-256-CBC encrypted at rest (key derived from WP salts), reversible because the raw secret is needed to compute codes. - recovery_codes: JSON array of SHA-256 hashes; plain codes are shown once at generation and consumed single-use on verification. - confirmed_at: enrollment confirmation timestamp. Self-service REST routes under escalated/v1/admin/two-factor act on the authenticating token's user: status, setup (secret + otpauth URI + recovery codes), confirm, verify (TOTP or recovery challenge), regenerate-recovery, and disable. Tests cover secret generation, RFC 6238 test vectors, correct/incorrect code verification, single-use recovery codes, at-rest encryption, and the full REST setup/confirm/verify/disable flow.
mpge
added a commit
that referenced
this pull request
Aug 1, 2026
Port the Escalated Laravel two-factor implementation to the WordPress plugin backend. TOTP follows RFC 6238 (HMAC-SHA1 over a base32-decoded secret, dynamic truncation) implemented in pure PHP with no external Composer dependency. Storage mirrors the api_tokens conventions in a new escalated_two_factors table (one row per user): - secret: AES-256-CBC encrypted at rest (key derived from WP salts), reversible because the raw secret is needed to compute codes. - recovery_codes: JSON array of SHA-256 hashes; plain codes are shown once at generation and consumed single-use on verification. - confirmed_at: enrollment confirmation timestamp. Self-service REST routes under escalated/v1/admin/two-factor act on the authenticating token's user: status, setup (secret + otpauth URI + recovery codes), confirm, verify (TOTP or recovery challenge), regenerate-recovery, and disable. Tests cover secret generation, RFC 6238 test vectors, correct/incorrect code verification, single-use recovery codes, at-rest encryption, and the full REST setup/confirm/verify/disable flow. Co-authored-by: Matt Gros <mpge@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports the Escalated Laravel two-factor implementation to the WordPress plugin backend. No 2FA existed here before (API tokens were the only auth extension); laravel/rails/django already ship full TOTP.
TOTP follows RFC 6238 — HMAC-SHA1 over a base32-decoded secret with dynamic truncation — implemented in pure PHP (
hash_hmac) with no external Composer dependency, matching the Laravel reference algorithm.How it's stored
New
escalated_two_factorstable (one row per user,UNIQUE(user_id)), added to the activator alongsideapi_tokensand modeled with the same static$wpdbhelper pattern:secret— AES-256-CBC encrypted at rest (key derived from WordPress salts). Reversible because the raw base32 secret is required to compute codes on each verification.recovery_codes— JSON array of SHA-256 hashes (one-way, mirroring howapi_tokenshashes tokens). Plain codes are surfaced to the user exactly once at generation and are single-use — a matched code is removed from the set on verification.confirmed_at— enrollment confirmation timestamp.REST surface
Self-service routes under
escalated/v1/admin/two-factor, each acting on the authenticating Bearer token's user (you can only manage your own 2FA — mirrors the Laravel controller operating on$request->user()):GET/admin/two-factorPOST/admin/two-factor/setupPOST/admin/two-factor/confirmPOST/admin/two-factor/verifyPOST/admin/two-factor/recovery-codesDELETE/admin/two-factorFiles
includes/Services/TwoFactorService.php— RFC 6238 TOTP (secret gen, otpauth URI, verify with ±1 period drift, recovery-code gen).includes/Models/TwoFactor.php— table access, encryption/hashing, single-use recovery consumption.includes/Api/class-two-factor-controller.php— REST controller (extendsBase_Controller).includes/class-activator.php—escalated_two_factorstable.includes/Api/class-api-bootstrap.php— controller registration.tests/Test_Two_Factor.php— 20 tests.Tests
tests/Test_Two_Factor.php(20 tests) proves: secret generation shape, RFC 6238 published test vectors, correct-code verification + clock-drift tolerance, wrong/malformed-code rejection, single-use recovery codes, at-rest encryption (plaintext secret/codes never hit the DB column), and the full REST setup → confirm → verify → disable flow.Full plugin suite green locally (378 tests, 975 assertions, pre-existing skips only). Pint clean.