Summary
Add an admin toggle that, when enabled, makes the branded login screen also reachable at the pretty URL /login — in addition to its normal home at wp-login.php. Think of it as a minimal version of the "custom login URL" feature other login plugins offer.
Explicitly in scope / out of scope for this request:
- Links and redirects can keep pointing at
wp-login.php — that's fine. The /login URL only needs to also render the branded login screen.
- This is not about hiding/moving
wp-login.php (no security-through-obscurity). Both URLs serve the form; /login is just a friendlier entry point.
Motivation
/login is a memorable, conventional URL. Site owners frequently expect it to work and it reads better in docs, emails, and onboarding than wp-login.php.
Proposed behaviour
- New boolean setting in
magicauth_settings (e.g. enable_login_url), default off.
- When on: a request to
/login (and /login/) renders the branded login screen — the same shell/branding as the ?action=magicauth screen on wp-login.php.
- When off:
/login behaves exactly as it does today (404 or whatever the site already serves).
Feasibility — research done
I ran exploration over the current codebase. Verdict: moderate — no architectural blockers, no changes to the security model. Estimated ~75 LOC across 4 files.
How the login screen works today
Frontend\LoginScreen hooks wp-login.php exclusively via login_init, login_form_login, login_form_magicauth, etc. (includes/Frontend/LoginScreen.php).
- It does not register any rewrite rules, query vars, or
template_redirect hooks today. The only init-priority-1 hook is Auth\Controller for ?magicauth=verify.
[magicauth_login] shortcode already renders the form on an arbitrary page, but without the full branded shell — so /login should reuse LoginScreen's render path, not the shortcode's.
Recommended approach
- Register a rewrite rule
add_rewrite_rule('login/?$', 'index.php?magicauth=login_route', 'top') so WP doesn't 404.
- Add a
template_redirect handler (priority ~5) in LoginScreen that detects the /login route, emits security headers, renders the branded shell, and exits.
flush_rewrite_rules(false) on activation/deactivation in includes/Installer.php.
- Standard settings plumbing for the new toggle.
Files that would change
includes/Frontend/LoginScreen.php — template_redirect hook + render_login_route() method (~40 LOC)
includes/Installer.php — rewrite rule register/flush on activate/deactivate + slug-collision check (~20 LOC)
includes/Admin/Settings.php — new field renderer, section wiring, sanitize branch (~15 LOC)
includes/helpers.php — add the setting to defaults (~1 LOC)
_dev/decisions.md — document the decision
Conflict analysis
| Conflict |
Severity |
Mitigation |
Existing Page/Post with slug login collides with the rewrite rule |
HIGH |
Detect at activation via get_page_by_path('login'); surface an admin notice instead of silently shadowing the post. Consider also refusing to enable the toggle while a login post exists. |
Security headers (X-Frame-Options: DENY, CSP: frame-ancestors 'none', X-Content-Type-Options: nosniff) are emitted in login_init — that hook does not fire on a front-end route |
HIGH |
The template_redirect handler must re-emit the exact same headers before rendering. Easy to forget — call it out in code review and tests. |
| Rewrite rule not flushed |
MEDIUM |
Flush on activate/deactivate. Without it, /login 404s until someone re-saves permalinks. |
?magicauth=off opt-out cookie is path-scoped to /wp-login.php, so it won't be read on /login |
LOW |
Recovery stack is not broken — the always-visible "Sign in with password" link and MAGICAUTH_DISABLE constant still work. Just document that the cookie opt-out is wp-login.php-scoped. |
Multisite subdirectory behaviour of a /login rule |
NOTE ONLY |
Multisite is out of scope for v1.0; document for later. |
| Throttling, user-enumeration timing jitter, TOCTOU token consumption |
NONE |
All route-agnostic — throttling is keyed on IP/email hashes, jitter is applied at response time, token consumption is atomic. Serving the form at a second URL changes none of this. |
Acceptance criteria
Researched and filed by @r00bbert. Feature scoped as minimal — both URLs serve the form, no wp-login.php hiding.
Summary
Add an admin toggle that, when enabled, makes the branded login screen also reachable at the pretty URL
/login— in addition to its normal home atwp-login.php. Think of it as a minimal version of the "custom login URL" feature other login plugins offer.Explicitly in scope / out of scope for this request:
wp-login.php— that's fine. The/loginURL only needs to also render the branded login screen.wp-login.php(no security-through-obscurity). Both URLs serve the form;/loginis just a friendlier entry point.Motivation
/loginis a memorable, conventional URL. Site owners frequently expect it to work and it reads better in docs, emails, and onboarding thanwp-login.php.Proposed behaviour
magicauth_settings(e.g.enable_login_url), default off./login(and/login/) renders the branded login screen — the same shell/branding as the?action=magicauthscreen onwp-login.php./loginbehaves exactly as it does today (404 or whatever the site already serves).Feasibility — research done
I ran exploration over the current codebase. Verdict: moderate — no architectural blockers, no changes to the security model. Estimated ~75 LOC across 4 files.
How the login screen works today
Frontend\LoginScreenhookswp-login.phpexclusively vialogin_init,login_form_login,login_form_magicauth, etc. (includes/Frontend/LoginScreen.php).template_redirecthooks today. The onlyinit-priority-1 hook isAuth\Controllerfor?magicauth=verify.[magicauth_login]shortcode already renders the form on an arbitrary page, but without the full branded shell — so/loginshould reuseLoginScreen's render path, not the shortcode's.Recommended approach
add_rewrite_rule('login/?$', 'index.php?magicauth=login_route', 'top')so WP doesn't 404.template_redirecthandler (priority ~5) inLoginScreenthat detects the/loginroute, emits security headers, renders the branded shell, and exits.flush_rewrite_rules(false)on activation/deactivation inincludes/Installer.php.Files that would change
includes/Frontend/LoginScreen.php—template_redirecthook +render_login_route()method (~40 LOC)includes/Installer.php— rewrite rule register/flush on activate/deactivate + slug-collision check (~20 LOC)includes/Admin/Settings.php— new field renderer, section wiring, sanitize branch (~15 LOC)includes/helpers.php— add the setting to defaults (~1 LOC)_dev/decisions.md— document the decisionConflict analysis
logincollides with the rewrite ruleget_page_by_path('login'); surface an admin notice instead of silently shadowing the post. Consider also refusing to enable the toggle while aloginpost exists.X-Frame-Options: DENY,CSP: frame-ancestors 'none',X-Content-Type-Options: nosniff) are emitted inlogin_init— that hook does not fire on a front-end routetemplate_redirecthandler must re-emit the exact same headers before rendering. Easy to forget — call it out in code review and tests./login404s until someone re-saves permalinks.?magicauth=offopt-out cookie is path-scoped to/wp-login.php, so it won't be read on/loginMAGICAUTH_DISABLEconstant still work. Just document that the cookie opt-out iswp-login.php-scoped./loginruleAcceptance criteria
enable_login_urltoggle in Settings, default off./loginand/login/render the branded login screen with full branding shell./loginis untouched (no rewrite rule active)./loginresponse carries the same security headers aswp-login.php(X-Frame-Options, CSPframe-ancestors,X-Content-Type-Options).loginpost/page and warns rather than silently shadowing it.?magicauth=offlink, password link) verified to still work when reaching the form via/login.Researched and filed by @r00bbert. Feature scoped as minimal — both URLs serve the form, no
wp-login.phphiding.