From f724d9aaa569139a1d644c25f3987f7759fe35a8 Mon Sep 17 00:00:00 2001 From: jagdeep sidhu Date: Fri, 24 Apr 2026 20:46:07 -0700 Subject: [PATCH] harden(auth): default credentialed API to same origin Use same-origin relative authenticated API paths in production so CSRF and host-only session cookies match the intended reverse-proxy deployment. Made-with: Cursor --- README.md | 9 ++++++--- src/lib/apiClient.js | 29 +++++++++++++---------------- src/lib/apiClient.test.js | 24 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4e3a493..6dd79b9 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,13 @@ Sysnode is designed to make that information easier to read, easier to verify, a ## Data Source -The frontend reads live dashboard data from the Sysnode backend API. The default production build targets: +The frontend reads live dashboard data from the Sysnode backend API. Anonymous public data can still be retargeted with `REACT_APP_API_BASE`, but the authenticated custody surface (`/auth`, `/vault`, `/gov`) is designed for same-origin production deployment: ```text -https://syscoin.dev +https://sysnode.info/ -> SPA +https://sysnode.info/auth/* -> backend +https://sysnode.info/vault/* -> backend +https://sysnode.info/gov/* -> backend ``` The backend aggregates data from a Syscoin Core node, Sentry Node RPC responses, market APIs, and supporting network datasets. For a fork or private deployment, override the API base URL at build time (no code change required): @@ -41,7 +44,7 @@ The backend aggregates data from a Syscoin Core node, Sentry Node RPC responses, REACT_APP_API_BASE=https://your-backend.example npm run build ``` -The value is read at build time by both `src/lib/apiClient.js` (authenticated surface) and `src/lib/api.js` (anonymous surface — superblock timing, governance feed, masternode stats); without it, development builds use `http://localhost:3001` and production builds use `https://syscoin.dev`. Keeping both clients on the same override means `REACT_APP_API_BASE` retargets the entire app in a single build. +The value is read at build time by both `src/lib/apiClient.js` (authenticated surface) and `src/lib/api.js` (anonymous surface — superblock timing, governance feed, masternode stats). Without it, development authenticated requests use `http://localhost:3001`; production authenticated requests use same-origin relative paths. Keeping the authenticated API under the SPA origin preserves host-only `Secure; SameSite=Lax` cookies and lets the SPA mirror the CSRF cookie into `X-CSRF-Token` without cross-site credentialed fetches. ## Getting Started diff --git a/src/lib/apiClient.js b/src/lib/apiClient.js index b15549a..9987149 100644 --- a/src/lib/apiClient.js +++ b/src/lib/apiClient.js @@ -20,25 +20,22 @@ import axios from 'axios'; // Default API base URL. // -// DO NOT fall back to `window.location.origin`. The sysnode-info SPA -// and the sysnode-backend are hosted on different origins — the SPA -// server only serves static files, it does not proxy /auth/* anywhere -// — so defaulting to the current browser origin makes login, register, -// and session hydration silently hit the wrong host and fail unless -// an operator remembers to set `REACT_APP_API_BASE`. (Codex round 5 -// P1.) -// // Priority: // 1. REACT_APP_API_BASE (build-time override for bespoke deployments) -// 2. Production builds → https://syscoin.dev (same host as the legacy -// public client in `./api.js`, which is where sysnode-backend is -// reachable) +// 2. Production builds → same-origin relative paths. Production must +// reverse-proxy /auth, /vault, and /gov under the SPA origin so +// host-only SameSite=Lax cookies and the readable csrf cookie work +// without cross-site credentialed fetches. // 3. Development builds → http://localhost:3001 (backend dev server) -const DEFAULT_BASE = - process.env.REACT_APP_API_BASE || - (process.env.NODE_ENV === 'production' - ? 'https://syscoin.dev' - : 'http://localhost:3001'); +export function resolveDefaultApiBase({ + apiBase = process.env.REACT_APP_API_BASE, + nodeEnv = process.env.NODE_ENV, +} = {}) { + if (apiBase) return apiBase; + return nodeEnv === 'production' ? '' : 'http://localhost:3001'; +} + +const DEFAULT_BASE = resolveDefaultApiBase(); const STATE_CHANGING = /^(POST|PUT|PATCH|DELETE)$/i; diff --git a/src/lib/apiClient.test.js b/src/lib/apiClient.test.js index 57e8e19..3d5750d 100644 --- a/src/lib/apiClient.test.js +++ b/src/lib/apiClient.test.js @@ -4,6 +4,7 @@ import { createApiClient, parseRetryAfter, readCsrfCookie, + resolveDefaultApiBase, setAuthLostHandler, toApiError, } from './apiClient'; @@ -38,6 +39,29 @@ describe('readCsrfCookie', () => { }); }); +describe('resolveDefaultApiBase', () => { + test('uses explicit REACT_APP_API_BASE override', () => { + expect( + resolveDefaultApiBase({ + apiBase: 'https://api.example.test', + nodeEnv: 'production', + }) + ).toBe('https://api.example.test'); + }); + + test('defaults production authenticated calls to same-origin relative paths', () => { + expect(resolveDefaultApiBase({ apiBase: '', nodeEnv: 'production' })).toBe( + '' + ); + }); + + test('keeps localhost backend default for development', () => { + expect(resolveDefaultApiBase({ apiBase: '', nodeEnv: 'development' })).toBe( + 'http://localhost:3001' + ); + }); +}); + describe('createApiClient — CSRF attachment', () => { test('attaches X-CSRF-Token to state-changing methods', async () => { const client = createApiClient({