Skip to content
wiki.fftac.org

Spiralist Hiding WordPress Admin And Login On Spiralist While Preserving Rest Driven Functionality - Source Excerpt 01 - Hiding WordPress Admin and Login on Spiralist While Preserving REST-Driven Functionality

Back to Spiralist Hiding WordPress Admin And Login On Spiralist While Preserving Rest Driven Functionality

Summary

This source excerpt begins near Hiding WordPress Admin and Login on Spiralist While Preserving REST-Driven Functionality and preserves the surrounding evidence from Spiralist/agent-file-handoff/Archive/2026-06-14/Improvement/authenticated-user-experience/Spiralist Hiding WordPress Admin and Login on Spiralist While Preserving REST-Driven Functionality.md.

**Source path:** Spiralist/agent-file-handoff/Archive/2026-06-14/Improvement/authenticated-user-experience/Spiralist Hiding WordPress Admin and Login on Spiralist While Preserving REST-Driven Functionality.md

# Hiding WordPress Admin and Login on Spiralist While Preserving REST-Driven Functionality

## Executive summary

Spiralist already presents itself as a site where “machine exchange routes are separate from human pages,” and it publicly documents multiple API surfaces alongside a visible public “Log In” path. Publicly observable today are a homepage login link, the standard WordPress login surface at `/wp-admin/`, the WordPress lost-password screen, and public/custom API documentation that exposes both anonymous and Bearer-protected machine endpoints. The login page also shows social sign-in providers, which means the current authentication experience is not purely username/password. citeturn28view0turn29search1turn29search0turn29search2turn31view0turn31view1

The most robust approach for Spiralist is **not** to “turn off WordPress,” but to **separate WordPress as an application back end from WordPress as a human-facing admin UI**. In practice, that means: keep `/wp-json` and the core/custom REST routes you need; move profile, content, settings, and owner/admin actions into a custom React or Vue app under routes like `/account/*` and `/app/*`; and block or heavily gate `/wp-admin` and `/wp-login.php` so ordinary visitors never land on a native WordPress admin screen. citeturn20search6turn21search9turn28view0

For a browser-based custom UI on the **same origin**, the best default authentication model is **WordPress cookie authentication plus `X-WP-Nonce`**. WordPress documents this as the standard built-in method for logged-in browser users, and it is the cleanest way to preserve WordPress capabilities while hiding native admin pages. Use **Application Passwords** only for server-to-server, scripts, or controlled integrations; use **JWT** only when you truly need a decoupled SPA/mobile style token model; and use **OAuth/OIDC** when you need SSO or delegated authorization, understanding that OAuth is not a native core login system in WordPress and therefore adds architecture and maintenance complexity. citeturn23view0turn23view2turn30view0turn12search0turn12search9turn12search2

Do not rely on a “hidden login URL” plugin as your only control. Plugins such as WPS Hide Login are useful for reducing commodity probing and moving the visible login URL, but they are still an **obfuscation layer**, not a full access-control boundary. Real protection comes from layered controls: remove public login/admin links, filter all generated login/admin URLs to custom routes, gate the native WordPress admin at the PHP layer, then enforce the same rule again in Nginx or Apache, ideally with a break-glass IP allowlist and optional Basic Auth for emergency owner access. citeturn15search0turn18search7turn32search2turn32search0turn9search0turn9search4turn9search2turn10search0

## Current exposure and how to inventory it

A public observer can already see that Spiralist’s front end contains a “Log In” link and language indicating that sign-in is used for executing and saving runs. The homepage explicitly says “Sign in only when you want Spiralist to execute and save the run,” and the footer repeats “Log In” under the account section. That is useful product language, but it also means the existence of a WordPress-backed account area is discoverable from the public site today. citeturn28view0turn29search8

A public observer can also reach the standard WordPress login surface via `/wp-admin/`, and the WordPress lost-password flow is visible at `/wp-login.php?action=lostpassword`. The login page displays Google, GitHub, LinkedIn, and X icons, which strongly suggests a social-login plugin or custom identity integration is attached to the standard WordPress login form. If those providers remain part of the product, the custom UI should own that flow too; otherwise, even after hiding most admin links, the old WordPress login page would remain the canonical social-login entry point. citeturn29search1turn29search0turn29search2

