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.

Files changed (2) hide show
  1. app.py +91 -27
  2. 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 handles Hugging Face Spaces load balancing
76
- by attempting to restore user session from stored session data
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
- # If not authenticated, try to restore from session data (for load balancing)
85
- if 'user_id' in session and 'user_email' in session:
86
- try:
87
- # Try to load user from database
88
- user = User.get_by_id(session['user_id'])
89
- if user:
90
- # Restore the user session
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) # Use remember=True for iframe compatibility
 
 
 
280
 
281
- # Store user info in session as backup for load balancing issues
282
  session['user_id'] = str(user.id)
283
  session['user_email'] = user.email
284
  session['login_time'] = datetime.utcnow().isoformat()
285
 
286
- # Debug session information
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
- # Redirect to next page or chat interface
295
  next_page = request.args.get('next')
296
  if next_page and next_page.startswith('/'):
297
- return redirect(next_page)
298
- return redirect(url_for('chat_interface'))
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Redirect to login page
326
- return redirect(url_for('login'))
 
 
 
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
- return redirect(url_for('login'))
 
 
 
 
 
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