akhaliq HF Staff commited on
Commit
4d53e2b
·
1 Parent(s): 63db11a

fix oauth issue

Browse files
README.md CHANGED
@@ -8,6 +8,7 @@ app_port: 7860
8
  pinned: false
9
  disable_embedding: false
10
  hf_oauth: true
 
11
  hf_oauth_scopes:
12
  - manage-repos
13
  ---
 
8
  pinned: false
9
  disable_embedding: false
10
  hf_oauth: true
11
+ hf_oauth_expiration_minutes: 43200
12
  hf_oauth_scopes:
13
  - manage-repos
14
  ---
backend_api.py CHANGED
@@ -8,7 +8,7 @@ from pydantic import BaseModel
8
  from typing import Optional, List, Dict, AsyncGenerator
9
  import json
10
  import asyncio
11
- from datetime import datetime
12
  import secrets
13
  import base64
14
  import urllib.parse
@@ -140,6 +140,46 @@ oauth_states = {}
140
  user_sessions = {}
141
 
142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  # Pydantic models for request/response
144
  class CodeGenerationRequest(BaseModel):
145
  query: str
@@ -325,12 +365,18 @@ async def oauth_callback(code: str, state: str, request: Request):
325
  userinfo_response.raise_for_status()
326
  user_info = userinfo_response.json()
327
 
 
 
 
 
 
328
  # Create session
329
  session_token = secrets.token_urlsafe(32)
330
  user_sessions[session_token] = {
331
  "access_token": access_token,
332
  "user_info": user_info,
333
  "timestamp": datetime.now(),
 
334
  "username": user_info.get("name") or user_info.get("preferred_username") or "user",
335
  "deployed_spaces": [] # Track deployed spaces for follow-up updates
336
  }
@@ -344,6 +390,21 @@ async def oauth_callback(code: str, state: str, request: Request):
344
  raise HTTPException(status_code=500, detail=f"OAuth failed: {str(e)}")
345
 
346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  @app.get("/api/auth/session")
348
  async def get_session(session: str):
349
  """Get user info from session token"""
@@ -351,6 +412,19 @@ async def get_session(session: str):
351
  raise HTTPException(status_code=401, detail="Invalid session")
352
 
