Переглянути джерело

Add signed cookie-based GitHub authentication caching (#4853)

Co-authored-by: openhands <openhands@all-hands.dev>
Robert Brennan 1 рік тому
батько
коміт
e1383afbc3
2 змінених файлів з 46 додано та 7 видалено
  1. 10 0
      frontend/src/services/api.ts
  2. 36 7
      openhands/server/listen.py

+ 10 - 0
frontend/src/services/api.ts

@@ -63,6 +63,16 @@ export async function request(
   } catch (e) {
     onFail(`Error fetching ${url}`);
   }
+  if (response?.status === 401) {
+    await request(
+      "/api/authenticate",
+      {
+        method: "POST",
+      },
+      true,
+    );
+    return request(url, options, disableToast, returnResponse, maxRetries - 1);
+  }
   if (response?.status && response?.status >= 400) {
     onFail(
       `${response.status} error while fetching ${url}: ${response?.statusText}`,

+ 36 - 7
openhands/server/listen.py

@@ -2,10 +2,12 @@ import asyncio
 import os
 import re
 import tempfile
+import time
 import uuid
 import warnings
 from contextlib import asynccontextmanager
 
+import jwt
 import requests
 from pathspec import PathSpec
 from pathspec.patterns import GitWildMatchPattern
@@ -15,6 +17,7 @@ from openhands.server.data_models.feedback import FeedbackDataModel, store_feedb
 from openhands.server.github import (
     GITHUB_CLIENT_ID,
     GITHUB_CLIENT_SECRET,
+    UserVerifier,
     authenticate_github_user,
 )
 from openhands.storage import get_file_store
@@ -60,7 +63,7 @@ from openhands.events.serialization import event_to_dict
 from openhands.events.stream import AsyncEventStreamWrapper
 from openhands.llm import bedrock
 from openhands.runtime.base import Runtime
-from openhands.server.auth import get_sid_from_token, sign_token
+from openhands.server.auth.auth import get_sid_from_token, sign_token
 from openhands.server.middleware import LocalhostCORSMiddleware, NoCacheMiddleware
 from openhands.server.session import SessionManager
 
@@ -204,12 +207,22 @@ async def attach_session(request: Request, call_next):
         response = await call_next(request)
         return response
 
-    github_token = request.headers.get('X-GitHub-Token')
-    if not await authenticate_github_user(github_token):
-        return JSONResponse(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            content={'error': 'Not authenticated'},
-        )
+    user_verifier = UserVerifier()
+    if user_verifier.is_active():
+        signed_token = request.cookies.get('github_auth')
+        if not signed_token:
+            return JSONResponse(
+                status_code=status.HTTP_401_UNAUTHORIZED,
+                content={'error': 'Not authenticated'},
+            )
+        try:
+            jwt.decode(signed_token, config.jwt_secret, algorithms=['HS256'])
+        except Exception as e:
+            logger.warning(f'Invalid token: {e}')
+            return JSONResponse(
+                status_code=status.HTTP_401_UNAUTHORIZED,
+                content={'error': 'Invalid token'},
+            )
 
     if not request.headers.get('Authorization'):
         logger.warning('Missing Authorization header')
@@ -864,10 +877,26 @@ async def authenticate(request: Request):
             content={'error': 'Not authorized via GitHub waitlist'},
         )
 
+    # Create a signed JWT token with 1-hour expiration
+    cookie_data = {
+        'github_token': token,
+        'exp': int(time.time()) + 3600,  # 1 hour expiration
+    }
+    signed_token = sign_token(cookie_data, config.jwt_secret)
+
     response = JSONResponse(
         status_code=status.HTTP_200_OK, content={'message': 'User authenticated'}
     )
 
+    # Set secure cookie with signed token
+    response.set_cookie(
+        key='github_auth',
+        value=signed_token,
+        max_age=3600,  # 1 hour in seconds
+        httponly=True,
+        secure=True,
+        samesite='strict',
+    )
     return response