Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JPKCom Hide Login

Plugin Name: JPKCom Hide Login
Plugin URI: https://github.com/JPKCom/jpkcom-hide-login
Description: Rename wp-login.php to a custom slug, with brute force protection and IP whitelist management.
Version: 1.2.9
Author: Jean Pierre Kolb jpk@jpkc.com
Author URI: https://www.jpkc.com/
Contributors: JPKCom
Tags: Login, Security, Brute Force Protection, Hide Login, Custom Login URL
Requires at least: 6.9
Tested up to: 7.1
Requires PHP: 8.3
Network: true
Stable tag: 1.2.9
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: jpkcom-hide-login
Domain Path: /languages

Rename wp-login.php to a custom slug, with brute force protection and IP whitelist management.


Description

JPKCom Hide Login is a security plugin that allows you to completely hide your WordPress login page by replacing the default wp-login.php URL with a custom slug of your choice. This significantly improves your site's security by preventing automated bot attacks and brute force attempts on the default login URL.

Key Features

  • Custom Login URL - Replace wp-login.php with any custom slug (e.g., /secure-login/)
  • Brute Force Protection - Automatically block IPs after failed login attempts with customizable thresholds
  • Customizable Security Thresholds - Configure max attempts, attempt window, and block duration
  • IP Whitelist Management - Add trusted IPs that will never be blocked (supports CIDR ranges)
  • WP-CLI Support - Full command-line management via WP-CLI commands
  • WordPress Multisite Support - Network-wide settings for all sites or per-site configuration
  • WooCommerce Compatible - Works seamlessly with WooCommerce login/logout
  • REST API Safe - Does not interfere with REST API or AJAX requests
  • No Core File Modifications - Uses only WordPress filters and hooks
  • Fully Translatable - Ready for translation (Text Domain: jpkcom-hide-login)
  • Clean Uninstall - Completely reversible, no database pollution

How It Works

Once activated, the plugin:

  1. Blocks all direct access to wp-login.php and wp-admin for non-logged-in users with a 404 response
  2. Creates a custom login URL (default: /jpkcom-login/) that serves the WordPress login page
  3. Tracks failed login attempts and automatically blocks IPs after threshold is reached
  4. Allows whitelisting of trusted IPs to prevent legitimate users from being blocked
  5. Provides a comprehensive admin interface for managing all settings and viewing blocked IPs

Security Benefits

  • Protects against automated attacks - Bots scanning for /wp-login.php will receive a 404 error
  • Prevents brute force attacks - Automatic IP blocking after multiple failed attempts
  • Reduces server load - Fewer malicious requests reaching your login page
  • Customizable security - Whitelist your own IPs for safe administration
  • No information disclosure - Blocked requests receive generic 404 responses

Installation

Automatic Installation (Recommended)

  1. Download the plugin ZIP file from GitHub or your source
  2. Go to Plugins → Add New → Upload Plugin in your WordPress admin
  3. Choose the ZIP file and click Install Now
  4. Click Activate Plugin

Manual Installation via FTP

  1. Download and extract the plugin ZIP file
  2. Upload the jpkcom-hide-login folder to /wp-content/plugins/
  3. Activate the plugin through the Plugins menu in WordPress

After Activation

  1. You'll see a success notice showing your new login URL (default: https://yourdomain.com/jpkcom-login/)
  2. Bookmark this URL immediately!
  3. Go to Settings → Hide Login to customize your settings
  4. (Optional) Add your IP to the whitelist to prevent accidental lockout

Configuration

Basic Settings

Navigate to Settings → Hide Login in your WordPress admin.

Custom Login Slug
  1. Enter your desired slug in the Custom Login URL Slug field
  2. Avoid using these forbidden slugs:
    • login, admin, dashboard, wp-admin, wp-login
  3. The slug cannot be the same as an existing page or post URL
  4. Click Save Changes

Your new login URL will be: https://yourdomain.com/your-custom-slug/

Brute Force Protection

