Spaces:
Sleeping
Sleeping
findEthics commited on
Commit ·
dcffe87
1
Parent(s): dcf19b1
Implement stateless JWT authentication for Hugging Face Spaces
Browse files- Add PyJWT dependency for JWT token handling
- Create JWT token functions: create_auth_token, verify_auth_token
- Implement stateless authentication using JWT cookies
- Update hf_login_required to use JWT tokens instead of sessions
- Set auth_token cookie on login with 48-hour expiry
- Clear auth_token cookie on logout
- Use very permissive cookie settings for HF Spaces iframe compatibility
This solves the load balancing issue where sessions are not shared
between different servers in Hugging Face Spaces by using stateless
JWT tokens stored in cookies instead of server-side sessions.
- app.py +91 -27
- requirements.txt +2 -1
app.py
CHANGED
|
@@ -3,7 +3,7 @@ Chatty - Ethics Chat Application
|
|
| 3 |
A Flask web application that provides a chat interface for the findEthics-Atlas API.
|
| 4 |
"""
|
| 5 |
|
| 6 |
-
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session
|
| 7 |
from flask_login import login_user, logout_user, login_required, current_user
|
| 8 |
from flask_wtf.csrf import CSRFProtect
|
| 9 |
import requests
|
|
@@ -13,6 +13,8 @@ import logging
|
|
| 13 |
from datetime import datetime, timedelta
|
| 14 |
from collections import defaultdict
|
| 15 |
import time
|
|
|
|
|
|
|
| 16 |
|
| 17 |
from config import get_config, validate_environment
|
| 18 |
from auth import init_login_manager, authenticate_user, create_user_account
|
|
@@ -69,34 +71,72 @@ logging.basicConfig(
|
|
| 69 |
# Initialize Flask-Login
|
| 70 |
init_login_manager(app)
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
# Custom login_required decorator for Hugging Face Spaces load balancing
|
| 73 |
def hf_login_required(f):
|
| 74 |
"""
|
| 75 |
-
Custom login_required decorator that
|
| 76 |
-
|
| 77 |
"""
|
| 78 |
@wraps(f)
|
| 79 |
def decorated_function(*args, **kwargs):
|
| 80 |
-
# First check if user is already authenticated
|
| 81 |
if current_user.is_authenticated:
|
| 82 |
return f(*args, **kwargs)
|
| 83 |
|
| 84 |
-
#
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
login_user(user, remember=True)
|
| 92 |
-
app.logger.info(f"Restored user session for load balancing: {user.email}")
|
| 93 |
-
return f(*args, **kwargs)
|
| 94 |
-
except Exception as e:
|
| 95 |
-
app.logger.warning(f"Failed to restore user session: {e}")
|
| 96 |
|
| 97 |
# If all else fails, redirect to login
|
| 98 |
app.logger.info(f"Authentication required - redirecting to login")
|
| 99 |
app.logger.info(f"Session data: {dict(session)}")
|
|
|
|
| 100 |
app.logger.info(f"Request IP: {request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)}")
|
| 101 |
|
| 102 |
return redirect(url_for('login', next=request.url))
|
|
@@ -273,29 +313,45 @@ def login():
|
|
| 273 |
success, user, error_message = authenticate_user(email, password)
|
| 274 |
|
| 275 |
if success and user:
|
| 276 |
-
# Login successful - create session
|
| 277 |
-
# For Hugging Face Spaces, we need to make sessions permanent to work in iframe
|
| 278 |
session.permanent = True
|
| 279 |
-
login_user(user, remember=True)
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
-
# Store user info in session as backup
|
| 282 |
session['user_id'] = str(user.id)
|
| 283 |
session['user_email'] = user.email
|
| 284 |
session['login_time'] = datetime.utcnow().isoformat()
|
| 285 |
|
| 286 |
-
# Debug
|
| 287 |
app.logger.info(f"Session after login_user: {dict(session)}")
|
| 288 |
app.logger.info(f"Current user authenticated: {current_user.is_authenticated}")
|
| 289 |
app.logger.info(f"Current user ID: {getattr(current_user, 'id', 'None')}")
|
|
|
|
| 290 |
app.logger.info(f"Request IP: {request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)}")
|
| 291 |
|
| 292 |
flash('Login successful!', 'success')
|
| 293 |
|
| 294 |
-
#
|
| 295 |
next_page = request.args.get('next')
|
| 296 |
if next_page and next_page.startswith('/'):
|
| 297 |
-
|
| 298 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 299 |
else:
|
| 300 |
# Login failed - record attempt and display error
|
| 301 |
record_login_attempt(client_ip)
|
|
@@ -322,15 +378,23 @@ def logout():
|
|
| 322 |
# Flash success message
|
| 323 |
flash('You have been logged out successfully.', 'info')
|
| 324 |
|
| 325 |
-
#
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
except Exception as e:
|
| 329 |
# Even if there's an error, still try to log out
|
| 330 |
logout_user()
|
| 331 |
session.clear()
|
| 332 |
flash('Logout completed.', 'info')
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
@app.route('/chat')
|
| 336 |
@hf_login_required
|
|
|
|
| 3 |
A Flask web application that provides a chat interface for the findEthics-Atlas API.
|
| 4 |
"""
|
| 5 |
|
| 6 |
+
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session, make_response
|
| 7 |
from flask_login import login_user, logout_user, login_required, current_user
|
| 8 |
from flask_wtf.csrf import CSRFProtect
|
| 9 |
import requests
|
|
|
|
| 13 |
from datetime import datetime, timedelta
|
| 14 |
from collections import defaultdict
|
| 15 |
import time
|
| 16 |
+
import jwt
|
| 17 |
+
import hashlib
|
| 18 |
|
| 19 |
from config import get_config, validate_environment
|
| 20 |
from auth import init_login_manager, authenticate_user, create_user_account
|
|
|
|
| 71 |
# Initialize Flask-Login
|
| 72 |
init_login_manager(app)
|
| 73 |
|
| 74 |
+
# Stateless authentication functions for Hugging Face Spaces
|
| 75 |
+
def create_auth_token(user_id, email):
|
| 76 |
+
"""Create a JWT token for stateless authentication"""
|
| 77 |
+
payload = {
|
| 78 |
+
'user_id': str(user_id),
|
| 79 |
+
'email': email,
|
| 80 |
+
'exp': datetime.utcnow() + timedelta(hours=48), # 48 hour expiry
|
| 81 |
+
'iat': datetime.utcnow()
|
| 82 |
+
}
|
| 83 |
+
return jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256')
|
| 84 |
+
|
| 85 |
+
def verify_auth_token(token):
|
| 86 |
+
"""Verify and decode JWT token"""
|
| 87 |
+
try:
|
| 88 |
+
payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
|
| 89 |
+
return payload
|
| 90 |
+
except jwt.ExpiredSignatureError:
|
| 91 |
+
app.logger.info("Auth token expired")
|
| 92 |
+
return None
|
| 93 |
+
except jwt.InvalidTokenError:
|
| 94 |
+
app.logger.info("Invalid auth token")
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
def get_current_user_from_token():
|
| 98 |
+
"""Get current user from JWT token in cookie"""
|
| 99 |
+
token = request.cookies.get('auth_token')
|
| 100 |
+
if not token:
|
| 101 |
+
return None
|
| 102 |
+
|
| 103 |
+
payload = verify_auth_token(token)
|
| 104 |
+
if not payload:
|
| 105 |
+
return None
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
user = User.get_by_id(payload['user_id'])
|
| 109 |
+
if user and user.email == payload['email']:
|
| 110 |
+
return user
|
| 111 |
+
except Exception as e:
|
| 112 |
+
app.logger.warning(f"Failed to load user from token: {e}")
|
| 113 |
+
|
| 114 |
+
return None
|
| 115 |
+
|
| 116 |
# Custom login_required decorator for Hugging Face Spaces load balancing
|
| 117 |
def hf_login_required(f):
|
| 118 |
"""
|
| 119 |
+
Custom login_required decorator that uses stateless JWT authentication
|
| 120 |
+
for Hugging Face Spaces load balancing compatibility
|
| 121 |
"""
|
| 122 |
@wraps(f)
|
| 123 |
def decorated_function(*args, **kwargs):
|
| 124 |
+
# First check if user is already authenticated via Flask-Login
|
| 125 |
if current_user.is_authenticated:
|
| 126 |
return f(*args, **kwargs)
|
| 127 |
|
| 128 |
+
# Try to authenticate using JWT token from cookie
|
| 129 |
+
user = get_current_user_from_token()
|
| 130 |
+
if user:
|
| 131 |
+
# Restore the user session for this request
|
| 132 |
+
login_user(user, remember=True)
|
| 133 |
+
app.logger.info(f"Restored user from JWT token: {user.email}")
|
| 134 |
+
return f(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
# If all else fails, redirect to login
|
| 137 |
app.logger.info(f"Authentication required - redirecting to login")
|
| 138 |
app.logger.info(f"Session data: {dict(session)}")
|
| 139 |
+
app.logger.info(f"Auth token present: {'auth_token' in request.cookies}")
|
| 140 |
app.logger.info(f"Request IP: {request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)}")
|
| 141 |
|
| 142 |
return redirect(url_for('login', next=request.url))
|
|
|
|
| 313 |
success, user, error_message = authenticate_user(email, password)
|
| 314 |
|
| 315 |
if success and user:
|
| 316 |
+
# Login successful - create session and JWT token
|
|
|
|
| 317 |
session.permanent = True
|
| 318 |
+
login_user(user, remember=True)
|
| 319 |
+
|
| 320 |
+
# Create JWT token for stateless authentication across load-balanced servers
|
| 321 |
+
auth_token = create_auth_token(user.id, user.email)
|
| 322 |
|
| 323 |
+
# Store user info in session as backup
|
| 324 |
session['user_id'] = str(user.id)
|
| 325 |
session['user_email'] = user.email
|
| 326 |
session['login_time'] = datetime.utcnow().isoformat()
|
| 327 |
|
| 328 |
+
# Debug information
|
| 329 |
app.logger.info(f"Session after login_user: {dict(session)}")
|
| 330 |
app.logger.info(f"Current user authenticated: {current_user.is_authenticated}")
|
| 331 |
app.logger.info(f"Current user ID: {getattr(current_user, 'id', 'None')}")
|
| 332 |
+
app.logger.info(f"JWT token created: {bool(auth_token)}")
|
| 333 |
app.logger.info(f"Request IP: {request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)}")
|
| 334 |
|
| 335 |
flash('Login successful!', 'success')
|
| 336 |
|
| 337 |
+
# Create response with JWT token cookie
|
| 338 |
next_page = request.args.get('next')
|
| 339 |
if next_page and next_page.startswith('/'):
|
| 340 |
+
response = make_response(redirect(next_page))
|
| 341 |
+
else:
|
| 342 |
+
response = make_response(redirect(url_for('chat_interface')))
|
| 343 |
+
|
| 344 |
+
# Set JWT token cookie with very permissive settings for HF Spaces
|
| 345 |
+
response.set_cookie(
|
| 346 |
+
'auth_token',
|
| 347 |
+
auth_token,
|
| 348 |
+
max_age=48*60*60, # 48 hours
|
| 349 |
+
secure=False, # HF handles HTTPS at proxy level
|
| 350 |
+
httponly=False, # Allow JS access for iframe compatibility
|
| 351 |
+
samesite=None # Most permissive for cross-origin
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
return response
|
| 355 |
else:
|
| 356 |
# Login failed - record attempt and display error
|
| 357 |
record_login_attempt(client_ip)
|
|
|
|
| 378 |
# Flash success message
|
| 379 |
flash('You have been logged out successfully.', 'info')
|
| 380 |
|
| 381 |
+
# Create response and clear JWT token cookie
|
| 382 |
+
response = make_response(redirect(url_for('login')))
|
| 383 |
+
response.set_cookie('auth_token', '', expires=0) # Clear the cookie
|
| 384 |
+
|
| 385 |
+
return response
|
| 386 |
|
| 387 |
except Exception as e:
|
| 388 |
# Even if there's an error, still try to log out
|
| 389 |
logout_user()
|
| 390 |
session.clear()
|
| 391 |
flash('Logout completed.', 'info')
|
| 392 |
+
|
| 393 |
+
# Clear JWT token cookie even on error
|
| 394 |
+
response = make_response(redirect(url_for('login')))
|
| 395 |
+
response.set_cookie('auth_token', '', expires=0)
|
| 396 |
+
|
| 397 |
+
return response
|
| 398 |
|
| 399 |
@app.route('/chat')
|
| 400 |
@hf_login_required
|
requirements.txt
CHANGED
|
@@ -21,4 +21,5 @@ gradio==5.0.1
|
|
| 21 |
|
| 22 |
# Additional dependencies for production
|
| 23 |
gunicorn==21.2.0 # WSGI server for production deployment
|
| 24 |
-
python-dateutil==2.8.2 # Enhanced date handling
|
|
|
|
|
|
| 21 |
|
| 22 |
# Additional dependencies for production
|
| 23 |
gunicorn==21.2.0 # WSGI server for production deployment
|
| 24 |
+
python-dateutil==2.8.2 # Enhanced date handling
|
| 25 |
+
PyJWT==2.8.0 # JWT tokens for stateless authentication
|