wu981526092 commited on
Commit
220ebd3
·
1 Parent(s): 45acb90

🔑 Add dual authentication system: API key + OAuth

Browse files

✨ Features Added:
- Dual authentication options on login page
- Users can choose between own API key or HF OAuth
- API key stored locally in browser
- Automatic API key header injection in all requests
- Authentication management in Settings modal

🔧 Technical Implementation:
- Updated login page with two authentication options
- Modified fetchApi to include X-OpenAI-API-Key header
- Enhanced backend auth dependencies to support API key mode
- Added comprehensive auth section in SettingsModal
- API key validation and local storage management

🎯 User Benefits:
- No forced authentication for users with own API keys
- Flexible authentication based on user preference
- Clear indication of current auth method
- Easy API key management and removal

💡 Security Features:
- API keys stored only locally
- Masked API key display in settings
- Confirmation dialogs for key removal
- Proper validation of API key format

backend/dependencies.py CHANGED
@@ -111,6 +111,18 @@ def get_current_user_optional(request: Request) -> Optional[Dict[str, Any]]:
111
  logger.debug("🏠 Auth disabled - no user required")
112
  return None
113
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  # Try to get user from session
115
  try:
116
  user = request.session.get("user")
@@ -167,6 +179,18 @@ def get_current_user_required(request: Request) -> Dict[str, Any]:
167
  "auth_method": "local_dev"
168
  }
169
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  user = get_current_user_optional(request)
171
  if not user:
172
  logger.warning(f"🚫 Authentication required for {request.url.path}")
@@ -185,11 +209,19 @@ def get_current_user_required(request: Request) -> Dict[str, Any]:
185
  def require_auth_in_hf_spaces(request: Request) -> None:
186
  """
187
  Dependency that enforces authentication only in HF Spaces.
 
188
  Raises 401 if in HF Spaces and user is not authenticated.
189
  """
190
  from utils.environment import should_enable_auth
191
 
192
  if should_enable_auth():
 
 
 
 
 
 
 
193
  user = get_current_user_optional(request)
194
  if not user:
195
  logger.warning(f"🚫 HF Spaces requires authentication for {request.url.path}")
 
111
  logger.debug("🏠 Auth disabled - no user required")
112
  return None
113
 
114
+ # Check for API key mode first
115
+ api_key_header = request.headers.get("X-OpenAI-API-Key")
116
+ if api_key_header and api_key_header.startswith("sk-"):
117
+ logger.info("🔑 User authenticated with API key mode")
118
+ return {
119
+ "id": "api_key_user",
120
+ "username": "api_key_user",
121
+ "name": "API Key User",
122
+ "auth_method": "api_key",
123
+ "api_key": api_key_header
124
+ }
125
+
126
  # Try to get user from session
127
  try:
128
  user = request.session.get("user")
 
179
  "auth_method": "local_dev"
180
  }
181
 
182
+ # Check for API key mode first
183
+ api_key_header = request.headers.get("X-OpenAI-API-Key")
184
+ if api_key_header and api_key_header.startswith("sk-"):
185
+ logger.info("🔑 User authenticated with API key mode")
186
+ return {
187
+ "id": "api_key_user",
188
+ "username": "api_key_user",
189
+ "name": "API Key User",
190
+ "auth_method": "api_key",
191
+ "api_key": api_key_header
192
+ }
193
+
194
  user = get_current_user_optional(request)
195
  if not user:
196
  logger.warning(f"🚫 Authentication required for {request.url.path}")
 
209
  def require_auth_in_hf_spaces(request: Request) -> None:
210
  """
211
  Dependency that enforces authentication only in HF Spaces.
212
+ Supports both API key mode and OAuth login mode.
213
  Raises 401 if in HF Spaces and user is not authenticated.
214
  """
215
  from utils.environment import should_enable_auth
216
 
217
  if should_enable_auth():