Automatic Protection - Fully customizable!

  • Default settings: 5 failed login attempts within 60 seconds blocks the IP for 10 minutes
  • Customize thresholds via admin interface: max attempts, attempt window, and block duration
  • Blocked IPs are shown in the admin interface with expiration times
  • View currently blocked IPs under Currently Blocked IPs section
  • Click Clear All Blocked IPs to manually unblock all IPs

IP Whitelist Management

Prevent trusted IPs from being blocked:

  1. Go to Settings → Hide Login
  2. Scroll to IP Whitelist section
  3. Your current IP is displayed for reference
  4. Enter an IP address or CIDR range (e.g., 192.168.1.0/24)
  5. Click Add to Whitelist

Whitelisted IPs:

  • Will never be blocked by brute force protection
  • Can access the login page even after failed attempts
  • Can be removed at any time using the Remove button

CIDR Range Examples:

  • Single IP: 192.168.1.100
  • Subnet: 192.168.1.0/24 (192.168.1.1 - 192.168.1.254)
  • Large range: 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)

Multisite Configuration

For WordPress Multisite installations:

Network-Wide Settings
  1. Go to Network Admin → Settings → Hide Login (Network)
  2. Set a global slug for all sites in the network
  3. Leave empty to allow each site to use its own slug
Per-Site Settings
  1. Each site can still access Settings → Hide Login
  2. If a network-wide slug is set, it takes priority
  3. Sites will see a notice indicating network-wide settings are active

WP-CLI Commands

Manage the plugin via command line with WP-CLI:

Plugin Status
# Display current configuration and status
wp jpkcom-hide-login status
Login Slug Management
# Get current login slug
wp jpkcom-hide-login get-slug

# Set new login slug
wp jpkcom-hide-login set-slug my-secure-login
IP Whitelist Management
# List all whitelisted IPs
wp jpkcom-hide-login whitelist list

# Add IP to whitelist (supports CIDR)
wp jpkcom-hide-login whitelist add 192.168.1.100
wp jpkcom-hide-login whitelist add 192.168.1.0/24

# Remove IP from whitelist
wp jpkcom-hide-login whitelist remove 192.168.1.100
Blocked IPs Management
# List currently blocked IPs
wp jpkcom-hide-login blocked list

# Clear all blocked IPs
wp jpkcom-hide-login blocked clear
Brute Force Protection Settings
# Set maximum login attempts (1-100)
wp jpkcom-hide-login protection max-attempts 10

# Set attempt tracking window in seconds (1-3600)
wp jpkcom-hide-login protection attempt-window 120

# Set block duration in seconds (1-86400)
wp jpkcom-hide-login protection block-duration 1800
Database Cleanup
# Manually cleanup expired login attempt data
# (This also runs automatically once per day via WordPress Cron)
wp jpkcom-hide-login cleanup

FAQ

I activated the plugin but can't find the login page!

The default login URL after activation is:

https://yourdomain.com/jpkcom-login/

If you changed the slug in the settings, use your custom URL instead.

Recovery Methods:

  1. Check your admin bar (if still logged in) - go to Settings → Hide Login
  2. Check your browser bookmarks for the saved login URL
  3. Disable the plugin via FTP - rename the folder:
    /wp-content/plugins/jpkcom-hide-login/
    
    to
    /wp-content/plugins/jpkcom-hide-login-disabled/
    
  4. Add emergency slug to wp-config.php:
    define('JPKCOM_HIDE_LOGIN_DEFAULT_SLUG', 'emergency-login');

I'm locked out due to IP blocking! How do I recover?

If you're blocked after too many failed login attempts:

Method 1: Wait 10 Minutes

The block automatically expires after 10 minutes.

Method 2: Add IP to Whitelist via Database

Execute this SQL query in phpMyAdmin or your database tool:

UPDATE wp_options
SET option_value = '["YOUR.IP.ADDRESS"]'
WHERE option_name = 'jpkcom_hide_login_ip_whitelist';

Replace YOUR.IP.ADDRESS with your actual IP address.

Method 3: Clear All Blocks via Database

