feat: Sprint 19 — password auth, security headers, login page

Auth system (off by default, zero friction for localhost):
- New api/auth.py module: password hashing (SHA-256 + STATE_DIR salt),
  signed HMAC session cookies (24h TTL), auth middleware
- Enable via HERMES_WEBUI_PASSWORD env var or Settings panel
- Minimal dark-themed login page at /login (self-contained HTML)
- POST /api/auth/login, /api/auth/logout, GET /api/auth/status
- Settings panel: "Access Password" field + "Sign Out" button
- password_hash added to settings.json (null = auth disabled)

Security hardening:
- Security headers on all responses: X-Content-Type-Options: nosniff,
  X-Frame-Options: DENY, Referrer-Policy: same-origin
- POST body size limit: 20MB cap in read_body() to prevent DoS

Closes #23. 9 new tests. Total: 304 passed, 0 regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nathan Esquenazi
2026-04-03 05:53:26 -07:00
parent 1c6db07c2b
commit b8b62722ec
8 changed files with 417 additions and 1 deletions

View File

@@ -282,7 +282,13 @@
<option value="ctrl+enter">Ctrl+Enter (Enter for newline)</option>
</select>
</div>
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
<label for="settingsPassword">Access Password</label>
<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Set a password to require login. Leave blank to disable auth.</div>
<input type="password" id="settingsPassword" placeholder="Enter new password (or blank to disable)" style="width:100%;padding:8px;background:var(--code-bg);color:var(--text);border:1px solid var(--border2);border-radius:6px;font-size:13px">
</div>
<button class="sm-btn" onclick="saveSettings()" style="margin-top:12px;width:100%;padding:8px;font-weight:600">Save Settings</button>
<button class="sm-btn" id="btnSignOut" onclick="signOut()" style="margin-top:6px;width:100%;padding:8px;font-weight:600;color:var(--accent);border-color:rgba(233,69,96,.3);display:none">Sign Out</button>
</div>
</div>
</div>