218
+ # Check for API key mode first
219
+ api_key_header = request.headers.get("X-OpenAI-API-Key")
220
+ if api_key_header and api_key_header.startswith("sk-"):
221
+ # User is using API key mode, allow access
222
+ logger.info("🔑 API key authentication successful")
223
+ return
224
+
225
  user = get_current_user_optional(request)
226
  if not user:
227
  logger.warning(f"🚫 HF Spaces requires authentication for {request.url.path}")
backend/templates/login.html CHANGED
@@ -160,32 +160,90 @@
160
  actionable insights for AI system analysis and robustness testing.
161
  </p>
162
 
163
- <!-- CTA Section -->
164
- <div class="space-y-4">
165
- <a
166
- href="/auth/login"
167
- class="btn-primary inline-flex items-center justify-center px-8 py-4 text-lg font-semibold rounded-lg transition-all duration-300 group"
168
- >
169
- Access Research Platform
170
- <svg
171
- class="ml-2 w-5 h-5 transition-transform group-hover:translate-x-1"
172
- fill="none"
173
- stroke="currentColor"
174
- viewBox="0 0 24 24"
175
  >
176
- <path
177
- stroke-linecap="round"
178
- stroke-linejoin="round"
179
- stroke-width="2"
180
- d="M13 7l5 5m0 0l-5 5m5-5H6"
181
- ></path>
182
- </svg>
183
- </a>
184
-
185
- <div class="glass-card p-4 rounded-lg">
186
- <p class="text-sm text-muted-foreground">
187
- Authentication required for responsible AI resource usage
188
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  </div>
190
  </div>
191
  </div>
@@ -208,5 +266,51 @@
208
  </div>
209
  </div>
210
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  </body>
212
  </html>
 
160
  actionable insights for AI system analysis and robustness testing.
161
  </p>
162
 
163
+ <!-- Access Options -->
164
+ <div class="space-y-6">
165
+ <!-- Option 1: Use Our Service -->
166
+ <div class="space-y-4">
167
+ <h3 class="text-lg font-semibold text-foreground">
168
+ Option 1: Use Our Service
169
+ </h3>
170
+ <a
171
+ href="/auth/login"
172
+ class="btn-primary inline-flex items-center justify-center px-8 py-4 text-lg font-semibold rounded-lg transition-all duration-300 group w-full"
 
 
173
  >
174
+ Login with Hugging Face
175
+ <svg
176
+ class="ml-2 w-5 h-5 transition-transform group-hover:translate-x-1"
177
+ fill="none"
178
+ stroke="currentColor"
179
+ viewBox="0 0 24 24"
180
+ >
181
+ <path
182
+ stroke-linecap="round"
183
+ stroke-linejoin="round"
184
+ stroke-width="2"
185
+ d="M13 7l5 5m0 0l-5 5m5-5H6"
186
+ ></path>
187
+ </svg>
188
+ </a>
189
+ <div class="glass-card p-4 rounded-lg">
190
+ <p class="text-sm text-muted-foreground">
191
+ Use our OpenAI API credits with authentication for
192
+ responsible usage tracking
193
+ </p>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Divider -->
198
+ <div class="relative">
199
+ <div class="absolute inset-0 flex items-center">
200
+ <div class="w-full border-t border-border"></div>
201
+ </div>
202
+ <div class="relative flex justify-center text-xs uppercase">
203
+ <span class="bg-background px-2 text-muted-foreground"
204
+ >Or</span
205
+ >
206
+ </div>
207
+ </div>
208
+
209
+ <!-- Option 2: Use Your Own API Key -->
210
+ <div class="space-y-4">
211
+ <h3 class="text-lg font-semibold text-foreground">
212
+ Option 2: Use Your Own OpenAI API Key
213
+ </h3>
214
+ <div class="space-y-3">
215
+ <input
216
+ type="password"
217
+ id="apiKey"
218
+ placeholder="sk-..."
219
+ class="w-full px-4 py-3 border border-input rounded-lg bg-background text-foreground placeholder-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
220
+ />
221
+ <button
222
+ id="useApiKeyBtn"
223
+ class="w-full inline-flex items-center justify-center px-8 py-4 text-lg font-semibold rounded-lg transition-all duration-300 group bg-secondary text-secondary-foreground hover:bg-secondary/80"
224
+ >
225
+ Continue with Your API Key
226
+ <svg
227
+ class="ml-2 w-5 h-5 transition-transform group-hover:translate-x-1"
228
+ fill="none"
229
+ stroke="currentColor"
230
+ viewBox="0 0 24 24"
231
+ >
232
+ <path
233
+ stroke-linecap="round"
234
+ stroke-linejoin="round"
235
+ stroke-width="2"
236
+ d="M13 7l5 5m0 0l-5 5m5-5H6"
237
+ ></path>
238
+ </svg>
239
+ </button>
240
+ </div>
241
+ <div class="glass-card p-4 rounded-lg">
242
+ <p class="text-sm text-muted-foreground">
243
+ Your API key will be stored locally and used directly with
244
+ OpenAI. No authentication required.
245
+ </p>
246
+ </div>
247
  </div>
