mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
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:
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user