DELETE FROM wp_options
WHERE option_name = 'jpkcom_hide_login_blocked_ips';

Can I use this plugin on Multisite?

Yes! The plugin fully supports WordPress Multisite with two configuration modes:

  1. Network-wide slug - Set one login URL for all sites
  2. Per-site slugs - Let each site administrator set their own custom slug

Network administrators can access network settings at: Network Admin → Settings → Hide Login (Network)

What happens if someone tries to access wp-login.php?

They receive a 404 Not Found error with no indication that the login page exists elsewhere. This provides better security than redirecting or showing a 403 error.

After 5 attempts within 60 seconds, the IP is blocked for 10 minutes with no further access to the site.

Are wp-admin/load-scripts.php and load-styles.php still reachable?

Yes, and no plugin can change that. Both files deliberately do not load WordPress — they require only noop.php, script-loader.php and version.php — so no hook, filter or init callback ever runs for them. They are the one pair of wp-admin URLs this plugin cannot mask.

What they leak is modest: a bare request returns an empty 200, and a crafted one serves concatenated WordPress core scripts. Neither reveals the custom login slug or whether an account exists; it only confirms that the site runs WordPress, which the REST API and /wp-includes/ assets already do.

If you still want them gone, it has to happen in the web server, above PHP. Both files are used only by wp-admin, and only when script concatenation is on (CONCATENATE_SCRIPTS); the front end never requests them. Restricting them to logged-in sessions is therefore safe:

nginx

