Skip to content

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.

$mailer->send(\WP_User $user, string $code, int $duration_minutes = 5): bool

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).