353
  session_data = user_sessions[session]
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  return {
355
  "access_token": session_data["access_token"],
356
  "user_info": session_data["user_info"],
@@ -359,14 +433,71 @@ async def get_session(session: str):
359
 
360
  @app.get("/api/auth/status")
361
  async def auth_status(authorization: Optional[str] = Header(None)):
362
- """Check authentication status"""
363
  auth = get_auth_from_header(authorization)
364
- if auth.is_authenticated():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  return AuthStatus(
366
  authenticated=True,
367
  username=auth.username,
368
  message=f"Authenticated as {auth.username}"
369
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  return AuthStatus(
371
  authenticated=False,
372
  username=None,
 
8
  from typing import Optional, List, Dict, AsyncGenerator
9
  import json
10
  import asyncio
11
+ from datetime import datetime, timedelta
12
  import secrets
13
  import base64
14
  import urllib.parse
 
140
  user_sessions = {}
141
 
142
 
143
+ def is_session_expired(session_data: dict) -> bool:
144
+ """Check if session has expired"""
145
+ expires_at = session_data.get("expires_at")
146
+ if not expires_at:
147
+ # If no expiration info, check if session is older than 8 hours
148
+ timestamp = session_data.get("timestamp", datetime.now())
149
+ return (datetime.now() - timestamp) > timedelta(hours=8)
150
+
151
+ return datetime.now() >= expires_at
152
+
153
+
154
+ # Background task for cleaning up expired sessions
155
+ async def cleanup_expired_sessions():
156
+ """Periodically clean up expired sessions"""
157
+ while True:
158
+ try:
159
+ await asyncio.sleep(3600) # Run every hour
160
+
161
+ expired_sessions = []
162
+ for session_token, session_data in user_sessions.items():
163
+ if is_session_expired(session_data):
164
+ expired_sessions.append(session_token)
165
+
166
+ for session_token in expired_sessions:
167
+ user_sessions.pop(session_token, None)
168
+ print(f"[Auth] Cleaned up expired session: {session_token[:10]}...")
169
+
170
+ if expired_sessions:
171
+ print(f"[Auth] Cleaned up {len(expired_sessions)} expired session(s)")
172
+ except Exception as e:
173
+ print(f"[Auth] Cleanup error: {e}")
174
+
175
+ # Start cleanup task on app startup
176
+ @app.on_event("startup")
177
+ async def startup_event():
178
+ """Run startup tasks"""
179
+ asyncio.create_task(cleanup_expired_sessions())
180
+ print("[Startup] ✅ Session cleanup task started")
181
+
182
+
183
  # Pydantic models for request/response
184
  class CodeGenerationRequest(BaseModel):
185
  query: str
 
365
  userinfo_response.raise_for_status()
366
  user_info = userinfo_response.json()
367
 
368
+ # Calculate token expiration
369
+ # OAuth tokens typically have expires_in in seconds
370
+ expires_in = token_data.get("expires_in", 28800) # Default 8 hours
371
+ expires_at = datetime.now() + timedelta(seconds=expires_in)
372
+
373
  # Create session
374
  session_token = secrets.token_urlsafe(32)
375
  user_sessions[session_token] = {
376
  "access_token": access_token,
377
  "user_info": user_info,
378
  "timestamp": datetime.now(),
379
+ "expires_at": expires_at,
380
  "username": user_info.get("name") or user_info.get("preferred_username") or "user",
381
  "deployed_spaces": [] # Track deployed spaces for follow-up updates
382
  }
 
390
  raise HTTPException(status_code=500, detail=f"OAuth failed: {str(e)}")
391
 
392
 
393
+ async def validate_token_with_hf(access_token: str) -> bool:
394
+ """Validate token with HuggingFace API"""
395
+ try:
396
+ async with httpx.AsyncClient() as client:
397
+ response = await client.get(
398
+ f"{OPENID_PROVIDER_URL}/oauth/userinfo",
399
+ headers={"Authorization": f"Bearer {access_token}"},
400
+ timeout=5.0
401
+ )
402
+ return response.status_code == 200
403
+ except Exception as e:
404
+ print(f"[Auth] Token validation error: {e}")
405
+ return False
406
+
407
+
408
  @app.get("/api/auth/session")
409
  async def get_session(session: str):
410
  """Get user info from session token"""
 
412
  raise HTTPException(status_code=401, detail="Invalid session")
413
 
414
  session_data = user_sessions[session]
415
+
416
+ # Check if session has expired
417
+ if is_session_expired(session_data):
418
+ # Clean up expired session
419
+ user_sessions.pop(session, None)
420
+ raise HTTPException(status_code=401, detail="Session expired. Please sign in again.")
421
+
422
+ # Validate token with HuggingFace
423
+ if not await validate_token_with_hf(session_data["access_token"]):
424
+ # Token is invalid, clean up session
425
+ user_sessions.pop(session, None)
426
+ raise HTTPException(status_code=401, detail="Authentication expired. Please sign in again.")
427
+
428
  return {
429
  "access_token": session_data["access_token"],
430
  "user_info": session_data["user_info"],
 
433
 
434
  @app.get("/api/auth/status")
435
  async def auth_status(authorization: Optional[str] = Header(None)):
436
+ """Check authentication status and validate token"""
437
  auth = get_auth_from_header(authorization)
438
+
439
+ if not auth.is_authenticated():
440
+ return AuthStatus(
441
+ authenticated=False,
442
+ username=None,
443
+ message="Not authenticated"
444
+ )
445
+
446
+ # For dev tokens, skip validation
447
+ if auth.token and auth.token.startswith("dev_token_"):
448
+ return AuthStatus(
449
+ authenticated=True,
450
+ username=auth.username,
451
+ message=f"Authenticated as {auth.username} (dev mode)"
452
+ )
453
+
454
+ # For session tokens, check expiration and validate
455
+ token = authorization.replace("Bearer ", "") if authorization else None
456
+ if token and "-" in token and len(token) > 20 and token in user_sessions:
457
+ session_data = user_sessions[token]
458
+
459
+ # Check if session has expired
460
+ if is_session_expired(session_data):
461
+ # Clean up expired session
462
+ user_sessions.pop(token, None)
463
+ return AuthStatus(
464
+ authenticated=False,
465
+ username=None,
466
+ message="Session expired"
467
+ )
468
+
469
+ # Validate token with HuggingFace
470
+ if not await validate_token_with_hf(session_data["access_token"]):
471
+ # Token is invalid, clean up session
472
+ user_sessions.pop(token, None)
473
+ return AuthStatus(
474
+ authenticated=False,
475
+ username=None,
476
+ message="Authentication expired"
477
+ )
478
+
479
  return AuthStatus(
480
  authenticated=True,
481
  username=auth.username,
482
  message=f"Authenticated as {auth.username}"
483
  )
484
+
485
+ # For direct OAuth tokens, validate with HF
486
+ if auth.token:
487
+ is_valid = await validate_token_with_hf(auth.token)
488
+ if is_valid:
489
+ return AuthStatus(
490
+ authenticated=True,
491
+ username=auth.username,
492
+ message=f"Authenticated as {auth.username}"
493
+ )
494
+ else:
495
+ return AuthStatus(
496
+ authenticated=False,
497
+ username=None,
498
+ message="Token expired or invalid"
499
+ )
500
+
501
  return AuthStatus(
502
  authenticated=False,
503
  username=None,
frontend/src/app/page.tsx CHANGED
@@ -95,6 +95,25 @@ export default function Home() {
95
  return () => window.removeEventListener('storage', handleStorageChange);
96
  }, []);
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  // Listen for window focus (user returns to tab after OAuth redirect)
99
  // Only check if backend was available before or if we're authenticated with token
100
  useEffect(() => {
 
95
  return () => window.removeEventListener('storage', handleStorageChange);
96
  }, []);
97
 
98
+ // Listen for authentication expiration events
99
+ useEffect(() => {
100
+ const handleAuthExpired = (e: CustomEvent) => {
101
+ console.log('[Auth] Session expired:', e.detail?.message);
102
+ // Clear authentication state
103
+ setIsAuthenticated(false);
104
+ setUsername(null);
105
+ apiClient.setToken(null);
106
+
107
+ // Show alert to user
108
+ if (typeof window !== 'undefined') {
109
+ alert(e.detail?.message || 'Your session has expired. Please sign in again.');
110
+ }
111
+ };
112
+
113
+ window.addEventListener('auth-expired', handleAuthExpired as EventListener);
114
+ return () => window.removeEventListener('auth-expired', handleAuthExpired as EventListener);
115
+ }, []);
116
+
117
  // Listen for window focus (user returns to tab after OAuth redirect)
118
  // Only check if backend was available before or if we're authenticated with token
119
  useEffect(() => {
frontend/src/lib/api.ts CHANGED
@@ -60,6 +60,28 @@ class ApiClient {
60
  return config;
61
  });
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  // Load token from localStorage on client side
64
  if (typeof window !== 'undefined') {
65
  this.token = localStorage.getItem('hf_oauth_token');
 
60
  return config;
61
  });
62
 
63
+ // Add response interceptor to handle authentication errors
64
+ this.client.interceptors.response.use(
65
+ (response) => response,
66
+ (error) => {
67
+ // Handle 401 errors (expired/invalid authentication)
68
+ if (error.response && error.response.status === 401) {
69
+ // Clear authentication data
70
+ if (typeof window !== 'undefined') {
71
+ localStorage.removeItem('hf_oauth_token');
72
+ localStorage.removeItem('hf_user_info');
73
+ this.token = null;
74
+
75
+ // Dispatch custom event to notify UI components
76
+ window.dispatchEvent(new CustomEvent('auth-expired', {
77
+ detail: { message: 'Your session has expired. Please sign in again.' }
78
+ }));
79
+ }
80
+ }
81
+ return Promise.reject(error);
82
+ }
83
+ );
84
+
85
  // Load token from localStorage on client side
86
  if (typeof window !== 'undefined') {
87
  this.token = localStorage.getItem('hf_oauth_token');
frontend/src/lib/auth.ts CHANGED
@@ -178,6 +178,46 @@ export function isAuthenticated(): boolean {
178
  return getStoredToken() !== null;
179
  }
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  /**
182
  * Development mode login (mock authentication)
183
  */
 
178
  return getStoredToken() !== null;
179
  }
180
 
181
+ /**
182
+ * Validate authentication with backend
183
+ * Returns true if authenticated, false if session expired
184
+ */
185
+ export async function validateAuthentication(): Promise<boolean> {
186
+ const token = getStoredToken();
187
+ if (!token) {
188
+ return false;
189
+ }
190
+
191
+ // Skip validation for dev mode tokens
192
+ if (isDevelopment && token.startsWith('dev_token_')) {
193
+ return true;
194
+ }
195
+
196
+ try {
197
+ const response = await fetch(`${API_BASE}/auth/status`, {
198
+ headers: {
199
+ 'Authorization': `Bearer ${token}`,
200
+ },
201
+ });
202
+
203
+ if (response.status === 401) {
204
+ // Session expired, clean up
205
+ logout();
206
+ return false;
207
+ }
208
+
209
+ if (!response.ok) {
210
+ return false;
211
+ }
212
+
213
+ const data = await response.json();
214
+ return data.authenticated === true;
215
+ } catch (error) {
216
+ console.error('Failed to validate authentication:', error);
217
+ return false;
218
+ }
219
+ }
220
+
221
  /**
222
  * Development mode login (mock authentication)
223
  */