Skip to content

Let providers declare their secrets; move secrets logic behind an injectable store - #3

Merged
ericmann merged 3 commits into
secrets-api-totpfrom
secrets-api-provider-secrets
Oct 9, 2026
Merged

ericmann merged 3 commits into
secrets-api-totpfrom
secrets-api-provider-secrets

Conversation

@ericmann

@ericmann ericmann commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Stacked on WordPress#1010 (base: secrets-api-totp). It answers two review comments from @kasparsd there:

It is a refactor: no change to what is stored or where, and no change to login behaviour.

1. An instance class behind the static facade

Two_Factor_Secrets was all static, so tests reached into it through a public $test_overrides array and the two_factor_secrets_api_present filter, and most of them needed the real Secrets API loaded.

  • Two_Factor_Secrets_Store is a small interface for where secrets are physically kept.
  • Two_Factor_Secrets_Api_Store implements it and is now the only code that calls the Secrets API.
  • Two_Factor_Secrets_Manager holds the logic and is constructed with a store.
  • Two_Factor_Secrets keeps every public static method and forwards to a shared manager, so callers are unchanged.

Tests install an in-memory store. $test_overrides and the two_factor_secrets_api_present filter are removed, and the secrets tests run with or without the feature plugin. A small test class still runs against the real store, so the in-memory one can't drift unnoticed.

2. Providers declare their secrets

All Secrets API handling for TOTP lived in Two_Factor_Totp, and the CLI called that class by name. A custom provider got none of it.

  • Providers declare secrets with one static method, modelled on uninstall_user_meta_keys():

    public static function user_secret_meta_keys() {
        return array( 'totp' => '_two_factor_totp_key' ); // slug => user meta key
    }
  • The storage logic (verified writes, lazy one-way migration, deletion, storage state) moves into the manager, generalised over a slug and a meta key.

  • Two_Factor_Secrets_Lifecycle handles deletion with the user and on uninstall, the unreachable-secrets notice, Site Health and the counts, for every declared secret. It is registered from Two_Factor_Core::add_hooks(), and Two_Factor_Totp::register_secret_lifecycle_hooks() is gone.

  • Two_Factor_Totp's public methods keep their signatures and become wrappers. The file drops by about 500 lines.

  • wp two-factor secrets migrate covers every declared secret, and secrets status prints one row per secret with a new secret column.

What stays the same

Design points worth a look

  • Providers turned off site-wide. The enabled-providers setting is enforced by a two_factor_providers filter that removes them, but a disabled provider may still hold users' secrets. Two_Factor_Core::get_provider_secrets() sets a flag that the enforcement filter checks and stands aside for. No provider is instantiated.
  • Slugs share one namespace and are part of the stored secret's name. A duplicate is refused with _doing_it_wrong() and the first declaration wins. A slug must not change once secrets exist.
  • Values are single strings. A provider with several keys serialises them itself.
  • TOTP is the only built-in consumer. Email tokens and backup codes are stored hashed, so the second consumer here is a test fixture (Two_Factor_Dummy_Secret).

Not done here

  • Admin-facing copy (Site Health, the admin notice, the settings section) still says "authenticator app secrets".
  • No --provider filter on secrets migrate.

Renames

All of these were introduced in WordPress#1010 and have never been released:

  • Site Health test ID: two_factor_totp_secret_storage → two_factor_secret_storage
  • Transient: two_factor_totp_affected_users → two_factor_secrets_affected_users
  • secrets migrate log lines now name the secret, e.g. User 12 (totp): …

Testing

PHPCS, PHPStan and both PHPUnit suites pass locally (453 tests each, single site and multisite). New tests cover the real store, the registry (including disabled providers, invalid and duplicate declarations) and core handling a second provider's secret through migration, user deletion, uninstall, Site Health and the CLI.

Open WordPress Playground Preview

Two_Factor_Secrets was all static, so tests reached into it through a
public $test_overrides array and the two_factor_secrets_api_present
filter, and most of them needed the real Secrets API loaded.

The logic now lives in Two_Factor_Secrets_Manager, which is constructed
with a Two_Factor_Secrets_Store. Two_Factor_Secrets_Api_Store is the only
code that calls the Secrets API. Two_Factor_Secrets keeps every public
static method, forwarding to a shared manager, so callers are unchanged.

Tests install an in-memory store instead, which removes $test_overrides
and the filter and lets the secrets tests run without the feature plugin.
A small set of tests still exercises the real store.
All Secrets API handling for TOTP lived in Two_Factor_Totp, and the CLI
called that class by name, so a custom provider got none of it.

Providers now declare the user meta keys that hold secrets through the
static Two_Factor_Provider::user_secret_meta_keys(). The storage logic
(verified writes, lazy one-way migration, deletion, storage state) moves
into Two_Factor_Secrets_Manager, generalised over a slug and a meta key.
Two_Factor_Secrets_Lifecycle handles deletion with the user and on
uninstall, the unreachable-secrets notice, Site Health and the counts for
every declared secret, registered from Two_Factor_Core::add_hooks().

Two_Factor_Totp's public methods keep their signatures and become
wrappers. Its slug, marker key and secret name are unchanged, so nothing
already stored moves. Two_Factor_Totp::register_secret_lifecycle_hooks()
is removed.

Two_Factor_Core::get_provider_secrets() lists secrets from every provider
class, including ones turned off site-wide, without instantiating any.
A duplicate slug is refused.

wp two-factor secrets migrate covers every declared secret, and secrets
status prints one row per secret.
@github-actions

github-actions Bot commented Oct 6, 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: ericmann <ericmann@git.wordpress.org>

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

@ericmann
ericmann merged commit f7628c1 into secrets-api-totp Oct 9, 2026
25 checks passed
@ericmann
ericmann deleted the secrets-api-provider-secrets branch October 9, 2026 19:49
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