Architecture¶
Class overview¶
src/
├── Plugin.php Bootstrap singleton
├── OtpSettings.php Settings reader
├── OtpStore.php OTP generation & validation (Transients)
├── OtpMailer.php Email delivery
├── OtpException.php Custom exception type
├── WpLoginProvider.php Traditional WP login flow
├── Admin.php WP Admin settings page
├── GraphQlProvider.php GraphQL mutations (requestOtp)
└── OtpGraphQlProviderConfig.php AxeWP provider integration
Class responsibilities¶
Plugin¶
Singleton bootstrapped on the plugins_loaded action. Instantiates all services, wires them together, and calls register_hooks() on each. Nothing else lives here.
plugins_loaded
└── Plugin::instance()
├── new OtpStore()
├── new OtpMailer()
├── new WpLoginProvider(store, mailer) → register_hooks()
├── new Admin() → register_hooks() [is_admin() only]
└── new GraphQlProvider(store, mailer) → register_hooks() [if AxeWP active]
└── new OtpGraphQlProviderConfig() → registered via filter
OtpSettings¶
Reads wp_otp_settings from the WordPress options table. Provides clamped, validated values with safe defaults. No dependencies on AxeWP.
$settings = new OtpSettings();
$settings->duration_minutes // int 1–60 (default 5)
$settings->code_length // int 4–12 (default 6)
$settings->charset // string 'numeric' | 'alpha' | 'alphanumeric'
$settings->max_attempts // int 1–10 (default 5)
$settings->logo_url // string absolute URL or '' (default '')
$settings->logo_height // int 20–500 px (default 50)
$settings->ttl_seconds() // int duration_minutes × 60
OtpStore¶
The only class that touches the WordPress Transients API. Generates and validates one-time codes. Redis-compatible (Transients are automatically backed by Redis when an object cache is active).
Transients written per OTP:
| Key | Value | Purpose |
|---|---|---|
wp_otp_{token} | { user_id, code_hash, attempts, max_attempts, expires_at } | WP login flow — token stored in HttpOnly cookie |
wp_otp_user_{user_id} | token | GraphQL flow — lookup by email |
wp_otp_rate_{user_id} | { count, expires_at } | Rate limiting — max 10 requests per 10-minute window |
Both OTP transients are deleted together on successful validation (one-time use), and share the same TTL. max_attempts and expires_at are snapshotted at generation time so that changing settings mid-flight does not affect in-progress tokens. On each failed attempt, attempts is incremented and the transient is re-saved with the remaining TTL. Once attempts >= max_attempts, both transients are revoked immediately. When a new OTP is generated for a user who already has one in flight, the existing token is revoked first.
Public API:
$store->generate(int $user_id, OtpSettings $settings): array
// Returns [ 'token' => string, 'code' => string ]
$store->validate(string $token, string $code): int
// Returns user_id. Used by WP login flow.
$store->validate_for_email(string $email, string $code): int
// Returns user_id. Used by GraphQL flow.
$store->revoke(string $token): void
// Deletes both transients early (e.g. on mail failure).
OtpMailer¶
Sends the OTP code via wp_mail. Exposes three filter hooks for customisation. See hooks-and-filters.md.
OtpException¶
A RuntimeException subclass. Used to signal both expected failures (expired token) and unexpected ones (entropy failure). Callers differentiate via context, not exception type.
WpLoginProvider¶
Hooks into wp-login.php to replace the password flow with OTP. See wp-login-flow.md for the full two-phase flow.
Hooks registered: - authenticate (priority 15) — phase 1: intercept login, send OTP - login_init — phase 2: validate OTP, set auth cookie - login_form — inject OTP code input - login_head — output CSS/JS to hide/show form fields - login_errors — generic error message
Admin¶
Registers a WordPress settings page under Settings → WP OTP. Writes to the wp_otp_settings option, which OtpSettings reads. Uses the WordPress Settings API.
GraphQlProvider¶
Registers the requestOtp GraphQL mutation with WPGraphQL. Handles the mutateAndGetPayload callback. See graphql-flow.md.
OtpGraphQlProviderConfig¶
Extends AxeWP's abstract ProviderConfig class. Registers the OTP provider with AxeWP's ProviderRegistry via the graphql_login_registered_provider_configs filter, enabling the login(provider: OTP, ...) mutation.
Dependency graph¶
Plugin
├── OtpSettings (no deps — reads WP option)
├── OtpStore (depends on OtpSettings, OtpException)
├── OtpMailer (no deps — uses wp_mail)
├── WpLoginProvider (depends on OtpStore, OtpMailer, OtpSettings)
├── Admin (depends on OtpSettings)
└── GraphQlProvider (depends on OtpStore, OtpMailer, OtpSettings)
└── OtpGraphQlProviderConfig (depends on OtpStore, OtpException — AxeWP required)
AxeWP is an optional dependency. OtpGraphQlProviderConfig is only loaded when WPGraphQL\Login\Auth\ProviderConfig\ProviderConfig exists (checked in Plugin).