Skip to content
Plugin PantryPlugins

Documentation / Pantry Login

Pantry Login

Contents: Limit login attempts and move the login page.

Limit login attempts and move the login page.

Pantry Login counts failed logins, locks out the address that made them, stops the form from confirming which usernames exist, serves the login form at an address of your choosing, and puts your own logo above it.

Settings

Plugin Pantry > Login. Everything lives on one screen, in one option (pantry_login_settings).

Failed attempts

Setting Default What it does
Lockout On Turns the counting and the lockout on or off. Everything else in this section still applies to the message only when this is off.
Attempts allowed 5 Failed attempts before the address is locked out.
Counting window 15 minutes How long a failed attempt is remembered. Each new failure restarts the window.
Lockout length 15 minutes How long the address is refused for.
Remaining attempts On Adds "You have 3 attempts left" to the error on the form.
Error message On Replaces "unknown username" and "the password you entered for the username x is incorrect" with one message that says neither.
Proxy header Off Reads the visitor address from the first entry in X-Forwarded-For instead of REMOTE_ADDR. Only switch this on when a proxy or CDN in front of the site sets that header, because anyone can send it.

Two counters run at once: one for the address, one for the address and username together. Whichever reaches the limit first starts the lockout, and both the address and that username are refused until it ends. A successful login clears both counters for that address. Counters and lockouts are transients, so they expire on their own and they never grow the database.

Login address

Setting Default What it does
Custom address Off Serves the login form at your own address.
Address my-login One path segment. Saved through sanitize_title(), so My Login becomes my-login.

With this on, https://example.com/my-login/ shows the login form, and wp-login.php and wp-admin return a 404 to anyone who is not logged in. WordPress emits the new address everywhere it would have printed wp-login.php: wp_login_url(), wp_logout_url(), wp_lostpassword_url(), the form action and the links in emails. Logging out, password-protected posts and password-reset links that carry a key still work through wp-login.php itself, because those arrive from links that were sent before the address changed.

Save the screen before you log out, and keep the address somewhere safe. If you lose it, add this to wp-config.php, load wp-login.php, and remove it again:

add_filter( 'option_pantry_login_settings', function ( $settings ) {
	$settings['hide_login_enabled'] = false;
	return $settings;
} );

The address is refused if it is empty, if WordPress reserves it (wp-admin, wp-json and similar), or if a published post or page already uses it. In that case the setting is switched back off and the screen says why.

Login form logo

Setting Default What it does
Logo none Image shown above the login form instead of the WordPress logo. The link points at your home page and its text is the site name.
Logo width 84px Width in pixels. The height follows the image's own proportions.

Nothing is printed on the login page unless a logo is set.

Lockout log

The last 50 lockouts are listed under the settings, newest first: when, the address with its last octet (or last hextet) replaced by xxx, the username that was tried, the attempts counted and how long the lockout was. The whole log is one option, pantry_login_log, and it never holds more than 50 entries.

Two buttons sit under the table. Release every lockout deletes the lockouts and counters for every entry in the log, which is how you let someone back in before their lockout ends. Clear the log empties the table.

Hooks

pantry_login_locked

Fires when an address is locked out. The array holds the masked address, the username that was tried, the attempts counted, the lockout length in minutes and the timestamp it ends.

add_action( 'pantry_login_locked', function ( array $event ) {
	error_log( sprintf( '%s locked out after %d attempts on %s', $event['ip'], $event['attempts'], $event['username'] ) );
} );

pantry_login_settings_after

Fires at the end of the settings screen, below the lockout log, with the settings screen object as its only argument. An add-on prints its own section here.

add_action( 'pantry_login_settings_after', function ( $settings ) {
	echo '<h2>' . esc_html__( 'My section', 'my-plugin' ) . '</h2>';
} );

Anything printed here sits outside the plugin's own form, so a section with fields of its own supplies its own form, its own settings group and its own save button. Posting the plugin's own group (pantry-login) from a second form would clear the settings that form does not carry.

Lockout::generic_message()

Not a hook, but public: Pantry\Login\Lockout::generic_message() returns the one message that never says which half of the credentials was wrong, already passed through pantry_login_generic_message. Anything else that refuses a login can print the same words.

pantry_login_generic_message

Filters the one message that replaces the username-revealing errors.

add_filter( 'pantry_login_generic_message', function ( string $message ) {
	return '<strong>' . esc_html__( 'Error:', 'my-theme' ) . '</strong> ' . esc_html__( 'Those details did not work.', 'my-theme' );
} );

