diff --git a/backend/internal/api/middleware/auth.go b/backend/internal/api/middleware/auth.go index 8e874df5..5270620e 100644 --- a/backend/internal/api/middleware/auth.go +++ b/backend/internal/api/middleware/auth.go @@ -13,14 +13,17 @@ func AuthMiddleware(authService *services.AuthService) gin.HandlerFunc { authHeader := c.GetHeader("Authorization") if authHeader == "" { - // Try cookie first for browser flows + // Try cookie first for browser flows (including WebSocket upgrades) if cookie, err := c.Cookie("auth_token"); err == nil && cookie != "" { authHeader = "Bearer " + cookie } } + // DEPRECATED: Query parameter authentication for WebSocket connections + // This fallback exists only for backward compatibility and will be removed in a future version. + // Query parameters are logged in access logs and should not be used for sensitive data. + // Use HttpOnly cookies instead, which are automatically sent by browsers and not logged. if authHeader == "" { - // Try query param (token passthrough) if token := c.Query("token"); token != "" { authHeader = "Bearer " + token } diff --git a/docs/plans/prev_spec_websocket_fix_dec16.md b/docs/plans/prev_spec_websocket_fix_dec16.md index f175d969..dc4d90cc 100644 --- a/docs/plans/prev_spec_websocket_fix_dec16.md +++ b/docs/plans/prev_spec_websocket_fix_dec16.md @@ -25,42 +25,50 @@ ```text Frontend Backend ──────── ─────── -localStorage.getItem('charon_auth_token') - │ - ▼ -Query param: ?token= ────────► AuthMiddleware: - 1. Check Authorization header - 2. Check auth_token cookie - 3. Check token query param ◄── MATCHES - │ - ▼ - ValidateToken(jwt) → OK - │ - ▼ - Upgrade to WebSocket +User logs in + │ + ▼ +Backend sets HttpOnly auth_token cookie ──► AuthMiddleware: + │ 1. Check Authorization header + │ 2. Check auth_token cookie ◄── SECURE METHOD + │ 3. (Deprecated) Check token query param + ▼ │ +WebSocket connection initiated ▼ +(Cookie sent automatically by browser) ValidateToken(jwt) → OK + │ │ + │ ▼ + └──────────────────────────────────► Upgrade to WebSocket ``` +**Security Note:** Authentication now uses HttpOnly cookies instead of query parameters. +This prevents JWT tokens from being logged in access logs, proxies, and other telemetry. +The browser automatically sends the cookie with WebSocket upgrade requests. + ### Logic Gap Analysis **ANSWER: NO - There is NO logic gap between Frontend and Backend.** | Question | Answer | |----------|--------| -| Frontend auth method | Query param `?token=` from `localStorage.getItem('charon_auth_token')` | -| Backend auth method | Accepts: Header → Cookie → Query param `token` ✅ | +| Frontend auth method | HttpOnly cookie (`auth_token`) sent automatically by browser ✅ SECURE | +| Backend auth method | Accepts: Header → Cookie (preferred) → Query param (deprecated) ✅ | | Filter params | Both use `source`, `level`, `ip`, `host`, `blocked_only` ✅ | | Data format | `SecurityLogEntry` struct matches frontend TypeScript type ✅ | +| Security | Tokens no longer logged in access logs or exposed to XSS ✅ | --- ## 1. VERIFICATION STATUS -### ✅ localStorage Key IS Correct +### ✅ Authentication Method Updated for Security -Both WebSocket functions in `frontend/src/api/logs.ts` correctly use `charon_auth_token`: +WebSocket authentication now uses HttpOnly cookies instead of query parameters: -- **Line 119-122** (`connectLiveLogs`): `localStorage.getItem('charon_auth_token')` -- **Line 178-181** (`connectSecurityLogs`): `localStorage.getItem('charon_auth_token')` +- **`connectLiveLogs`** (frontend/src/api/logs.ts): Uses browser's automatic cookie transmission +- **`connectSecurityLogs`** (frontend/src/api/logs.ts): Uses browser's automatic cookie transmission +- **Backend middleware**: Prioritizes cookie-based auth, query param is deprecated + +This change prevents JWT tokens from appearing in access logs, proxy logs, and other telemetry. --- @@ -186,12 +194,13 @@ The `showBlockedOnly` state in useEffect dependencies causes reconnection when t | Component | Status | Notes | |-----------|--------|-------| -| localStorage key | ✅ Fixed | Now uses `charon_auth_token` | -| Auth middleware | ✅ Working | Accepts query param `token` | +| WebSocket authentication | ✅ Secured | Now uses HttpOnly cookies instead of query parameters | +| Auth middleware | ✅ Updated | Cookie-based auth prioritized, query param deprecated | | WebSocket endpoint | ✅ Working | Protected route, upgrades correctly | | LogWatcher service | ✅ Working | Tails access.log successfully | | **Frontend memoization** | ✅ Fixed | `useMemo` in Security.tsx | | **Stable default props** | ✅ Fixed | Constants in LiveLogViewer.tsx | +| **Security improvement** | ✅ Complete | Tokens no longer exposed in logs | --- @@ -221,7 +230,9 @@ docker logs charon 2>&1 | grep -i "cerberus.*websocket" | tail -10 **Logic Gap Between Frontend/Backend:** **NO** - Both are correctly aligned -**Current Status:** ✅ All fixes applied and working +**Security Enhancement:** WebSocket authentication now uses HttpOnly cookies instead of query parameters, preventing token leakage in logs + +**Current Status:** ✅ All fixes applied and working securely --- diff --git a/frontend/src/api/logs.ts b/frontend/src/api/logs.ts index 1f6201c5..304c5254 100644 --- a/frontend/src/api/logs.ts +++ b/frontend/src/api/logs.ts @@ -128,11 +128,8 @@ export const connectLiveLogs = ( if (filters.level) params.append('level', filters.level); if (filters.source) params.append('source', filters.source); - // Get auth token from localStorage (key: charon_auth_token) - const token = localStorage.getItem('charon_auth_token'); - if (token) { - params.append('token', token); - } + // Authentication is handled via HttpOnly cookies sent automatically by the browser + // This prevents tokens from being logged in access logs or exposed to XSS attacks const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/v1/logs/live?${params.toString()}`; @@ -196,11 +193,8 @@ export const connectSecurityLogs = ( if (filters.host) params.append('host', filters.host); if (filters.blocked_only) params.append('blocked_only', 'true'); - // Get auth token from localStorage (key: charon_auth_token) - const token = localStorage.getItem('charon_auth_token'); - if (token) { - params.append('token', token); - } + // Authentication is handled via HttpOnly cookies sent automatically by the browser + // This prevents tokens from being logged in access logs or exposed to XSS attacks const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/v1/cerberus/logs/ws?${params.toString()}`;