buyer: single-operator password gate (BUYER_PASSWORD + signed session cookie)
Adds an Express requireAuth middleware on /v1/* + the SPA shell and a
Vite/React login screen so anyone who reaches mp-buyers.* must authenticate
before driving the operator's wallet via /v1/submit-prompt or /v1/accept.
Why: the buyer-app sits on a public HTTP port and signs txs with a
server-held private key. Until now the only gate was network reachability.
Mechanics:
- BUYER_PASSWORD + SESSION_SECRET (>=32 chars) loaded by config.ts and
surfaced as createApp() deps. Production refuses to start without both.
- buyer/src/auth.ts: HMAC-SHA256 signed buyer_session cookie (HttpOnly,
SameSite=Lax, Path=/, optional Secure), 7-day rolling expiry,
timing-safe password compare, in-memory 5/15-min per-IP rate limiter.
- /v1/auth/{login,logout,whoami}. Login is rate-limited; 6th wrong
attempt returns 429 + Retry-After. Success resets the bucket.
- SPA boot block (__BUYER_BOOT__ injected by server.ts) is gated by the
same cookie so the buyer's address never reaches an unauthenticated
browser.
- New <AuthProvider>/<RequireAuth> in buyer/src/ui/state/AuthContext.tsx
probes /v1/auth/whoami on mount and a scoped window.fetch patch drops
to the login screen on any mid-session 401 from /v1/* (excluding
/v1/auth/* so wrong-password 401s stay local to <Login>).
- Tailwind login card at buyer/src/ui/pages/Login.tsx. App.tsx wraps in
<RequireAuth> and adds a "Sign out" button in the nav.
Tests: tests/unit/buyer-server-auth.test.ts covers sign/verify, rate
limiter, cookie helpers, login flow (400/401/204/429), whoami
(no-cookie/valid/tampered/expired), rolling renewal, logout, gated
/v1/*, /healthz still open, and the no-auth-deps construction shape.
Updated buyer-server-config + buyer-ui-smoke for the new required env
vars and the AuthProvider wrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,14 @@ BUYER_PRIV_KEY_HEX=PASTE_64_HEX_CHARS_HERE
|
||||
# Indexer base URL. Inside compose this is the service name.
|
||||
INDEXER_URL=http://indexer:8090
|
||||
|
||||
# Single-operator login password. Anyone who knows this can spend funds
|
||||
# via /v1/submit-prompt + /v1/accept, so treat it like BUYER_PRIV_KEY_HEX.
|
||||
BUYER_PASSWORD=CHANGE_ME
|
||||
|
||||
# HMAC key for the buyer_session cookie (HttpOnly, SameSite=Lax, 7-day rolling).
|
||||
# Minimum 32 chars. Generate with: openssl rand -hex 32
|
||||
SESSION_SECRET=PASTE_64_HEX_CHARS_HERE_OR_LONGER
|
||||
|
||||
# ─── Optional ─────────────────────────────────────────────────────────────
|
||||
|
||||
# HTTP listen port (default 3002 in buyer/src/config.ts; we override to
|
||||
@@ -26,6 +34,10 @@ BUYER_PORT=8070
|
||||
# See ARCHITECTURE.md §9 follow-up — naming drift to be reconciled.)
|
||||
NETWORK_ID=0
|
||||
|
||||
# Set "0" for plain-HTTP local dev so the buyer_session cookie still rides
|
||||
# along over loopback. Production behind TLS should leave this unset / "1".
|
||||
# COOKIE_SECURE=1
|
||||
|
||||
# ─── Optional: CIP-33 reference scripts ───────────────────────────────────
|
||||
# When set, buyer-signed script-spending txs (Accept, Reclaim) read the
|
||||
# escrow validator from an on-chain reference UTxO instead of inlining
|
||||
|
||||
Reference in New Issue
Block a user