Skip to content
wiki.fftac.org

Spiralist Hiding WordPress Admin And Login On Spiralist While Preserving Rest Driven Functionality - Source Excerpt 02 - Authentication options compared

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

Summary

This source excerpt begins near Authentication options compared 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

The cleanest target state for Spiralist is a **same-origin app shell**: public users browse normal pages on `https://spiralist.org/...`; authenticated users use a custom UI on paths like `https://spiralist.org/account/login`, `https://spiralist.org/account/profile`, and `https://spiralist.org/app/*`; WordPress stays behind the scenes as the content, user, settings, and plugin back end over REST; and `/wp-admin` plus `/wp-login.php` become break-glass owner-only surfaces. That preserves the power of WordPress while removing the native WordPress interface from ordinary visitor journeys. citeturn20search6turn23view0turn18search7

' ' ' mermaid
flowchart LR
    Visitor[Regular visitor] --> Public[Public Spiralist pages]
    Member[Authenticated member/owner] --> UI[Custom React or Vue UI]
    UI --> Session[Custom session endpoints]
    UI --> CoreREST[WordPress core REST API]
    UI --> CustomREST[Spiralist custom REST endpoints]
    Session --> WP[WordPress runtime]
    CoreREST --> WP
    CustomREST --> WP
    Owner[Owner break-glass access] --> Gate[IP allowlist or Basic Auth]
    Gate --> Native[Native /wp-admin and /wp-login.php]
' ' ' 

For the browser UI, **cookie authentication with `X-WP-Nonce`** is the best default if the app lives on the same origin. WordPress explicitly documents cookie auth as the standard built-in method, and the REST API accepts the nonce either as `_wpnonce` or in the `X-WP-Nonce` header. If no nonce is supplied, WordPress treats the request as unauthenticated even if the browser is otherwise logged in. WordPress also refreshes the REST nonce in some cookie-auth flows. citeturn23view0turn19search2turn19search12

For non-browser integrations, **Application Passwords** are strongly preferable to sharing a user’s main password. WordPress describes them as revocable, per-application credentials for programmatic access, stored hashed, and explicitly not valid for interactive `wp-admin` login. WordPress ships both the feature and REST endpoints for creating, listing, introspecting, updating, and deleting them, including last-used time and last IP address. citeturn23view2turn25view4

For truly decoupled SPAs, mobile apps, or separate origins, **JWT** is a viable pattern, but not a native WordPress core feature. JWT is a standard token format defined by RFC 7519, and WordPress’s own REST auth hook documentation makes clear that sites can run multiple auth methods, including OAuth, in parallel. In WordPress practice, JWT and OAuth typically come from plugins or an external identity layer. That makes them powerful, but also means more attack surface, more session-revocation design work, and more operational complexity than same-origin cookie auth. citeturn12search0turn30view0turn13search4turn12search9turn12search2

### Authentication options compared

| Method | Best fit | Strengths | Main risks or drawbacks | Spiralist recommendation |
|---|---|---|---|---|
| **Cookie auth + `X-WP-Nonce`** | Same-origin React/Vue app | Native to WordPress; best fit for browser sessions; works naturally with capability checks and logged-in REST requests. citeturn23view0turn19search2 | Requires same-origin or careful cookie/CORS handling; CSRF protections must be respected; nonces are not authorization. citeturn19search14turn11search0turn40view0 | **Primary choice** for `/account/*` and `/app/*` on `spiralist.org`. |
| **Application Passwords** | Server-to-server, scripts, CI, maintenance tools | Revocable per app; hashed in WordPress; separate from main password; introspection includes last-used metadata. citeturn23view2turn25view4 | Uses HTTP Basic over HTTPS; not suitable for human browser login UX; scope is still user identity, not a delegated browser session. citeturn23view2turn23view1 | Use for owner tools, deployment automations, external integrations. Not for visitor/member front-end login. |
| **JWT** | Cross-origin SPA or mobile client | Stateless token model; good for APIs and mobile apps; standardized format. citeturn12search0 | Not core WordPress auth; storage/revocation/refresh design is on you; plugin/custom implementation risk. citeturn30view0turn13search4 | Secondary option only if same-origin cookie auth is not feasible. |
| **OAuth or OIDC** | SSO, delegated authorization, enterprise identity | Industry-standard delegated authorization; ideal for external IdP, SSO, and social/enterprise identity. citeturn12search9turn12search2 | More moving parts; not native as a complete WordPress core browser-login replacement; requires plugin or external identity layer. citeturn30view0 | Use if Spiralist is moving to a true SSO/IdP architecture, not merely hiding WordPress screens. |