248
  </div>
249
  </div>
 
266
  </div>
267
  </div>
268
  </div>
269
+
270
+ <script>
271
+ // Handle API key submission
272
+ document
273
+ .getElementById("useApiKeyBtn")
274
+ .addEventListener("click", function () {
275
+ const apiKey = document.getElementById("apiKey").value.trim();
276
+
277
+ if (!apiKey) {
278
+ alert("Please enter your OpenAI API key");
279
+ return;
280
+ }
281
+
282
+ if (!apiKey.startsWith("sk-")) {
283
+ alert("Please enter a valid OpenAI API key (starts with sk-)");
284
+ return;
285
+ }
286
+
287
+ // Store API key in localStorage
288
+ localStorage.setItem("openai_api_key", apiKey);
289
+ localStorage.setItem("auth_mode", "api_key");
290
+
291
+ // Redirect to main application
292
+ window.location.href = "/";
293
+ });
294
+
295
+ // Allow Enter key to submit API key
296
+ document
297
+ .getElementById("apiKey")
298
+ .addEventListener("keypress", function (e) {
299
+ if (e.key === "Enter") {
300
+ document.getElementById("useApiKeyBtn").click();
301
+ }
302
+ });
303
+
304
+ // Check if user already has an API key stored
305
+ window.addEventListener("load", function () {
306
+ const storedApiKey = localStorage.getItem("openai_api_key");
307
+ const authMode = localStorage.getItem("auth_mode");
308
+
309
+ if (storedApiKey && authMode === "api_key") {
310
+ // User already has API key, redirect to main app
311
+ window.location.href = "/";
312
+ }
313
+ });
314
+ </script>
315
  </body>
316
  </html>
frontend/src/components/shared/SettingsModal.tsx CHANGED
@@ -11,6 +11,8 @@ import { Button } from "@/components/ui/button";
11
  import { Badge } from "@/components/ui/badge";
12
  import { Separator } from "@/components/ui/separator";
13
  import { ScrollArea } from "@/components/ui/scroll-area";
 
 
14
  import {
15
  Settings,
16
  Palette,
@@ -25,6 +27,11 @@ import {
25
  Zap,
26
  Clock,
27
  DollarSign,
 
 
 
 
 
28
  } from "lucide-react";
29
  import { useKGDisplayMode } from "@/context/KGDisplayModeContext";
30
  import { useModelPreferences } from "@/hooks/useModelPreferences";
@@ -47,6 +54,23 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
47
  updateModelPreference,
48
  isLoading,
49
  } = useModelPreferences();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
  const handleThemeChange = (theme: Theme) => {
52
  setCurrentTheme(theme);
@@ -114,6 +138,11 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
114
  name: "Models",
115
  icon: Brain,
116
  },
 
 
 
 
 
