RoyAalekh commited on
Commit
d69ea55
·
1 Parent(s): c779a3d

Fix authentication redirect loop with dual cookie+header auth

Browse files

- Add cookie-based authentication for web page requests
- Fix redirect loop issue after successful login
- Set secure cookies for HTTPS production deployment
- Maintain backward compatibility with Bearer token API calls
- Update permission checking to work with both auth methods
- Ensure proper cookie deletion on logout
- All authentication now works seamlessly on HuggingFace Spaces

Files changed (2) hide show
  1. app.py +53 -14
  2. static/login.html +2 -7
app.py CHANGED
@@ -12,7 +12,7 @@ from typing import Any, Optional, List, Dict
12
  import uvicorn
13
  from fastapi import FastAPI, HTTPException, Request, status, File, UploadFile, Form, Depends, Cookie
14
  from fastapi.middleware.cors import CORSMiddleware
15
- from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
16
  from fastapi.staticfiles import StaticFiles
17
  from pydantic import BaseModel, Field, field_validator
18
  import uuid
@@ -82,13 +82,19 @@ class UserInfo(BaseModel):
82
 
83
  # Helper function for authentication
84
  def get_current_user(request: Request) -> Optional[Dict[str, Any]]:
85
- """Extract user info from request headers"""
 
86
  auth_header = request.headers.get('Authorization')
87
- if not auth_header or not auth_header.startswith('Bearer '):
88
- return None
 
 
 
 
 
 
89
 
90
- token = auth_header.split(' ')[1]
91
- return auth_manager.validate_session(token)
92
 
93
  def require_auth(request: Request) -> Dict[str, Any]:
94
  """Dependency that requires authentication"""
@@ -296,7 +302,7 @@ async def health_check():
296
 
297
  # Authentication routes
298
  @app.post("/api/auth/login", response_model=LoginResponse, tags=["Authentication"])
299
- async def login(login_data: LoginRequest):
300
  """Authenticate user and create session"""
301
  result = auth_manager.authenticate(login_data.username, login_data.password)
302
  if not result:
@@ -304,6 +310,17 @@ async def login(login_data: LoginRequest):
304
  status_code=status.HTTP_401_UNAUTHORIZED,
305
  detail="Invalid username or password"
306
  )
 
 
 
 
 
 
 
 
 
 
 
307
  return result
308
 
309
  @app.get("/api/auth/validate", tags=["Authentication"])
@@ -315,12 +332,26 @@ async def validate_session(user: Dict[str, Any] = Depends(require_auth)):
315
  }
316
 
317
  @app.post("/api/auth/logout", tags=["Authentication"])
318
- async def logout(request: Request):
319
  """Logout user and invalidate session"""
 
 
320
  auth_header = request.headers.get('Authorization')
321
  if auth_header and auth_header.startswith('Bearer '):
322
  token = auth_header.split(' ')[1]
 
 
 
 
323
  auth_manager.logout(token)
 
 
 
 
 
 
 
 
324
  return {"message": "Logged out successfully"}
325
 
326
  @app.get("/api/auth/user", response_model=UserInfo, tags=["Authentication"])
@@ -472,11 +503,15 @@ async def update_tree(tree_id: int, tree_update: TreeUpdate, request: Request):
472
  detail=f"Tree with ID {tree_id} not found",
473
  )
474
 
475
- # Check if user can edit this tree
 
476
  auth_header = request.headers.get('Authorization', '')
477
- token = auth_header.split(' ')[1] if auth_header.startswith('Bearer ') else ''
 
 
 
478
 
479
- if not auth_manager.can_edit_tree(token, existing_tree.get('created_by', '')):
480
  raise HTTPException(
481
  status_code=status.HTTP_403_FORBIDDEN,
482
  detail="You don't have permission to edit this tree",
@@ -523,11 +558,15 @@ async def delete_tree(tree_id: int, request: Request):
523
  detail=f"Tree with ID {tree_id} not found",
524
  )
525
 
526
- # Check if user can delete this tree
 
527
  auth_header = request.headers.get('Authorization', '')
528
- token = auth_header.split(' ')[1] if auth_header.startswith('Bearer ') else ''
 
 
 
529
 