### Recommended flows for Spiralist

There are effectively **two identity stories** on Spiralist right now: a WordPress account/login flow and a documented Bearer-key participant flow for AI-related endpoints. Keep those separate in the UI and in code. The custom human UI should authenticate WordPress users via a browser session. The AI participant flow can remain its own Bearer-key model if that is product-correct. Mixing those two user types into one token story would make future security reviews and support much harder. citeturn29search1turn31view1

' ' ' mermaid
sequenceDiagram
    participant U as User
    participant SPA as Custom UI
    participant WP as WordPress custom session route
    participant API as /wp-json/wp/v2

    U->>SPA: Open /account/login
    SPA->>WP: POST /wp-json/spiralist-auth/v1/login
    WP-->>SPA: Set-Cookie + JSON { user, nonce, capabilities }
    SPA->>API: GET /wp/v2/users/me?context=edit with credentials + X-WP-Nonce
    API-->>SPA: User profile, roles, capabilities
    SPA->>API: POST /wp/v2/posts or /wp/v2/settings
    API-->>SPA: Success / validation error
' ' ' 

## Step-by-step implementation plan

### Move all human auth and admin journeys into custom routes

Create custom public routes first, before you block anything:

- `/account/login`
- `/account/forgot-password`
- `/account/reset-password`
- `/account/profile`
- `/account/security`
- `/app`
- `/app/content`
- `/app/media`
- `/app/settings`
- `/app/plugins` or `/app/system` for owner-only features

Then remap all WordPress-generated URLs so theme/plugin code that uses WordPress helper functions points to your custom UI rather than native WordPress screens. WordPress provides official filters for login, lost-password, registration, and admin URLs. citeturn32search2turn32search0turn33search0turn18search7

' ' ' php
<?php
/**
 * Plugin Name: Spiralist Headless Admin Guard
 * Description: Hides native WordPress admin/login surfaces behind a custom app UI.
 */

defined('ABSPATH') || exit;

final class Spiralist_Headless_Admin_Guard
{
    /**
     * Owner break-glass IP allowlist.
     *
     * Replace with your real office/home/VPN IPs.
     *
     * @var string[]
     */
    private array $ownerIps = [
        '203.0.113.10',
        '2001:db8::10',
    ];

    public function __construct()
    {
        // Hide the front-end admin bar entirely.
        add_filter('show_admin_bar', '__return_false');

        // Repoint generated WordPress URLs to the custom UI.
        add_filter('login_url', [$this, 'filterLoginUrl'], 10, 3);
        add_filter('lostpassword_url', [$this, 'filterLostPasswordUrl'], 10, 2);
        add_filter('register_url', [$this, 'filterRegisterUrl']);
        add_filter('admin_url', [$this, 'filterAdminUrl'], 10, 4);

        // Block native WordPress human UI routes.
        add_action('login_init', [$this, 'blockNativeLoginUi']);
        add_action('admin_init', [$this, 'blockNativeAdminUi']);

        // Register custom session routes.
        add_action('rest_api_init', [$this, 'registerSessionRoutes']);
    }

    /**
     * Returns true when the current request is from an owner break-glass IP.
     *
     * @return bool
     */
    private function isBreakGlassRequest(): bool
    {
        $ip = $_SERVER['REMOTE_ADDR'] ?? '';
        return in_array($ip, $this->ownerIps, true);
    }

    /**
     * Rewrites the default login URL to the custom login screen.
     *
     * @param string $loginUrl The generated WordPress login URL.
     * @param string $redirect Redirect destination after login.
     * @param bool   $forceReauth Whether reauth is forced.
     * @return string
     */
    public function filterLoginUrl(string $loginUrl, string $redirect, bool $forceReauth): string
    {
        $url = home_url('/account/login');
        if ($redirect !== '') {
            $url = add_query_arg('redirect_to', rawurlencode($redirect), $url);
        }
        if ($forceReauth) {
            $url = add_query_arg('reauth', '1', $url);
        }
        return $url;
    }