117
  {
118
  id: "general",
119
  name: "General",
@@ -349,6 +378,197 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
349
  </div>
350
  );
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  const renderGeneralSection = () => (
353
  <div className="space-y-6">
354
  <div>
@@ -431,6 +651,8 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
431
  return renderAppearanceSection();
432
  case "models":
433
  return renderModelsSection();
 
 
434
  case "general":
435
  return renderGeneralSection();
436
  default:
 
11
  import { Badge } from "@/components/ui/badge";
12
  import { Separator } from "@/components/ui/separator";
13
  import { ScrollArea } from "@/components/ui/scroll-area";
14
+ import { Input } from "@/components/ui/input";
15
+ import { Label } from "@/components/ui/label";
16
  import {
17
  Settings,
18
  Palette,
 
27
  Zap,
28
  Clock,
29
  DollarSign,
30
+ Key,
31
+ Shield,
32
+ Eye,
33
+ EyeOff,
34
+ Trash2,
35
  } from "lucide-react";
36
  import { useKGDisplayMode } from "@/context/KGDisplayModeContext";
37
  import { useModelPreferences } from "@/hooks/useModelPreferences";
 
54
  updateModelPreference,
55
  isLoading,
56
  } = useModelPreferences();
57
+
58
+ // Authentication state
59
+ const [showApiKey, setShowApiKey] = useState(false);
60
+ const [apiKey, setApiKey] = useState("");
61
+ const [authMode, setAuthMode] = useState<string | null>(null);
62
+
63
+ // Load auth state on mount
64
+ React.useEffect(() => {
65
+ const storedApiKey = localStorage.getItem('openai_api_key');
66
+ const storedAuthMode = localStorage.getItem('auth_mode');
67
+ if (storedApiKey) {
68
+ setApiKey(storedApiKey);
69
+ }
70
+ if (storedAuthMode) {
71
+ setAuthMode(storedAuthMode);
72
+ }
73
+ }, []);
74
 
75
  const handleThemeChange = (theme: Theme) => {
76
  setCurrentTheme(theme);
 
138
  name: "Models",
139
  icon: Brain,
140
  },
141
+ {
142
+ id: "authentication",
143
+ name: "Authentication",
144
+ icon: Shield,
145
+ },
146
  {
147
  id: "general",
148
  name: "General",
 
378
  </div>
379
  );
380
 
