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.
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.
- Custom Login URL - Replace
wp-login.phpwith 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
Once activated, the plugin:
- Blocks all direct access to
wp-login.phpandwp-adminfor non-logged-in users with a 404 response - Creates a custom login URL (default:
/jpkcom-login/) that serves the WordPress login page - Tracks failed login attempts and automatically blocks IPs after threshold is reached
- Allows whitelisting of trusted IPs to prevent legitimate users from being blocked
- Provides a comprehensive admin interface for managing all settings and viewing blocked IPs
- Protects against automated attacks - Bots scanning for
/wp-login.phpwill 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
- Download the plugin ZIP file from GitHub or your source
- Go to Plugins → Add New → Upload Plugin in your WordPress admin
- Choose the ZIP file and click Install Now
- Click Activate Plugin
- Download and extract the plugin ZIP file
- Upload the
jpkcom-hide-loginfolder to/wp-content/plugins/ - Activate the plugin through the Plugins menu in WordPress
- You'll see a success notice showing your new login URL (default:
https://yourdomain.com/jpkcom-login/) - Bookmark this URL immediately!
- Go to Settings → Hide Login to customize your settings
- (Optional) Add your IP to the whitelist to prevent accidental lockout
Navigate to Settings → Hide Login in your WordPress admin.
- Enter your desired slug in the Custom Login URL Slug field
- Avoid using these forbidden slugs:
login,admin,dashboard,wp-admin,wp-login
- The slug cannot be the same as an existing page or post URL
- Click Save Changes
Your new login URL will be: https://yourdomain.com/your-custom-slug/
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
Prevent trusted IPs from being blocked:
- Go to Settings → Hide Login
- Scroll to IP Whitelist section
- Your current IP is displayed for reference
- Enter an IP address or CIDR range (e.g.,
192.168.1.0/24) - 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)
For WordPress Multisite installations:
- Go to Network Admin → Settings → Hide Login (Network)
- Set a global slug for all sites in the network
- Leave empty to allow each site to use its own slug
- Each site can still access Settings → Hide Login
- If a network-wide slug is set, it takes priority
- Sites will see a notice indicating network-wide settings are active
Manage the plugin via command line with WP-CLI:
# Display current configuration and status
wp jpkcom-hide-login status# Get current login slug
wp jpkcom-hide-login get-slug
# Set new login slug
wp jpkcom-hide-login set-slug my-secure-login# 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# List currently blocked IPs
wp jpkcom-hide-login blocked list
# Clear all blocked IPs
wp jpkcom-hide-login blocked clear# 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# Manually cleanup expired login attempt data
# (This also runs automatically once per day via WordPress Cron)
wp jpkcom-hide-login cleanupThe 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:
- Check your admin bar (if still logged in) - go to Settings → Hide Login
- Check your browser bookmarks for the saved login URL
- Disable the plugin via FTP - rename the folder:
to
/wp-content/plugins/jpkcom-hide-login//wp-content/plugins/jpkcom-hide-login-disabled/ - Add emergency slug to wp-config.php:
define('JPKCOM_HIDE_LOGIN_DEFAULT_SLUG', 'emergency-login');
If you're blocked after too many failed login attempts:
The block automatically expires after 10 minutes.
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.
DELETE FROM wp_options
WHERE option_name = 'jpkcom_hide_login_blocked_ips';Yes! The plugin fully supports WordPress Multisite with two configuration modes:
- Network-wide slug - Set one login URL for all sites
- 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)
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.
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.
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.
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
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 1800No. The plugin uses only WordPress hooks and filters. It does not modify:
wp-login.phpwp-adminfiles- Core WordPress files
This means WordPress updates are completely safe and won't break the plugin.
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' );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.
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)
This usually means there's a PHP error. Check your error logs:
- Enable debugging in
wp-config.php:define( 'WP_DEBUG', true ); define( 'WP_DEBUG_LOG', true ); define( 'WP_DEBUG_DISPLAY', false );
- Check
/wp-content/debug.logfor errors - Report any errors to the plugin support
- Check that you have
manage_optionscapability - Verify the slug doesn't conflict with existing pages
- Check for JavaScript errors in browser console
- Temporarily disable other plugins to identify conflicts
This shouldn't happen. Possible causes:
- Browser cache - Clear your browser cache and cookies
- Plugin conflict - Temporarily disable other security plugins
- Permalink structure - Go to Settings → Permalinks and click Save Changes
The logout redirect should automatically use the custom login URL. If not:
- Check if any other plugin is filtering
logout_redirect - Temporarily disable other plugins
- Report the issue with details about your WordPress version and active plugins
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
The plugin uses WordPress options and transients:
Options:
jpkcom_hide_login_slug- Per-site custom slugjpkcom_hide_login_network_slug- Network-wide slug (Multisite)jpkcom_hide_login_ip_whitelist- Array of whitelisted IPsjpkcom_hide_login_notice_shown- First-time activation notice flag
Transients:
jpkcom_hide_login_blocked_ips- Currently blocked IP list with expirationjpkcom_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 8.3+
- WordPress 6.9+ - Tested with latest WordPress versions
- Multisite - Full support for network activation
- WooCommerce - Compatible with WooCommerce 8.0+
- 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 declaresNetwork: trueexplicitly - 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.mdwere translated and trimmed. No effect on the shipped plugin
- Changed:
Tested up toraised 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
requiresandtestednow 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
- Fixed: a block renewed itself for as long as anyone kept trying. A rejected attempt still fired
wp_login_failed, so the counter grew andblock_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_PROXIESline to add - Changed: the runtime check now requires WordPress 6.9, matching the
Requires at leastheader, which demanded 6.9 while the check still let 6.8 through. Both are driven byJPKCOM_HIDE_LOGIN_MIN_WP/JPKCOM_HIDE_LOGIN_MIN_PHPso 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 ofunlink(), 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/languagesjust in time from theDomain Pathheader - Fixed: the shipped German
.l10n.phptranslations stored plural entries as PHP arrays instead of a NUL-joined string. WordPress' translation controller passes that value straight toexplode(), so on a German site the second failed login attempt died with a fatalTypeError— the login page returned HTTP 500 and no IP was ever blocked. Both files are regenerated withwp i18n make-php - Security:
/wp-signup.phpis now blocked on single sites too. Core answers it with a redirect towp_registration_url(), which this plugin rewrites to the custom slug — so one anonymous request returned the secret login slug in theLocationheader - Fixed: the
wp_login,wp_login_failed,authenticate,login_url,logout_url,lostpassword_url,logout_redirectandsite_urlcallbacks required strictly typed arguments. Third-party code that fires these with fewer arguments or anull— MainWP Child callsdo_action( 'wp_login', $user_login )with one argument — caused a fatalArgumentCountError/TypeError. All callbacks now accept the foreign shapes and normalise internally - Fixed:
wp jpkcom-hide-login get-slug/set-slugdid not exist; WP-CLI had registered them asget_slug/set_slug. Both are now declared with@subcommand, matching the documentation - Changed: verbose request tracing moved from
WP_DEBUGto its ownJPKCOM_HIDE_LOGIN_DEBUGconstant (default off). It used to write ~10 lines per request on anyWP_DEBUGsite, including the request URI and the full$_GETof every login attempt — password-reset keys included
- Changed: the plugin banners (
assets/banner-1544x500.avif,assets/banner-772x250.avif) are now a plain#3c4955surface with no lettering
- Security: the client IP is now taken from
REMOTE_ADDR;X-Forwarded-For,CF-Connecting-IPandX-Real-IPare only believed when the request arrives from a proxy declared viaJPKCOM_HIDE_LOGIN_TRUSTED_PROXIESor thejpkcom_hide_login_trusted_proxiesfilter. 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.phpand/wp-%6cogin.phpbypassed 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
Locationheader - 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 viainet_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 baremd5(), 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
- 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 raisesArgumentCountErroron PHP 8.3 - Fixed: the "View Details" modal could fail with a
TypeErrorwhen the manifest omittedrequires_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)
- 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-laterwith the HTTPS license URI
- Security: updater prefers an exact match against the manifest
download_urlover the slug heuristic, so a tampered manifest can no longer bypass the checksum gate - Security: timing-safe checksum comparison (
hash_equals()) with anis_string()guard againsthash_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_namenow provided) - Fixed PHP warning/deprecation on
wp plugin listby completing theno_updatetransient entry (new_version,package,tested,requires_php)
- 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
- 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
…
- Initial Release
- Basic login URL masking
- Simple IP blocking
- Multisite support
- Basic admin settings