Two-factor authentication for WordPress via SMS, email, or an authenticator app. Supports Twilio, Vonage, and AWS SNS through a swappable provider interface — no bundled SDKs, built entirely on the WordPress HTTP API.
- Authenticator app (TOTP) support — Google Authenticator, Authy, 1Password, etc. — with a locally-rendered QR code, no SMS cost and no third-party API involved
- SMS one-time password (OTP) on login, sent via Twilio, Vonage, or AWS SNS
- Pluggable provider interface — add a new SMS provider by implementing one PHP interface
- Per-user opt-in and admin-enforced 2FA by role (Administrator, Editor, etc.)
- Secure interim session between password verification and OTP verification
- Bcrypt-hashed OTP storage — codes are never stored in plain text
- AES-256-GCM authenticated encryption for API credentials and the TOTP secret at rest
- Rate limiting with configurable lockout after repeated failed attempts
- Resend-code cooldown timer
- Phone number verification flow on profile setup
- Custom country picker (flag + dial code) for phone number fields
- One-time backup codes for account recovery if a phone is lost
- Emergency break-glass bypass (
wp-config.phpconstant + WP-CLI) if SMS delivery fails site-wide - Email OTP fallback for users without a verified phone (voluntary opt-in or admin-enforced by role)
- Audit log of logins, failed attempts, lockouts, and 2FA changes, visible to admins
- 2FA status column and a per-user "Enforce 2FA" bulk action on the Users list
- Security alert emails when 2FA/phone settings change, so a user notices if an attacker disables their 2FA
- Per-IP rate limiting, in addition to per-user, to catch credential-stuffing across many accounts
- "Remember this device" for 30 days, with a profile-page "Forget All Devices" control
- First-run setup checklist and a 2FA adoption widget on the settings page
- Paste a full international number (e.g.
+442071234567) into any phone field and the country is auto-detected - Built-in support for WordPress's personal data export and erase tools (Tools → Export/Erase Personal Data)
- Zero third-party PHP SDKs — just
wp_remote_post()andwp_mail()
- WordPress 6.0+
- PHP 7.4+
- A Twilio, Vonage, or AWS SNS account if you want SMS delivery — not required if you only offer the authenticator app or email fallback
- Clone or download this repository into
wp-content/plugins/smsentry - Activate SMSentry from the WordPress Plugins screen
- Go to SMSentry → SMS Provider and enter your Twilio or Vonage credentials
- Use the Test & Validate tab to confirm delivery works end-to-end
- Visit any user's Profile page and verify a phone number to enable 2FA for that account
- User submits username + password on the normal WordPress login form
- WordPress authenticates the credentials as usual
- If the user has 2FA enabled (or their role requires it), SMSentry intercepts the
authenticatefilter and challenges them via their configured method — an authenticator app code (nothing to send), an SMS OTP, or an email OTP - The user is redirected to a verification screen (still inside
wp-login.php) - On a correct code, login completes; incorrect codes are rate-limited and the session locks out after a configurable number of attempts
Instead of a code, a user can also enter one of their backup codes generated on the profile page — useful if their phone or authenticator device is lost.
The strongest and cheapest method: a standard 30-second TOTP code (RFC 6238), compatible with Google Authenticator, Authy, 1Password, Microsoft Authenticator, or any other authenticator app. No SMS/email is sent — both sides derive the same code from a shared secret and the current time, so there's no delivery cost and it works with no signal or internet on the device.
- Set up from the profile page (Profile → Two-Factor Authentication → Set Up Authenticator App): scan the QR code, or enter the secret manually, then confirm with a generated code.
- The QR code is rendered entirely in the browser from a locally-bundled library (
assets/js/vendor/qrcode.js) — the secret is never sent to a third-party QR image API. - Setup isn't saved until confirmed with a real code, so a missed scan or typo can't silently leave the account without a working second factor.
- The secret is encrypted (AES-256-GCM) before being stored, the same way provider API credentials are.
- Takes priority over SMS/email when more than one method is configured on an account — it costs nothing per login and doesn't depend on delivery working.
- Backup codes and "remember this device" both work the same way with TOTP as with SMS/email.
Once a phone is verified, users can generate 10 one-time backup codes from their profile page (Profile → Backup Codes). Each code:
- Is shown in plain text exactly once, immediately after generation — only a bcrypt hash is stored afterward
- Can be used exactly once, as a drop-in replacement for the SMS code on the login screen ("Use a backup code instead")
- Is invalidated (along with the rest of the set) if the user regenerates the list
Two recovery mechanisms exist for when SMS delivery is broken or a user is otherwise locked out:
Site-wide bypass — add to wp-config.php:
define( 'SMSENTRY_DISABLE_2FA', true );This disables all 2FA enforcement immediately, without touching the database. Useful when the SMS provider account is suspended/out of credit and admins can't log in at all.
Per-user reset via WP-CLI:
wp smsentry list # show every user with 2FA configured
wp smsentry reset <user> # clear 2FA for one user (ID, login, or email)
wp smsentry reset --all # clear 2FA for every user on the siteFor users without a verified phone number, SMSentry can deliver the OTP by email instead, via wp_mail():
- Voluntary: from the profile page, a user without a phone can click "Use Email Instead" to enable email-based codes immediately (no verification needed — it's the account's existing, already-trusted email).
- Admin-enforced: if a role is marked "Require 2FA" but a user under that role hasn't verified a phone yet, they're automatically challenged via email instead of being skipped entirely.
- Verifying a phone number later automatically switches the account back to SMS.
- Controlled site-wide via SMSentry → Security → Email Fallback (on by default).
A dedicated wp_smsentry_audit_log table records logins, failed code attempts, lockouts, phone/email 2FA changes, and backup code usage, with timestamp, user, event type, free-text details, and IP address. View and filter it under SMSentry → Audit Log. Entries older than 90 days are pruned automatically via WP-Cron (configurable with the smsentry_audit_log_retention_days filter).
The Users list table shows a 2FA column (method + an "Enforced" badge) for every user. Select one or more users and use the bulk actions dropdown:
- Enforce 2FA — requires 2FA for these specific users on their next login, regardless of role
- Remove 2FA enforcement — undoes it
This complements role-based enforcement (Security tab) for one-off cases — e.g. a single contractor account that needs 2FA without making it mandatory for their entire role.
In addition to the per-user lockout, failed code attempts are also tracked per IP address. The IP lockout threshold is max_attempts × 4 (using whatever you've set on the Security tab), high enough to tolerate several genuine users behind shared NAT/Wi-Fi, but low enough to stop an attacker spraying codes across many different accounts from a single IP — something per-user limiting alone can't catch.
When a user's phone is verified, 2FA is enabled/disabled, email 2FA is toggled, backup codes are regenerated, or the account is locked out, SMSentry emails the account owner. This closes the gap where an attacker who already has a password could quietly turn off 2FA or swap the phone number without the real user finding out. Toggle this under SMSentry → Security → Security Alert Emails (on by default).
After a successful code entry, a user can check "Trust this device for 30 days" to skip the code on that browser next time. A random token is stored in a cookie; only its SHA-256 hash lives in user meta, so the cookie itself is the only proof of trust. Users can revoke all trusted devices from their profile page ("Forget All Devices"), and admins can disable the feature site-wide under SMSentry → Security → Remember Device.
A dismissible checklist appears above the settings tabs until you've added provider credentials and sent a successful test message. The Security tab also shows a live breakdown of 2FA adoption — how many users have it active (by method, including the authenticator app), and how many users under a required role still haven't set it up.
SMSentry registers with WordPress's built-in privacy tools (Tools → Export Personal Data / Erase Personal Data):
- Export includes the account's phone number, which 2FA methods are enabled, how many trusted devices are remembered, and its audit log activity (logins, failures, lockouts, 2FA changes).
- Erase removes the phone number, backup codes, TOTP secret, email-2FA flag, and trusted devices — effectively disabling 2FA on the account, as if the user had removed it themselves.
- Audit log entries are intentionally not erased. If an attacker briefly compromised an account, letting that same access wipe the audit trail via an erasure request would destroy the evidence of the compromise. Entries still expire on their own via the normal retention/pruning policy (90 days by default).
smsentry/
├── smsentry.php # Bootstrap
├── uninstall.php # Cleanup on uninstall
├── includes/
│ ├── class-plugin.php # Singleton — wires everything together
│ ├── class-authenticator.php # OTP generate / hash / verify (SMS, email, TOTP)
│ ├── class-totp.php # RFC 6238 TOTP: secret gen, provisioning URI, verify
│ ├── class-privacy.php # WP personal-data export/erase integration
│ ├── class-session.php # Interim cookie session
│ ├── class-rate-limiter.php # Attempt tracking + lockout
│ ├── class-crypto.php # AES-256-GCM for stored secrets
│ ├── class-countries.php # Country/dial-code dataset + picker UI
│ ├── class-cli-command.php # WP-CLI: list / reset (emergency recovery)
│ ├── class-audit-log.php # Audit log table, queries, pruning
│ ├── class-stats.php # 2FA adoption summary (cached 5 min)
│ ├── class-notifier.php # Security alert emails on audit events
│ ├── class-device-trust.php # "Remember this device" cookie/meta logic
│ └── providers/
│ ├── interface-sms-provider.php
│ ├── class-twilio-provider.php
│ ├── class-vonage-provider.php
│ └── class-aws-sns-provider.php
├── admin/
│ ├── class-admin.php # Settings page, test SMS, validate
│ ├── class-user-profile.php # Profile 2FA section + AJAX
│ ├── class-users-list.php # 2FA column + bulk enforce/unenforce
│ └── views/
├── public/
│ ├── class-login-handler.php # authenticate / wp_login_failed hooks
│ └── views/
└── assets/
├── css/smsentry.css
└── js/
├── smsentry.js
└── vendor/qrcode.js # Client-side QR rendering (MIT, Kazuhiko Arase)
Implement SMSentry_SMS_Provider:
interface SMSentry_SMS_Provider {
public function send( string $to, string $message ): true|WP_Error;
public function validate_credentials(): true|WP_Error;
public function get_name(): string;
public function get_label(): string;
}Then wire it up in SMSentry_Plugin::resolve_provider().
- OTPs and backup codes are hashed with
password_hash()before being stored — never logged or stored in plain text - API credentials (Twilio Auth Token, Vonage API Secret, AWS Secret Key) and the TOTP secret are encrypted with AES-256-GCM (authenticated — tamper-evident, unlike plain CBC) using a key derived from the site's
AUTH_KEY - TOTP codes are verified against the RFC 6238 algorithm with a tolerance of ±1 time step (30s) for clock drift; the QR code used for setup is rendered entirely client-side, so the secret never passes through a third-party image API
- OTP entry is rate-limited per user and per IP; a configurable number of failed attempts locks out either
- All AJAX endpoints are nonce-protected and capability-checked
- Audit log entries are never deletable via the WP personal-data eraser, so a malicious actor can't use a data-erasure request to wipe evidence of an attack
- Trusted-device cookies store a random token client-side; only its SHA-256 hash is kept server-side, so nothing useful is exposed if the database leaks
- 2FA configuration changes trigger an email to the account owner, so a password-only compromise can't silently disable 2FA unnoticed
GPL-2.0-or-later. See readme.txt for the full WordPress.org plugin description.