The WebSocket code in logs.ts was reading from 'token' instead of 'charon_auth_token', causing all WebSocket connections to fail authentication with 401 errors. This resulted in the Security Dashboard Live Log Viewer showing "Disconnected" with rapid connect/disconnect cycling. - Changed localStorage key from 'token' to 'charon_auth_token' - Both connectLiveLogs and connectSecurityLogs functions updated
6.4 KiB
Security Dashboard Live Log Viewer Bug Fix Plan
Date: December 16, 2025 Issue: Live Log Viewer shows "Disconnected" error with rapid connect/disconnect flashing
Executive Summary
ROOT CAUSE IDENTIFIED: The WebSocket authentication token retrieval uses the wrong localStorage key.
The auth token is stored under charon_auth_token but the WebSocket code reads from token (which doesn't exist), causing every WebSocket connection to be sent without authentication, resulting in immediate 401 rejection.
1. Root Cause Analysis
Token Storage Key Mismatch
| Component | localStorage Key Used | Correct? |
|---|---|---|
| AuthContext.tsx (login) | charon_auth_token |
✅ Source of truth |
| AuthContext.tsx (logout) | charon_auth_token |
✅ |
| client.ts (axios) | Gets token from AuthContext | ✅ |
| logs.ts (WebSocket) | token |
❌ WRONG |
Code Evidence
AuthContext.tsx (correct storage):
// Line 31 - stores token with correct key
localStorage.setItem('charon_auth_token', token);
logs.ts (incorrect retrieval):
// Line 132 & 200 - reads wrong key
const token = localStorage.getItem('token'); // Returns null!
Result
- User logs in → token stored as
charon_auth_token - Security Dashboard opens → WebSocket tries
localStorage.getItem('token')→ returnsnull - WebSocket URL:
/api/v1/cerberus/logs/ws(no token query param) - Backend auth middleware rejects with 401 Unauthorized
- Frontend receives
onErrorevent → shows "Disconnected" message - User sees rapid flashing if any auto-reconnect logic exists
2. Files to Modify
| File | Change Type | Description |
|---|---|---|
frontend/src/api/logs.ts |
BUG FIX | Change localStorage key from token to charon_auth_token |
That's it! This is a one-line fix in two locations within the same file.
3. Specific Code Changes
File: frontend/src/api/logs.ts
Change #1: connectLiveLogs function (Line 132)
Before:
// Get auth token from localStorage
const token = localStorage.getItem('token');
After:
// Get auth token from localStorage (must match AuthContext key)
const token = localStorage.getItem('charon_auth_token');
Change #2: connectSecurityLogs function (Line 200)
Before:
// Get auth token from localStorage
const token = localStorage.getItem('token');
After:
// Get auth token from localStorage (must match AuthContext key)
const token = localStorage.getItem('charon_auth_token');
4. Phase 1: Backend Fix
No backend changes required.
The backend auth middleware already correctly handles token extraction from query parameters:
// backend/internal/api/middleware/auth.go - Lines 21-24
// Try query param (token passthrough)
if token := c.Query("token"); token != "" {
authHeader = "Bearer " + token
}
The middleware checks in this order:
Authorizationheaderauth_tokencookietokenquery parameter ← WebSocket uses this
The backend is working correctly. The issue is purely frontend.
5. Phase 2: Frontend Fix
Implementation Steps
-
Edit
frontend/src/api/logs.ts:- Line 132: Change
localStorage.getItem('token')tolocalStorage.getItem('charon_auth_token') - Line 200: Change
localStorage.getItem('token')tolocalStorage.getItem('charon_auth_token')
- Line 132: Change
-
Optional Enhancement - Add debug logging for auth issues:
const token = localStorage.getItem('charon_auth_token'); if (!token) { console.warn('WebSocket: No auth token found in localStorage'); } -
Consider creating a shared constant for the token key:
// In a shared constants file export const AUTH_TOKEN_KEY = 'charon_auth_token';
6. Testing Checklist
Manual Testing Steps
- Clear browser data (ensure clean state)
- Login to Charon (stores token correctly)
- Navigate to Security Dashboard
- Verify Live Log Viewer shows "Connected" (green badge)
- Verify logs start streaming (wait for traffic or trigger test request)
- Check browser DevTools:
- Network tab: WebSocket connection should be
101 Switching Protocols - Console: Should show
Cerberus logs WebSocket connection established - WebSocket frames: Should show JSON log entries, not error responses
- Network tab: WebSocket connection should be
Verification Commands
# Test WebSocket endpoint with token (from container)
TOKEN=$(cat /tmp/test_token) # or get from browser localStorage
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \
"http://localhost:8080/api/v1/cerberus/logs/ws?token=$TOKEN"
# Should see: HTTP/1.1 101 Switching Protocols
Unit Test Update
Update frontend/src/components/__tests__/LiveLogViewer.test.tsx if needed to mock the correct localStorage key:
beforeEach(() => {
// Mock localStorage with correct key
vi.spyOn(Storage.prototype, 'getItem').mockImplementation((key) => {
if (key === 'charon_auth_token') return 'mock-jwt-token';
return null;
});
});
7. Regression Prevention
Add ESLint Rule (Optional)
Consider adding a custom ESLint rule or code comment to prevent future mismatches:
// frontend/src/constants/auth.ts
/**
* IMPORTANT: This key must match across all auth-related code.
* Used by: AuthContext.tsx, logs.ts (WebSocket)
* @see AuthContext.tsx for login/logout logic
*/
export const AUTH_TOKEN_STORAGE_KEY = 'charon_auth_token';
Update Contributing Guidelines
Add to .github/copilot-instructions.md or CONTRIBUTING.md:
Auth Token Key: Always use
charon_auth_tokenfor localStorage operations. Import from shared constants when possible.
8. Summary
| Aspect | Details |
|---|---|
| Bug | WebSocket reads wrong localStorage key |
| Impact | Security Dashboard Live Logs never connects |
| Fix | Change token → charon_auth_token in 2 locations |
| Risk | Low - isolated change, no side effects |
| Effort | 5 minutes |
| Testing | Manual verification + existing unit tests |
9. Implementation Command
# Single sed command to fix both occurrences
sed -i "s/localStorage.getItem('token')/localStorage.getItem('charon_auth_token')/g" \
frontend/src/api/logs.ts
Or apply the fix manually in VS Code.