WP Manifestindependent plugin directory
manifest / users / wp-2fa-email-login

Email Code Login

Add a 'login with email' option to Wordpress

by ssamjh · github.com/ssamjh/wp-2fa-email-login

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/ssamjh/wp-2fa-email-login/archive/refs/heads/main.zip

Passwordless login for WordPress using a one-time 6-digit code sent by email.

The email contains no links at all, only the code as plain text. Link scanners such as Microsoft Defender Safe Links cannot follow and consume a code that isn't a link, which is the failure mode that breaks most magic-link plugins.

How it works

  1. The user enters their email address or username on the login screen.
  2. If the account exists, a 6-digit code is emailed to them.
  3. They type the code in. On success they are logged in.

Codes expire after 10 minutes, can be used once, and are stored hashed.

Install

Copy email-code-login.php into wp-content/mu-plugins/, creating that directory if it does not already exist.

That is the whole install. Must-use plugins load automatically, so there is no activation step and nothing to switch on. The code form replaces the password form at /wp-login.php as soon as the file is in place.

Two things to know about mu-plugins:

  • The file must sit directly in mu-plugins/, not in a subdirectory. WordPress only loads PHP files at the top level of that directory.
  • It cannot be deactivated from wp-admin. It appears under Plugins > Must-Use as a read-only entry. To disable it you delete or rename the file over SFTP, SSH, or your host's file manager.

Being undeactivatable is the point: a must-use plugin cannot be switched off by an attacker who gets into wp-admin, and it cannot be turned off by accident. It does mean you need file access to remove it, so keep the fallback below in mind.

Password login is still available

Password login stays reachable at:

/wp-login.php?use_password=1

Nothing on that page is gated by a captcha. This is the deliberate escape hatch: if email delivery breaks, or a captcha provider goes down, administrators can still get in.

This matters more as a must-use plugin than it would as a regular one. Since you cannot deactivate the plugin from wp-admin to recover, ?use_password=1 is your first line of recovery, and file access is the second.

Settings

There is no settings page. Everything is configured with constants in wp-config.php.

This is intentional. Constants cannot be edited through wp-admin, so an attacker who compromises an administrator account still cannot disable the captcha or loosen the rate limits.

Captcha

WordPress core ships no captcha of its own, so the default here is a built-in honeypot that uses only core functions, makes no external requests, needs no API keys, and sends nothing about your users to a third party.

define( 'ECL_CAPTCHA', 'honeypot' );  // default
define( 'ECL_CAPTCHA', 'none' );
define( 'ECL_CAPTCHA', 'turnstile' ); // Cloudflare Turnstile
define( 'ECL_CAPTCHA', 'hcaptcha' );  // hCaptcha
define( 'ECL_CAPTCHA', 'recaptcha' ); // Google reCAPTCHA v2 checkbox

The three hosted providers also need keys:

define( 'ECL_CAPTCHA_SITE_KEY', '...' );
define( 'ECL_CAPTCHA_SECRET',   '...' );

If a hosted provider is selected but no keys are defined, the plugin falls back to the honeypot and logs why, rather than silently rejecting every login attempt.

The honeypot stops commodity form-spam bots. It is weaker than a hosted challenge against someone who has actually looked at your HTML. If you are seeing targeted abuse rather than background spam, use Turnstile.

Tuning

These are class constants near the top of the plugin file. Changing them means editing the file.

Constant Default Purpose
TTL 600 Code lifetime in seconds
MAX_TRIES 5 Wrong guesses before the code is burned
USER_LIMIT 3 Codes per account per window
IP_LIMIT 10 Codes per IP per window
WINDOW 900 Rate limit window in seconds
MIN_FILL 3 Honeypot minimum seconds before submit

Security notes

Captchas fail closed. If the verifier is unreachable or returns something unreadable, the request is rejected rather than waved through. A challenge that lets everyone past when the verifier is down is a challenge an attacker can arrange to have down. Transient failures get one retry first. Password login covers the lockout risk.

No account enumeration. The request form shows the same screen whether or not the account exists.

Rate limiting behind a proxy. ip() reads REMOTE_ADDR. If the site sits behind Cloudflare or another reverse proxy, that is the proxy address, which collapses the per-IP limits onto a handful of addresses. Fixing this means reading the proxy's own header, but only after verifying the request genuinely came from the proxy. Do not trust X-Forwarded-For blindly.

Remember me is offered on the code entry form and is ticked by default. Untick it on a shared machine. To change the default, see the comment above $remember in screen_verify().

Requirements

WordPress 5.0 or later, PHP 7.0 or later. The site must be able to send email.

Licence

GPLv2 or later, matching WordPress. The full text is in LICENSE.