381
+ // Authentication functions
382
+ const handleApiKeyUpdate = () => {
383
+ if (!apiKey.trim()) {
384
+ alert('Please enter an API key');
385
+ return;
386
+ }
387
+
388
+ if (!apiKey.startsWith('sk-')) {
389
+ alert('Please enter a valid OpenAI API key (starts with sk-)');
390
+ return;
391
+ }
392
+
393
+ localStorage.setItem('openai_api_key', apiKey);
394
+ localStorage.setItem('auth_mode', 'api_key');
395
+ setAuthMode('api_key');
396
+
397
+ // Reload the page to update authentication state
398
+ window.location.reload();
399
+ };
400
+
401
+ const handleRemoveApiKey = () => {
402
+ if (confirm('Are you sure you want to remove your API key? You will need to authenticate again.')) {
403
+ localStorage.removeItem('openai_api_key');
404
+ localStorage.removeItem('auth_mode');
405
+ setApiKey('');
406
+ setAuthMode(null);
407
+
408
+ // Redirect to login page
409
+ window.location.href = '/auth/login-page';
410
+ }
411
+ };
412
+
413
+ const renderAuthenticationSection = () => (
414
+ <div className="space-y-6">
415
+ <div>
416
+ <h3 className="text-lg font-semibold mb-4">Authentication Method</h3>
417
+
418
+ {authMode === 'api_key' ? (
419
+ // Current API Key Section
420
+ <Card className="border-green-200 bg-green-50/50 dark:border-green-800 dark:bg-green-950/50">
421
+ <CardContent className="p-4">
422
+ <div className="flex items-start justify-between">
423
+ <div className="flex items-center gap-3">
424
+ <div className="p-2 rounded-lg bg-green-100 dark:bg-green-900">
425
+ <Key className="h-4 w-4 text-green-600 dark:text-green-400" />
426
+ </div>
427
+ <div>
428
+ <h4 className="font-medium text-green-900 dark:text-green-100">
429
+ Using Your OpenAI API Key
430
+ </h4>
431
+ <p className="text-sm text-green-700 dark:text-green-300">
432
+ API Key: {apiKey.substring(0, 7)}...{apiKey.substring(apiKey.length - 4)}
433
+ </p>
434
+ <p className="text-xs text-green-600 dark:text-green-400 mt-1">
435
+ Your API key is stored locally and sent directly to OpenAI
436
+ </p>
437
+ </div>
438
+ </div>
439
+ <Button
440
+ variant="destructive"
441
+ size="sm"
442
+ onClick={handleRemoveApiKey}
443
+ >
444
+ <Trash2 className="h-4 w-4 mr-2" />
445
+ Remove
446
+ </Button>
447
+ </div>
448
+ </CardContent>
449
+ </Card>
450
+ ) : authMode === 'huggingface_oauth' ? (
451
+ // HF OAuth Section
452
+ <Card className="border-blue-200 bg-blue-50/50 dark:border-blue-800 dark:bg-blue-950/50">
453
+ <CardContent className="p-4">
454
+ <div className="flex items-center gap-3">
455
+ <div className="p-2 rounded-lg bg-blue-100 dark:bg-blue-900">
456
+ <Shield className="h-4 w-4 text-blue-600 dark:text-blue-400" />
457
+ </div>
458
+ <div>
459
+ <h4 className="font-medium text-blue-900 dark:text-blue-100">
460
+ Authenticated with Hugging Face
461
+ </h4>
462
+ <p className="text-sm text-blue-700 dark:text-blue-300">
463
+ Using our provided OpenAI API credits
464
+ </p>
465
+ </div>
466
+ </div>
467
+ </CardContent>
468
+ </Card>
469
+ ) : (
470
+ // No Authentication Section
471
+ <Card className="border-yellow-200 bg-yellow-50/50 dark:border-yellow-800 dark:bg-yellow-950/50">
472
+ <CardContent className="p-4">
473
+ <div className="flex items-center gap-3 mb-4">
474
+ <div className="p-2 rounded-lg bg-yellow-100 dark:bg-yellow-900">
475
+ <Key className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />
476
+ </div>
477
+ <div>
478
+ <h4 className="font-medium text-yellow-900 dark:text-yellow-100">
479
+ Add Your OpenAI API Key
480
+ </h4>
481
+ <p className="text-sm text-yellow-700 dark:text-yellow-300">
482
+ Use your own API key to access AgentGraph features
483
+ </p>
484
+ </div>
485
+ </div>
486
+
487
+ <div className="space-y-3">
488
+ <div>
489
+ <Label htmlFor="api-key">OpenAI API Key</Label>
490
+ <div className="flex gap-2 mt-1">
491
+ <div className="relative flex-1">
492
+ <Input
493
+ id="api-key"
494
+ type={showApiKey ? "text" : "password"}
495
+ placeholder="sk-..."
496
+ value={apiKey}
497
+ onChange={(e) => setApiKey(e.target.value)}
498
+ className="pr-10"
499
+ />
500
+ <Button
501
+ type="button"
502
+ variant="ghost"
503
+ size="sm"
504
+ className="absolute right-1 top-1/2 -translate-y-1/2 h-7 w-7 p-0"
505
+ onClick={() => setShowApiKey(!showApiKey)}
506
+ >
507
+ {showApiKey ? (
508
+ <EyeOff className="h-4 w-4" />
509
+ ) : (
510
+ <Eye className="h-4 w-4" />
511
+ )}
512
+ </Button>
513
+ </div>
514
+ <Button onClick={handleApiKeyUpdate} disabled={!apiKey.trim()}>
515
+ Save
516
+ </Button>
517
+ </div>
518
+ </div>
519
+
520
+ <div className="text-xs text-muted-foreground bg-muted/50 p-2 rounded">
521
+ <p>• Your API key will be stored locally in your browser</p>
522
+ <p>• It will be sent directly to OpenAI for API calls</p>
523
+ <p>• No authentication with our service required</p>
524
+ </div>
525
+ </div>
526
+ </CardContent>
527
+ </Card>
528
+ )}
529
+ </div>
530
+
531
+ <Separator />
532
+
533
+ <div>
534
+ <h3 className="text-lg font-semibold mb-4">Authentication Options</h3>
535
+ <div className="grid grid-cols-1 gap-3">
536
+ <Card className="cursor-pointer transition-all hover:shadow-md hover:bg-muted/50">
537
+ <CardContent className="p-4">
538
+ <div className="flex items-center gap-3">
539
+ <div className="p-2 rounded-lg bg-muted">
540
+ <Key className="h-4 w-4" />
541
+ </div>
542
+ <div>
543
+ <h4 className="font-medium">Use Your Own API Key</h4>
544
+ <p className="text-sm text-muted-foreground">
545
+ Provide your OpenAI API key for direct access
546
+ </p>
547
+ </div>
548
+ </div>
549
+ </CardContent>
550
+ </Card>
551
+
552
+ <Card className="cursor-pointer transition-all hover:shadow-md hover:bg-muted/50">
553
+ <CardContent className="p-4">
554
+ <div className="flex items-center gap-3">
555
+ <div className="p-2 rounded-lg bg-muted">
556
+ <Shield className="h-4 w-4" />
557
+ </div>
558
+ <div>
559
+ <h4 className="font-medium">Login with Hugging Face</h4>
560
+ <p className="text-sm text-muted-foreground">
561
+ Use our provided API credits with authentication
562
+ </p>
563
+ </div>
564
+ </div>
565
+ </CardContent>
566
+ </Card>
567
+ </div>
568
+ </div>
569
+ </div>
570
+ );
571
+
572
  const renderGeneralSection = () => (
573
  <div className="space-y-6">
574
  <div>
 
651
  return renderAppearanceSection();
652
  case "models":
653
  return renderModelsSection();
654
+ case "authentication":
655
+ return renderAuthenticationSection();
656
  case "general":
657
  return renderGeneralSection();
658
  default:
frontend/src/lib/api.ts CHANGED
@@ -24,11 +24,22 @@ async function fetchApi<T>(
24
  options?: RequestInit,
25
  retryCount = 0
26
  ): Promise<T> {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  const response = await fetch(`${API_BASE}${endpoint}`, {
28
- headers: {
29
- "Content-Type": "application/json",
30
- ...options?.headers,
31
- },
32
  ...options,
33
  });
34
 
 
24
  options?: RequestInit,
25
  retryCount = 0
26
  ): Promise<T> {
27
+ // Get API key from localStorage if available
28
+ const apiKey = localStorage.getItem('openai_api_key');
29
+ const authMode = localStorage.getItem('auth_mode');
30
+
31
+ const headers: Record<string, string> = {
32
+ "Content-Type": "application/json",
33
+ ...options?.headers,
34
+ };
35
+
36
+ // Add API key header if user is in API key mode
37
+ if (apiKey && authMode === 'api_key') {
38
+ headers["X-OpenAI-API-Key"] = apiKey;
39
+ }
40
+
41
  const response = await fetch(`${API_BASE}${endpoint}`, {
42
+ headers,
 
 
 
43
  ...options,
44
  });
45