What it stores

Three options (pantry_login_settings, pantry_login_log, pantry_login_version) and transients named pantry_login_c_, pantry_login_i_, pantry_login_lp_ and pantry_login_li_ followed by a hash. No tables, no post types, no cron events, no post meta, and no request ever leaves your server. Uninstalling removes all of it.

Scope

This plugin does not do two-factor authentication, CAPTCHAs, IP allow or deny lists, or country blocking, and it never calls out to another service.

Pro modules

What Pantry Pro adds to this plugin. None of it is inside the free plugin, and a module only runs when the free plugin is active.

Pantry Pro: Login, IP allow and deny lists

Pantry\Pro\Modules\Login\Ip_Lists, in src/modules/login/class-ip-lists.php.

Judges a login by the address it came from before Pantry Login's lockout has anything to say about it. Needs Pantry Login 1.1.0 or newer, because it prints its settings through that version's pantry_login_settings_after action.

What it adds

  • A deny list. Addresses and ranges that may never log in.
  • An allow list. With anything in it, only those addresses may log in and the deny list is not consulted at all.
  • IPv4 and IPv6, plain or CIDR. 203.0.113.9, 203.0.113.0/24, 2001:db8::1, 2001:db8::/32. An IPv4 address arriving in its IPv6 form, ::ffff:203.0.113.9, still matches an IPv4 rule.
  • The free plugin's own address reading. The address comes from Pantry\Login\Ip::current() with Pantry Login's own proxy setting, so switching "Read the visitor address from X-Forwarded-For" on that screen changes this module too. There is no second proxy setting.
  • The free plugin's own refusal message. A refused address is told exactly what a wrong password is told, through Pantry\Login\Lockout::generic_message(), so the form gives nothing away. A site that has filtered pantry_login_generic_message sees its own words here too.

Settings

Plugin Pantry > Login, in the "Address lists" section, saved in the option pantry_pro_login_ip_lists with its own Save button.

Setting Default What it does
Address lists Off Judges every login by the address it came from.
Allow list empty One address or range per line. Leave this empty unless you mean it. With one line in it, nobody can log in from any other address.
Deny list empty One address or range per line. Ignored while the allow list has anything in it.

The section prints the address you are reading it from, so that address can be copied into the allow list before it is switched on.

Lines that are not an address or a range are dropped when the form is saved, and so are duplicates. The remaining lines keep the order they were typed in.

Not locking yourself out

Two things guard against it.

  • On save. If the lists as submitted would refuse the address the administrator is saving from, they are saved but left switched off, and the screen says which address is the problem.

  • In wp-config.php. If it happens anyway, from another address or another proxy setting, add this, log in, fix the list, and take it out again:

    define( 'PANTRY_PRO_LOGIN_IP_LISTS_OFF', true );
    

WP-CLI is never judged, so wp option update pantry_pro_login_ip_lists also gets a site back.

Where it sits in the login

On authenticate at priority 25: after WordPress has checked the password at 20, which is as early as a refusal can be made to stick, because wp_authenticate_username_password() replaces whatever the filter chain held before it. That is still before Pantry Login's lockout gate at 30 and before the multisite spam check at 99, so a refused address never reaches them.

A refusal is a failed login like any other, so Pantry Login counts it towards a lockout. Repeated attempts from a refused address end up locked out as well as refused, which is the behaviour worth having.

What it stores

One option, pantry_pro_login_ip_lists. No tables, no meta, no transients, no requests. uninstall() deletes that option.

Hooks

The module adds no hooks of its own. It uses WordPress's authenticate and admin_init, and Pantry Login's pantry_login_settings_after. It reads Pantry Login's pantry_login_settings option for the proxy setting and calls that plugin's Ip::current() and Lockout::generic_message().

Pantry Pro: Login, two-factor authentication

Pantry\Pro\Modules\Login\Two_Factor, in src/modules/login/class-two-factor.php, with two helper classes beside it that are not modules: Totp in class-totp.php does the RFC 6238 arithmetic and the base32, and Qr in class-qr.php draws the symbol. The module is 830 lines, the helpers 151 and 602; splitting them out is what keeps the module itself inside the line budget.

Adds a time-based one-time code after the password to Pantry Login. Needs Pantry Login 1.1.0 or newer, because it prints its settings through that version's pantry_login_settings_after action.