location ~ ^/wp-admin/load-(scripts|styles)\.php$ {
    if ($http_cookie !~* "wordpress_logged_in_") {
        return 404;
    }
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

Apache (.htaccess in wp-admin/)

<FilesMatch "^load-(scripts|styles)\.php$">
    RewriteEngine On
    RewriteCond %{HTTP_COOKIE} !wordpress_logged_in_ [NC]
    RewriteRule .* - [R=404,L]
</FilesMatch>

Test the admin area afterwards with a cleared cache: if the rule is wrong, wp-admin loses its stylesheets and scripts.

Does this affect WordPress REST API or AJAX?

No. The plugin intelligently detects and allows:

  • WordPress REST API requests
  • Admin AJAX calls (admin-ajax.php)
  • WooCommerce AJAX requests
  • Other legitimate background requests

Only direct browser access to wp-login.php and wp-admin is blocked for non-logged-in users.

Is it compatible with WooCommerce?

Yes! The plugin includes special compatibility for WooCommerce:

  • My Account login forms work correctly
  • Login/logout redirects are properly handled
  • Checkout login is not affected
  • WooCommerce AJAX requests are never blocked

Can I customize the number of attempts or block duration?

Yes! All brute force protection thresholds are fully customizable:

Navigate to Settings → Hide Login and scroll to Brute Force Protection Settings to configure:

  • Maximum Login Attempts (1-100) - Default: 5
  • Attempt Window (1-3600 seconds) - Default: 60 seconds
  • Block Duration (1-86400 seconds / 24 hours) - Default: 600 seconds (10 minutes)

You can also configure these settings via WP-CLI:

wp jpkcom-hide-login protection max-attempts 10
wp jpkcom-hide-login protection attempt-window 120
wp jpkcom-hide-login protection block-duration 1800

Does this plugin modify core WordPress files?

No. The plugin uses only WordPress hooks and filters. It does not modify:

  • wp-login.php
  • wp-admin files
  • Core WordPress files

This means WordPress updates are completely safe and won't break the plugin.

Can I override default values in wp-config.php?

Yes! Add these constants to wp-config.php (before /* That's all, stop editing! */):

// Custom default login slug
define( 'JPKCOM_HIDE_LOGIN_DEFAULT_SLUG', 'my-secret-login' );

// Custom option name for per-site slug
define( 'JPKCOM_HIDE_LOGIN_OPTION', 'my_custom_login_slug' );

// Custom option name for network slug (Multisite)
define( 'JPKCOM_HIDE_LOGIN_NETWORK_OPTION', 'my_network_login_slug' );

Will this affect password reset emails?

No. Password reset emails automatically use the custom login URL. When users click the reset link, they're redirected to your custom slug with the reset token intact.

Can I use this with other security plugins?

Generally yes, but avoid using multiple plugins that:

  • Also hide the login URL (conflict)
  • Implement their own brute force protection (may interfere)

Compatible with:

  • Wordfence (disable Login Security features)
  • Sucuri Security
  • iThemes Security (disable Login URL changing)
  • All in One WP Security (disable Login Page changing)

Troubleshooting

Login page shows a white screen

This usually means there's a PHP error. Check your error logs:

  1. Enable debugging in wp-config.php:
    define( 'WP_DEBUG', true );
    define( 'WP_DEBUG_LOG', true );
    define( 'WP_DEBUG_DISPLAY', false );
  2. Check /wp-content/debug.log for errors
  3. Report any errors to the plugin support

Settings page doesn't save changes

  1. Check that you have manage_options capability
  2. Verify the slug doesn't conflict with existing pages
  3. Check for JavaScript errors in browser console
  4. Temporarily disable other plugins to identify conflicts

Logged in but still see 404 on wp-admin

This shouldn't happen. Possible causes:

  1. Browser cache - Clear your browser cache and cookies
  2. Plugin conflict - Temporarily disable other security plugins
  3. Permalink structure - Go to Settings → Permalinks and click Save Changes

After logout, redirected to 404 page

The logout redirect should automatically use the custom login URL. If not:

  1. Check if any other plugin is filtering logout_redirect
  2. Temporarily disable other plugins
  3. Report the issue with details about your WordPress version and active plugins

Technical Details

Architecture

The plugin uses a modular, object-oriented architecture:

jpkcom-hide-login/
├── jpkcom-hide-login.php          # Bootstrap & initialization
└── includes/
    ├── class-ip-manager.php       # IP whitelist/blocklist management
    ├── class-login-protection.php # Brute force protection
    ├── class-mask-login.php       # Login URL masking
    ├── class-admin-settings.php   # Admin interface
    └── class-plugin-updater.php   # GitHub-based updates

Database Usage

The plugin uses WordPress options and transients:

Options:

  • jpkcom_hide_login_slug - Per-site custom slug
  • jpkcom_hide_login_network_slug - Network-wide slug (Multisite)
  • jpkcom_hide_login_ip_whitelist - Array of whitelisted IPs
  • jpkcom_hide_login_notice_shown - First-time activation notice flag

Transients:

  • jpkcom_hide_login_blocked_ips - Currently blocked IP list with expiration
  • jpkcom_hide_login_attempts_{hash} - Failed login attempt counters per IP

Automatic Cleanup:

  • Expired login attempt transients are automatically cleaned up daily via WordPress Cron
  • Manual cleanup available via WP-CLI: wp jpkcom-hide-login cleanup
  • All data is removed on plugin deactivation

PHP Requirements

  • PHP 8.3+

WordPress Compatibility

  • WordPress 6.9+ - Tested with latest WordPress versions
  • Multisite - Full support for network activation
  • WooCommerce - Compatible with WooCommerce 8.0+

Changelog

1.2.9

  • Changed: the update manifest generator now defaults a missing Network: header to false instead of true, matching WordPress' own default. No change for this plugin, which declares Network: true explicitly
  • CI: the lint and guard workflow now also runs on pushes to main. It only covered pull requests, so a direct push with bypass rights skipped every check
  • Changed: comments, workflow step names and CI output across the repository are now English throughout, and the developer notes in CLAUDE.md were translated and trimmed. No effect on the shipped plugin

1.2.8

  • Changed: Tested up to raised to WordPress 7.1
  • Changed: the bundled updater's runtime floor now matches the plugin's own minimum. It bailed out below WordPress 6.8 while the plugin header has required 6.9 for several releases, so the check could never fire on a supported installation
  • Docs: the remaining "WordPress 6.8" requirement statements now say 6.9, matching the plugin header
  • CI: the release manifest's fallback values for requires and tested now say 6.9 and 7.1. They only apply when the README metadata cannot be read, but a stale fallback would have published a minimum the plugin no longer supports

1.2.7

  • Fixed: a block renewed itself for as long as anyone kept trying. A rejected attempt still fired wp_login_failed, so the counter grew and block_ip() was called again with a fresh full duration — the "try again in N minutes" message was untrue and a locked-out administrator hitting reload never got back in. Attempts from an already blocked IP are no longer counted
  • Added: the settings screen now warns when the detected client address is a loopback, private or link-local address and no trusted proxy is configured — the case where every visitor shares one address (five wrong passwords lock out everybody) or, if that address is whitelisted, where the protection is silently off altogether. The notice names the JPKCOM_HIDE_LOGIN_TRUSTED_PROXIES line to add
  • Changed: the runtime check now requires WordPress 6.9, matching the Requires at least header, which demanded 6.9 while the check still let 6.8 through. Both are driven by JPKCOM_HIDE_LOGIN_MIN_WP / JPKCOM_HIDE_LOGIN_MIN_PHP so they cannot drift apart again
  • Changed: cleared every WordPress Plugin Check finding except plugin_updater_detected, which is inherent to a self-hosted updater — escaping at the point of output, prepared statements inline, wp_delete_file() instead of unlink(), documented nonce and sanitisation exemptions on the request-path parsing that must stay byte-exact
  • Removed: the manual load_plugin_textdomain() call. Since WordPress 6.7 the textdomain registry loads the plugin's own /languages just in time from the Domain Path header
  • Fixed: the shipped German .l10n.php translations stored plural entries as PHP arrays instead of a NUL-joined string. WordPress' translation controller passes that value straight to explode(), so on a German site the second failed login attempt died with a fatal TypeError — the login page returned HTTP 500 and no IP was ever blocked. Both files are regenerated with wp i18n make-php
  • Security: /wp-signup.php is now blocked on single sites too. Core answers it with a redirect to wp_registration_url(), which this plugin rewrites to the custom slug — so one anonymous request returned the secret login slug in the Location header
  • Fixed: the wp_login, wp_login_failed, authenticate, login_url, logout_url, lostpassword_url, logout_redirect and site_url callbacks required strictly typed arguments. Third-party code that fires these with fewer arguments or a null — MainWP Child calls do_action( 'wp_login', $user_login ) with one argument — caused a fatal ArgumentCountError / TypeError. All callbacks now accept the foreign shapes and normalise internally
  • Fixed: wp jpkcom-hide-login get-slug / set-slug did not exist; WP-CLI had registered them as get_slug / set_slug. Both are now declared with @subcommand, matching the documentation
  • Changed: verbose request tracing moved from WP_DEBUG to its own JPKCOM_HIDE_LOGIN_DEBUG constant (default off). It used to write ~10 lines per request on any WP_DEBUG site, including the request URI and the full $_GET of every login attempt — password-reset keys included

1.2.6

  • Changed: the plugin banners (assets/banner-1544x500.avif, assets/banner-772x250.avif) are now a plain #3c4955 surface with no lettering

1.2.5

  • Security: the client IP is now taken from REMOTE_ADDR; X-Forwarded-For, CF-Connecting-IP and X-Real-IP are only believed when the request arrives from a proxy declared via JPKCOM_HIDE_LOGIN_TRUSTED_PROXIES or the jpkcom_hide_login_trusted_proxies filter. Previously a single spoofed header made the plugin see a whitelisted address, which disabled both the wp-login.php block and the brute-force protection, and allowed an attacker to get someone else's address blocked
  • Security: the wp-login.php / wp-signup.php block no longer relies on a substring test against the raw request URI. //wp-login.php and /wp-%6cogin.php bypassed it; matching is now done against the script the server actually resolved, plus a normalised (decoded, slash-collapsed) path
  • Security: redirects only reveal the custom slug while the masked login page is being served or for an authenticated request — an anonymous probe no longer receives it in the Location header
  • Fixed: the wp-admin block matched any path containing "wp-admin", so a post with a slug such as /my-wp-admin-guide/ returned 404 for every visitor. It now matches the first path segment
  • Fixed: CIDR whitelist entries never matched IPv6 addresses because ip2long() returns false for them, while the admin UI happily accepted IPv6 ranges. Matching now works for both families via inet_pton(), and the prefix ceiling depends on the family (/128 vs /32)
  • Changed: IP keys are derived with a salted hash_hmac( 'sha256', … , wp_salt( 'auth' ) ) instead of a bare md5(), which was reversible for IPv4 in seconds. Existing attempt counters and blocks are invalidated once on upgrade
  • Added: tests/test-security.php — regression tests for every issue above, each written to fail against the previous implementation. Run in CI on every pull request

1.2.4

  • Security: update packages are now verified before installation — the verified file is handed to WordPress instead of being downloaded a second time, so the bytes that were checked are the bytes that get installed
  • Security: a missing or unfetchable SHA-256 checksum now aborts the update instead of installing unverified code (previously it silently skipped verification)
  • Security: pinned every GitHub Action to a full commit SHA and added Dependabot with a 7-day cooldown, so a moved tag can no longer change the release build
  • Security: tightened which download the updater claims, so sibling plugins cannot match each other's package
  • Fixed: sprintf() calls in the updater bound named arguments to a variadic parameter, which raises ArgumentCountError on PHP 8.3
  • Fixed: the "View Details" modal could fail with a TypeError when the manifest omitted requires_plugins
  • Performance: a failed manifest fetch is now cached for an hour instead of being retried on every admin request
  • Added: CI workflow on every pull request (PHP lint, named-argument check, YAML validation, action-pinning guard)

1.2.3 (2026-06-16)

  • Raised the minimum WordPress version to 6.9 and "Tested up to" to WordPress 7.0
  • Switched license metadata to the SPDX identifier GPL-2.0-or-later with the HTTPS license URI

1.2.2 (2026-06-16)

  • Security: updater prefers an exact match against the manifest download_url over the slug heuristic, so a tampered manifest can no longer bypass the checksum gate
  • Security: timing-safe checksum comparison (hash_equals()) with an is_string() guard against hash_file() failures
  • Security: manifest fetch via wp_safe_remote_get() (SSRF defense-in-depth)
  • Fixed PHP warning and missing contributor names in the plugin detail popup (display_name now provided)
  • Fixed PHP warning/deprecation on wp plugin list by completing the no_update transient entry (new_version, package, tested, requires_php)

1.2.1 (2026-03-13)

Fixed

  • ZIP-Upload Fix - Manual ZIP uploads via WP-Admin no longer fail with "invalid URL" error during checksum verification
  • Release ZIP Structure - GitHub Actions release now creates ZIP with top-level plugin folder, ensuring WordPress correctly recognizes updates

1.2.0 (2025-11-12)

Added

  • IP Whitelist Management - Add trusted IPs that will never be blocked
  • CIDR Range Support - Whitelist entire IP ranges (e.g., 192.168.1.0/24)
  • Enhanced Admin Interface - Professional settings page with all features
  • Blocked IP Viewer - See currently blocked IPs with expiration times
  • Brute Force Protection - Automatic IP blocking after failed login attempts
  • Customizable Protection Thresholds - Configure max attempts, attempt window, and block duration
  • Login Attempt Counter - Shows remaining attempts on failed logins
  • Current IP Display - See your current IP in admin settings
  • One-Click Block Clearing - Clear all blocked IPs with a button
  • Automatic Database Cleanup - Daily WordPress Cron job removes expired login attempt data
  • WP-CLI Cleanup Command - Manual database cleanup via wp jpkcom-hide-login cleanup
  • Full WP-CLI Support - Complete command-line management for all plugin features

1.0.0 (2024-11-30)

  • Initial Release
  • Basic login URL masking
  • Simple IP blocking
  • Multisite support
  • Basic admin settings

About

Rename the WordPress login URL to a custom slug for enhanced security. Includes brute force protection and IP whitelist management.

Topics

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages