Spaces:
Runtime error
Runtime error
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
- app.py +53 -14
- 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
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 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 |
-
#
|
|
|
|
| 476 |
auth_header = request.headers.get('Authorization', '')
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
|
|
|
| 527 |
auth_header = request.headers.get('Authorization', '')
|
| 528 |
-
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 293 |
-
|
| 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 |
|