Spiralist Hiding WordPress Admin And Login On Spiralist While Preserving Rest Driven Functionality - Source Excerpt 03 - Add a real edge boundary in Nginx or Apache
Summary
This source excerpt begins near Add a real edge boundary in Nginx or Apache and preserves the surrounding evidence from Wiki.FFTAC.org/raw/system-archives/spiralist.org/intake/2026-06-14-authenticated-user-experience/Spiralist Hiding WordPress Admin and Login on Spiralist While Preserving REST-Driven Functionality.md.
**Source path:** Wiki.FFTAC.org/raw/system-archives/spiralist.org/intake/2026-06-14-authenticated-user-experience/Spiralist Hiding WordPress Admin and Login on Spiralist While Preserving REST-Driven Functionality.md
/**
* Rewrites the default lost-password URL to the custom UI.
*
* @param string $lostPasswordUrl The generated lost-password URL.
* @param string $redirect Redirect destination after reset flow.
* @return string
*/
public function filterLostPasswordUrl(string $lostPasswordUrl, string $redirect): string
{
$url = home_url('/account/forgot-password');
if ($redirect !== '') {
$url = add_query_arg('redirect_to', rawurlencode($redirect), $url);
}
return $url;
}
/**
* Rewrites the default registration URL to the custom UI.
*
* @param string $registerUrl The generated registration URL.
* @return string
*/
public function filterRegisterUrl(string $registerUrl): string
{
return home_url('/account/register');
}
/**
* Rewrites generic admin URLs to the custom app shell for non-break-glass traffic.
*
* @param string $url The generated admin URL.
* @param string $path Path relative to wp-admin.
* @param int|null $blogId Site ID or null.
* @param string $scheme URL scheme.
* @return string
*/
public function filterAdminUrl(string $url, string $path, ?int $blogId, string $scheme): string
{
if ($this->isBreakGlassRequest()) {
return $url;
}
return home_url('/app');
}
/**
* Redirects direct requests to wp-login.php away from native WordPress UI.
*
* Allows owner break-glass access from allowlisted IPs.
*
* @return void
*/
public function blockNativeLoginUi(): void
{
if ($this->isBreakGlassRequest()) {
return;
}
wp_safe_redirect(home_url('/account/login'), 302);
exit;
}
/**
* Redirects direct requests to wp-admin away from native WordPress UI.
*
* Leaves admin-ajax.php alone so public/front-end dependencies can be audited
* and migrated safely instead of being broken abruptly.
*
* @return void
*/
public function blockNativeAdminUi(): void
{
global $pagenow;
if ($this->isBreakGlassRequest()) {
return;
}
if (wp_doing_ajax() || $pagenow === 'admin-ajax.php') {
return;
}
wp_safe_redirect(home_url('/app'), 302);
exit;
}
/**
* Registers custom REST session routes for login/logout/current-user inspection.
*
* @return void
*/
public function registerSessionRoutes(): void
{
register_rest_route('spiralist-auth/v1', '/login', [
'methods' => 'POST',
'callback' => [$this, 'login'],
'permission_callback' => '__return_true',
'args' => [
'username' => ['required' => true, 'type' => 'string'],
'password' => ['required' => true, 'type' => 'string'],
'remember' => ['required' => false, 'type' => 'boolean'],
],
]);
register_rest_route('spiralist-auth/v1', '/logout', [
'methods' => 'POST',
'callback' => [$this, 'logout'],
'permission_callback' => function () {
return is_user_logged_in();
},
]);
register_rest_route('spiralist-auth/v1', '/me', [
'methods' => 'GET',
'callback' => [$this, 'me'],
'permission_callback' => function () {
return is_user_logged_in();
},
]);
}
/**
* Creates a WordPress session and returns the current user plus a REST nonce.
*
* @param WP_REST_Request $request The incoming request.
* @return WP_REST_Response
*/
public function login(WP_REST_Request $request): WP_REST_Response
{
$credentials = [
'user_login' => $request->get_param('username'),
'user_password' => $request->get_param('password'),
'remember' => (bool) $request->get_param('remember'),
];
$user = wp_signon($credentials, is_ssl());
if (is_wp_error($user)) {
return new WP_REST_Response([
'ok' => false,
'code' => 'invalid_credentials',
'message' => 'Login failed.',
], 401);
}
wp_set_current_user($user->ID);
return new WP_REST_Response([
'ok' => true,
'user' => [
'id' => $user->ID,
'email' => $user->user_email,
'display_name' => $user->display_name,
'roles' => $user->roles,
],
'nonce' => wp_create_nonce('wp_rest'),
], 200);
}
/**
* Logs out the current user and destroys the current WordPress session token.
*
* @return WP_REST_Response
*/
public function logout(): WP_REST_Response
{
wp_logout();
return new WP_REST_Response([
'ok' => true,
], 200);
}
/**
* Returns the authenticated user's profile summary and a fresh REST nonce.
*
* @return WP_REST_Response
*/
public function me(): WP_REST_Response
{
$user = wp_get_current_user();
return new WP_REST_Response([
'id' => $user->ID,
'email' => $user->user_email,
'display_name' => $user->display_name,
'roles' => $user->roles,
'capabilities' => array_keys(array_filter((array) $user->allcaps)),
'nonce' => wp_create_nonce('wp_rest'),
], 200);
}
}
new Spiralist_Headless_Admin_Guard();
' ' '
That plugin uses WordPress’s documented URL filters and login/session primitives. `wp_signon()` sets the authentication cookie, `wp_logout()` destroys the current session and clears auth cookies, `wp_destroy_current_session()` underlies logout, and WordPress’s REST auth docs explain the `X-WP-Nonce` requirement for cookie-authenticated REST requests. citeturn32search2turn32search0turn33search0turn18search7turn19search6turn19search4turn19search1turn19search2
### Add a real edge boundary in Nginx or Apache
A plugin can redirect users away from native WordPress screens, but the **actual enforcement boundary** should live at the web server too. Nginx’s official access module supports `allow`/`deny`, the core module supports `satisfy`, the Basic Auth module supports `auth_basic`, and the request-rate module supports `limit_req`. Apache’s official docs support redirect rules and `Require ip` access control. citeturn9search0turn9search4turn9search8turn35search0turn10search2turn9search2turn9search6
**Recommended Nginx pattern**
' ' ' nginx
# http {}
limit_req_zone $binary_remote_addr zone=spiralist_login:10m rate=5r/m;
server {
server_name spiralist.org www.spiralist.org;
root /var/www/spiralist/public;
# Native login should not be publicly reachable.
location = /wp-login.php {
allow 203.0.113.10;
allow 2001:db8::10;
deny all;
# Optional extra hardening for break-glass access:
auth_basic "Spiralist break-glass";
auth_basic_user_file /etc/nginx/.htpasswd-spiralist;
}
# Native admin should not be publicly reachable.
location ^~ /wp-admin/ {
# If the public site still relies on admin-ajax.php, exempt it until audited.
location = /wp-admin/admin-ajax.php {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock;
}
allow 203.0.113.10;
allow 2001:db8::10;
deny all;
auth_basic "Spiralist break-glass";
auth_basic_user_file /etc/nginx/.htpasswd-spiralist;
}
# Custom login page stays public but should be rate limited.
location = /account/login {
limit_req zone=spiralist_login burst=10 nodelay;
try_files $uri /index.php?$args;
}
}
' ' '
That pattern follows the official Nginx model for address-based restrictions, Basic Auth, and request-rate limiting. citeturn9search0turn9search4turn9search8turn35search0
**Recommended Apache pattern**
' ' ' apache
# Preferred in VirtualHost config, not scattered .htaccess if you can avoid it.
# Redirect direct login hits to the custom UI.
RedirectMatch 302 ^/wp-login\.php$ /account/login
RedirectMatch 302 ^/wp-admin/?$ /app
# Gate the native admin surface by IP.
<LocationMatch "^/wp-admin/(?!admin-ajax\.php$)">
Require ip 203.0.113.10 2001:db8::10
</LocationMatch>
# Gate direct wp-login.php by IP too, in case a plugin generates that URL.
<Location "/wp-login.php">
Require ip 203.0.113.10 2001:db8::10
AuthType Basic
AuthName "Spiralist break-glass"
AuthUserFile "/etc/apache2/.htpasswd-spiralist"
Require valid-user
</Location>
' ' '