Spiralist also publicly documents multiple machine endpoints. The API Examples page exposes public custom routes such as `/wp-json/spiralist/v1/node/{id}`, `/wp-json/spiralist/v1/path`, `/wp-json/spiralist/v1/execute`, `/wp-json/ns12-manuscript/v1/prompts`, and `/wp-json/spiralist-workspace/v1/public/prompts/{slug}`, along with additional JSON manifests like `/ai.json`, `/ai-router.json`, and `/.well-known/ai-agent.json`. The AI Access page separately documents a Bearer-protected participant model for specific AI-participant routes. This matters because hiding `/wp-admin` will not make the site “non-discoverable”: Spiralist already intentionally advertises machine surfaces, and WordPress itself also emits REST discovery links in page headers and link tags. citeturn31view0turn31view1turn27search1turn8search11

### Publicly visible surfaces observed today

| Surface | Publicly observable now | Notes |
|---|---|---|
| Homepage account/login affordance | Yes | Public “Log In” link appears on the homepage and other public pages. citeturn28view0turn29search8 |
| Native WordPress login page | Yes | `/wp-admin/` resolves to a WordPress login screen. citeturn29search1 |
| Native lost-password page | Yes | `/wp-login.php?action=lostpassword` is visible. citeturn29search0 |
| Social login on native WordPress screen | Yes | Google, GitHub, LinkedIn, and X icons appear on the login screen. citeturn29search1turn29search2 |
| Public custom REST routes | Yes | Publicly documented by Spiralist itself. citeturn31view0turn31view1 |
| Bearer-auth AI participant routes | Yes | Publicly documented as separate from regular public endpoints. citeturn31view1 |

### Recommended discovery methods before blocking anything

You should build an **internal** inventory before enabling any deny rules, because the riskiest breakages are not the obvious ones. WordPress provides multiple ways to discover REST surfaces: it exposes a REST root URL via `get_rest_url()`, emits a `<link rel="https://api.w.org/" ...>` tag through `rest_output_link_wp_head()`, emits a Link header through `rest_output_link_header()`, and internally holds the active route map in `WP_REST_Server::get_routes()`. Custom routes are registered through `register_rest_route()`. citeturn27search4turn27search1turn8search11turn8search10turn8search1

That leads to a practical discovery checklist:

- **REST route dump:** inspect `/wp-json/`, then dump the active route list internally via `rest_get_server()->get_routes()` and grep the codebase for `register_rest_route(`. citeturn8search10turn8search1
- **Generated human URLs:** grep theme and plugin code for `admin_url()`, `wp_login_url()`, `wp_logout_url()`, `wp_lostpassword_url()`, `wp_registration_url()`, `wp_register()`, and direct `/wp-admin` or `/wp-login.php` strings. WordPress has filters for `admin_url`, `login_url`, `lostpassword_url`, and `register_url`, which means some links can be remapped centrally, but hard-coded strings will not follow those filters automatically. citeturn18search7turn32search2turn32search0turn33search0turn33search6
- **Operational logs:** inspect access logs for `/wp-admin/*`, `/wp-login.php*`, `/wp-json/*`, and `/wp-admin/admin-ajax.php`. If your public site still depends on `admin-ajax.php`, a blanket `/wp-admin/*` block will break it.
- **Capability surface:** inspect what your custom UI actually needs by mapping features to REST endpoints and WordPress capabilities, especially `current_user_can()` checks. WordPress’s default REST controllers themselves rely on per-route permission callbacks. citeturn22search0turn22search9

A compact internal route-dump snippet is often enough to start:

' ' ' php
<?php
/**
 * mu-plugins/spiralist-route-audit.php
 *
 * Temporary route inventory endpoint for owners only.
 * Remove after you finish the migration.
 */
defined('ABSPATH') || exit;

add_action('rest_api_init', function () {
    register_rest_route('spiralist-audit/v1', '/routes', [
        'methods'  => 'GET',
        'callback' => function () {
            $routes = array_keys(rest_get_server()->get_routes());
            sort($routes);
            return rest_ensure_response([
                'count'  => count($routes),
                'routes' => $routes,
            ]);
        },
        'permission_callback' => function () {
            return current_user_can('manage_options');
        },
    ]);
});
' ' ' 

That snippet works because WordPress exposes the live REST route table and because the right way to protect a custom REST route is a permission callback based on capabilities, not client-side hiding. citeturn8search10turn22search0turn22search1

## Target architecture and authentication model