Skip to content

feat: two-factor authentication (TOTP + recovery codes) - #71

Merged
mpge merged 1 commit into
mainfrom
feat/two-factor-auth
Aug 1, 2026
Merged

feat: two-factor authentication (TOTP + recovery codes)#71
mpge merged 1 commit into
mainfrom
feat/two-factor-auth

Conversation

@mpge

@mpge mpge commented Aug 1, 2026

Copy link
Copy Markdown
Member

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_factors table (one row per user, UNIQUE(user_id)), added to the activator alongside api_tokens and modeled with the same static $wpdb helper 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 how api_tokens hashes 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()):

Method Route Purpose
GET /admin/two-factor status (enabled / pending / recovery codes remaining)
POST /admin/two-factor/setup generate secret + recovery codes; returns secret, otpauth:// URI, plain codes
POST /admin/two-factor/confirm confirm enrollment with the first TOTP code
POST /admin/two-factor/verify challenge: verify a TOTP or single-use recovery code
POST /admin/two-factor/recovery-codes regenerate the recovery-code set
DELETE /admin/two-factor disable and remove 2FA

Files

  • 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 (extends Base_Controller).
  • includes/class-activator.phpescalated_two_factors table.
  • 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.

Existing installs must reactivate the plugin (or bump the version) to create the new table via maybe_upgrade().

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
mpge merged commit f9e4845 into main Aug 1, 2026
3 checks passed
@mpge
mpge deleted the feat/two-factor-auth branch August 1, 2026 14:29
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>
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.

1 participant