feat: Implement API key authentication with rate limiting and secure comparison

- Added rate limiting to the authentication middleware to prevent brute-force attacks.
- Introduced a secure comparison function to mitigate timing attacks during API key validation.
- Created a new rate limiter class to track failed authentication attempts and block requests after exceeding the maximum allowed failures.
- Updated the authentication middleware to handle rate limiting and secure key comparison.
- Enhanced error handling for rate-limited requests, providing appropriate responses to clients.
This commit is contained in:
Test User
2025-12-24 14:49:47 -05:00
parent 97af998066
commit c7ebdb1f80
22 changed files with 1439 additions and 99 deletions

View File

@@ -59,8 +59,9 @@ export function useApiKeyManagement() {
hasGoogleKey: status.hasGoogleKey,
});
}
} catch (error) {
console.error('Failed to check API key status:', error);
} catch {
// Silently handle API key status check failures to avoid exposing
// sensitive error details in the console
}
}
};
@@ -98,26 +99,29 @@ export function useApiKeyManagement() {
};
// Test Google/Gemini connection
// TODO: Add backend endpoint for Gemini API key verification
// NOTE: Full API key validation requires a backend call to verify the key
// against Google's API. The current client-side validation only checks
// basic format requirements and cannot confirm the key is actually valid.
const handleTestGeminiConnection = async () => {
setTestingGeminiConnection(true);
setGeminiTestResult(null);
// Basic validation - check key format
// Basic client-side format validation only
// This does NOT verify the key is valid with Google's API
if (!googleKey || googleKey.trim().length < 10) {
setGeminiTestResult({
success: false,
message: 'Please enter a valid API key.',
message: 'Please enter an API key with at least 10 characters.',
});
setTestingGeminiConnection(false);
return;
}
// For now, just validate the key format (starts with expected prefix)
// Full verification requires a backend endpoint
// Client-side validation cannot confirm key validity.
// The key will be verified when first used with the Gemini API.
setGeminiTestResult({
success: true,
message: 'API key saved. Connection test not yet available.',
message: 'API key format accepted. Key will be validated on first use with Gemini API.',
});
setTestingGeminiConnection(false);
};

View File

@@ -296,7 +296,7 @@ export function TerminalView() {
const headers: Record<string, string> = {};
if (terminalState.authToken) {
headers['X-Terminal-Token'] = terminalState.authToken;
headers['Authorization'] = `Bearer ${terminalState.authToken}`;
}
console.log(`[Terminal] Killing ${sessionIds.length} sessions on server`);
@@ -459,7 +459,7 @@ export function TerminalView() {
try {
const headers: Record<string, string> = {};
if (terminalState.authToken) {
headers['X-Terminal-Token'] = terminalState.authToken;
headers['Authorization'] = `Bearer ${terminalState.authToken}`;
}
const response = await fetch(`${serverUrl}/api/terminal/settings`, { headers });
const data = await response.json();
@@ -488,7 +488,7 @@ export function TerminalView() {
'Content-Type': 'application/json',
};
if (terminalState.authToken) {
headers['X-Terminal-Token'] = terminalState.authToken;
headers['Authorization'] = `Bearer ${terminalState.authToken}`;
}
// Try to use the bulk delete endpoint if available, otherwise delete individually
@@ -501,7 +501,7 @@ export function TerminalView() {
const xhr = new XMLHttpRequest();
xhr.open('DELETE', url, false); // synchronous
if (terminalState.authToken) {
xhr.setRequestHeader('X-Terminal-Token', terminalState.authToken);
xhr.setRequestHeader('Authorization', `Bearer ${terminalState.authToken}`);
}
xhr.send();
} catch {
@@ -595,7 +595,7 @@ export function TerminalView() {
// Get fresh auth token from store
const authToken = useAppStore.getState().terminalState.authToken;
if (authToken) {
headers['X-Terminal-Token'] = authToken;
headers['Authorization'] = `Bearer ${authToken}`;
}
// Helper to check if a session still exists on server
@@ -833,7 +833,7 @@ export function TerminalView() {
'Content-Type': 'application/json',
};
if (terminalState.authToken) {
headers['X-Terminal-Token'] = terminalState.authToken;
headers['Authorization'] = `Bearer ${terminalState.authToken}`;
}
const response = await fetch(`${serverUrl}/api/terminal/sessions`, {
@@ -892,7 +892,7 @@ export function TerminalView() {
'Content-Type': 'application/json',
};
if (terminalState.authToken) {
headers['X-Terminal-Token'] = terminalState.authToken;
headers['Authorization'] = `Bearer ${terminalState.authToken}`;
}
const response = await fetch(`${serverUrl}/api/terminal/sessions`, {
@@ -952,7 +952,7 @@ export function TerminalView() {
try {
const headers: Record<string, string> = {};
if (terminalState.authToken) {
headers['X-Terminal-Token'] = terminalState.authToken;
headers['Authorization'] = `Bearer ${terminalState.authToken}`;
}
const response = await fetch(`${serverUrl}/api/terminal/sessions/${sessionId}`, {
@@ -998,7 +998,7 @@ export function TerminalView() {
// Kill all sessions on the server
const headers: Record<string, string> = {};
if (terminalState.authToken) {
headers['X-Terminal-Token'] = terminalState.authToken;
headers['Authorization'] = `Bearer ${terminalState.authToken}`;
}
await Promise.all(

View File

@@ -940,7 +940,12 @@ export function TerminalPanel({
if (!terminal) return;
const connect = () => {
// Build WebSocket URL with token
// Build WebSocket URL with token in query string
// Note: WebSocket API in browsers does not support custom headers during the upgrade handshake,
// so we must pass the token via query string. This is acceptable because:
// 1. WebSocket URLs are not exposed in HTTP Referer headers
// 2. The connection is upgraded to a secure WebSocket protocol immediately
// 3. Server-side logging should not log query parameters containing tokens
let url = `${wsUrl}/api/terminal/ws?sessionId=${sessionId}`;
if (authToken) {
url += `&token=${encodeURIComponent(authToken)}`;

View File

@@ -2676,7 +2676,10 @@ export const useAppStore = create<AppState & AppActions>()(
kanbanCardDetailLevel: state.kanbanCardDetailLevel,
boardViewMode: state.boardViewMode,
// Settings
apiKeys: state.apiKeys,
// NOTE: apiKeys are intentionally NOT persisted to localStorage for security.
// API keys are stored server-side only via the storeApiKey API to prevent
// exposure through XSS attacks. The apiKeys state is populated on app load
// from the secure server-side storage.
maxConcurrency: state.maxConcurrency,
// Note: autoModeByProject is intentionally NOT persisted
// Auto-mode should always default to OFF on app refresh