What it adds

  • A code after the password. A correct password is not a login. It earns a signed, single-use pending token that lives five minutes; the login only happens when the code is right. Nothing is written to the authentication cookie until then.
  • TOTP, RFC 6238. SHA-1, six digits, thirty seconds, one step of drift allowed either side, written in PHP with hash_hmac. No library, no service. A step that has been accepted once is never accepted again.
  • Setup on the profile screen. A QR code drawn as inline SVG by this plugin, the secret in readable groups, and the whole otpauth:// address as text for anyone typing it in by hand. Nothing is fetched from anywhere.
  • Ten recovery codes. Made when two-factor is switched on, shown once, stored only as wp_hash() digests, each usable once. The profile screen says how many are left and can replace the lot.
  • Per-role enforcement. Roles picked on the Pantry Login screen are sent to their profile when they log in and told on every admin screen until they have set two-factor up. Nobody is ever shut out of the site for not having done it.
  • A custom login address still works. The module hooks the login flow, never the address: the code form is built from wp_login_url(), so it appears at whatever address Pantry Login is serving the form from.

Settings

Plugin Pantry > Login, in the "Two-factor authentication" section, saved in the option pantry_pro_login_two_factor with its own Save button.

Setting Default What it does
Two-factor Off Offers two-factor on this site. With it off nothing is registered at all. Nobody is asked for a code until they have set one up, so switching it on changes nothing for anyone until they do.
Ask these roles to set it up none Anyone in a ticked role who has not set two-factor up lands on their profile after logging in and sees a warning notice on every admin screen.

Setting two-factor up

Users > Profile, "Two-factor authentication". The screen makes a pending secret the first time it is opened, shows the QR code, and waits. Enter the code the app shows, save the profile, and two-factor is on: the ten recovery codes appear once, at that moment, and never again. Two checkboxes appear from then on: one replaces the recovery codes, one turns two-factor off.

An administrator looking at somebody else's profile sees whether two-factor is on and can turn it off for them, which is how a person who has lost both phone and recovery codes gets back in. The secret and the QR code are only ever shown to the person they belong to.

What a login looks like

  1. Username and password are posted to the login form as usual.
  2. On authenticate at priority 100, after WordPress has checked the password and after Pantry Login's lockout, the module stores a pending token as a transient, writes a signed cookie holding the token id, the user id, the expiry and an HMAC over all three, and redirects to ?action=pantry_pro_2fa.
  3. That screen asks for the code. A six-digit code from the app or one of the recovery codes is accepted.
  4. On the right code the token is thrown away, wp_set_auth_cookie() runs, wp_login fires and the browser goes where it was going.

A wrong code fires wp_login_failed, so Pantry Login counts it towards a lockout like any other failed attempt. After five wrong codes the pending token is destroyed and the login has to start again.

Limits worth knowing

  • The code is asked for on the login form. An application password is its own credential and is never challenged. Anything else that offers a password without a browser, XML-RPC in particular, is refused outright with a message saying the code can only be given on the login form.
  • WP-CLI is not challenged, as it does not authenticate with a password.
  • The QR encoder covers versions 1 to 10 at error-correction level L, which is far more than an otpauth:// address needs. If a site name were long enough to overflow that, the QR is left out and the secret and address stay.

What it stores

  • Option pantry_pro_login_two_factor.
  • User meta pantry_pro_login_2fa_secret, pantry_pro_login_2fa_pending, pantry_pro_login_2fa_codes (hashes only) and pantry_pro_login_2fa_used.
  • Transients pantry_pro_login_2fa_t_<id> for a pending login, five minutes, and pantry_pro_login_2fa_new_<user id> for the codes about to be shown once.
  • Cookie pantry_pro_login_2fa, HttpOnly, SameSite Lax, secure on HTTPS, which lives only as long as the pending login.

uninstall() removes the option, all four meta keys for every user and both transient families.

Hooks

The module adds no hooks of its own. It uses WordPress's authenticate, login_form_pantry_pro_2fa, login_redirect, show_user_profile, edit_user_profile, personal_options_update, edit_user_profile_update, application_password_did_authenticate and admin_notices, and Pantry Login's pantry_login_settings_after. It fires WordPress's own wp_login and wp_login_failed exactly where wp_signon() would have.

Comparison

How Pantry Login compares with the popular plugins that do the same job, and when one of them is the better choice.

Pantry Login compared with Limit Login Attempts Security, WPS Hide Login, Loginizer and Wordfence

The plugin