Spaces:
Running
Running
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 |
|