530
- if not auth_manager.can_delete_tree(token, tree.get('created_by', '')):
531
  raise HTTPException(
532
  status_code=status.HTTP_403_FORBIDDEN,
533
  detail="You don't have permission to delete this tree",
 
12
  import uvicorn
13
  from fastapi import FastAPI, HTTPException, Request, status, File, UploadFile, Form, Depends, Cookie
14
  from fastapi.middleware.cors import CORSMiddleware
15
+ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
16
  from fastapi.staticfiles import StaticFiles
17
  from pydantic import BaseModel, Field, field_validator
18
  import uuid
 
82
 
83
  # Helper function for authentication
84
  def get_current_user(request: Request) -> Optional[Dict[str, Any]]:
85
+ """Extract user info from request headers or cookies"""
86
+ # Try Authorization header first (for API calls)
87
  auth_header = request.headers.get('Authorization')
88
+ if auth_header and auth_header.startswith('Bearer '):
89
+ token = auth_header.split(' ')[1]
90
+ return auth_manager.validate_session(token)
91
+
92
+ # Try cookie for web page requests
93
+ auth_cookie = request.cookies.get('auth_token')
94
+ if auth_cookie:
95
+ return auth_manager.validate_session(auth_cookie)
96
 
97
+ return None
 
98
 
99
  def require_auth(request: Request) -> Dict[str, Any]:
100
  """Dependency that requires authentication"""
 
302
 
303
  # Authentication routes
304
  @app.post("/api/auth/login", response_model=LoginResponse, tags=["Authentication"])
305
+ async def login(login_data: LoginRequest, response: Response):
306
  """Authenticate user and create session"""
307
  result = auth_manager.authenticate(login_data.username, login_data.password)
308
  if not result:
 
310
  status_code=status.HTTP_401_UNAUTHORIZED,
311
  detail="Invalid username or password"
312
  )
313
+
314
+ # Set authentication cookie for web page requests
315
+ response.set_cookie(
316
+ key="auth_token",
317
+ value=result["token"],
318
+ max_age=8*60*60, # 8 hours (same as session timeout)
319
+ httponly=True, # Prevent JavaScript access for security
320
+ secure=True, # HTTPS required for HuggingFace Spaces
321
+ samesite="lax" # CSRF protection
322
+ )
323
+
324
  return result
325
 
326
  @app.get("/api/auth/validate", tags=["Authentication"])
 
332
  }
333
 
334
  @app.post("/api/auth/logout", tags=["Authentication"])
335
+ async def logout(request: Request, response: Response):
336
  """Logout user and invalidate session"""
337
+ # Get token from header or cookie
338
+ token = None
339
  auth_header = request.headers.get('Authorization')
340
  if auth_header and auth_header.startswith('Bearer '):
341
  token = auth_header.split(' ')[1]
342
+ else:
343
+ token = request.cookies.get('auth_token')
344
+
345
+ if token:
346
  auth_manager.logout(token)
347
+
348
+ # Clear the authentication cookie (must match creation parameters)
349
+ response.delete_cookie(
350
+ key="auth_token",
351
+ secure=True,
352
+ samesite="lax"
353
+ )
354
+
355
  return {"message": "Logged out successfully"}
356
 
357
  @app.get("/api/auth/user", response_model=UserInfo, tags=["Authentication"])
 
503
  detail=f"Tree with ID {tree_id} not found",
504
  )
505
 
506
+ # Get token from header or cookie for permission checking
507
+ token = None
508
  auth_header = request.headers.get('Authorization', '')
509
+ if auth_header.startswith('Bearer '):
510
+ token = auth_header.split(' ')[1]
511
+ else:
512
+ token = request.cookies.get('auth_token')
513
 
514
+ if not token or not auth_manager.can_edit_tree(token, existing_tree.get('created_by', '')):
515
  raise HTTPException(
516
  status_code=status.HTTP_403_FORBIDDEN,
517
  detail="You don't have permission to edit this tree",
 
558
  detail=f"Tree with ID {tree_id} not found",
559
  )
560
 
561
+ # Get token from header or cookie for permission checking
562
+ token = None
563
  auth_header = request.headers.get('Authorization', '')
564
+ if auth_header.startswith('Bearer '):
565
+ token = auth_header.split(' ')[1]
566
+ else:
567
+ token = request.cookies.get('auth_token')
568
 
569
+ if not token or not auth_manager.can_delete_tree(token, tree.get('created_by', '')):
570
  raise HTTPException(
571
  status_code=status.HTTP_403_FORBIDDEN,
572
  detail="You don't have permission to delete this tree",
static/login.html CHANGED
@@ -289,13 +289,8 @@
289
  <div class="account-username">Tree research & documentation</div>
290
  </div>
291
  </div>
292
- <div style="margin-top: 1rem; padding: 0.75rem; background: rgba(255, 193, 7, 0.1); border: 1px solid rgba(255, 193, 7, 0.3); border-radius: 8px; font-size: 0.75rem; color: #856404;">
293
- ℹ️ <strong>Temporary Development Passwords:</strong><br>
294
- • aalekh: <code>aalekh_secure_2025</code><br>
295
- • admin: <code>admin_secure_2025</code><br>
296
- • ishita: <code>ishita_secure_2025</code><br>
297
- • jeeb: <code>jeeb_secure_2025</code><br>
298
- <em>Set environment variables to override these defaults.</em>
299
  </div>
300
  </div>
301
 
 
289
  <div class="account-username">Tree research & documentation</div>
290
  </div>
291
  </div>
292
+ <div style="margin-top: 1rem; padding: 0.75rem; background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 8px; font-size: 0.75rem; color: #166534;">
293
+ <strong>Secure Authentication:</strong> All passwords are configured via environment variables for maximum security.
 
 
 
 
 
294
  </div>
295
  </div>
296