Spaces:
Running
Running
Implement server-side OAuth for Docker Space
Browse files- Add OAuth endpoints to FastAPI backend (/api/auth/login, /api/auth/callback, /api/auth/session)
- Replace client-side OAuth with server-side flow using OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET
- Handle HuggingFace OAuth callback and token exchange
- Create session management for authenticated users
- Update frontend to use backend OAuth endpoints instead of @huggingface/hub
- Remove @huggingface/hub dependency (not needed for Docker Spaces)
- Fix 'Failed to start login process' error by implementing proper Docker Space OAuth
- backend_api.py +119 -2
- frontend/package.json +0 -1
- frontend/src/lib/auth.ts +51 -18
backend_api.py
CHANGED
|
@@ -1,19 +1,23 @@
|
|
| 1 |
"""
|
| 2 |
FastAPI backend for AnyCoder - provides REST API endpoints
|
| 3 |
"""
|
| 4 |
-
from fastapi import FastAPI, HTTPException, Header, WebSocket, WebSocketDisconnect
|
| 5 |
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
-
from fastapi.responses import StreamingResponse
|
| 7 |
from pydantic import BaseModel
|
| 8 |
from typing import Optional, List, Dict, AsyncGenerator
|
| 9 |
import json
|
| 10 |
import asyncio
|
| 11 |
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# Import only what we need, avoiding Gradio UI imports
|
| 14 |
import sys
|
| 15 |
import os
|
| 16 |
from huggingface_hub import InferenceClient
|
|
|
|
| 17 |
|
| 18 |
# Define models and languages here to avoid importing Gradio UI
|
| 19 |
AVAILABLE_MODELS = [
|
|
@@ -39,6 +43,19 @@ app.add_middleware(
|
|
| 39 |
allow_headers=["*"],
|
| 40 |
)
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
|
| 43 |
# Pydantic models for request/response
|
| 44 |
class CodeGenerationRequest(BaseModel):
|
|
@@ -132,6 +149,106 @@ async def get_languages():
|
|
| 132 |
return {"languages": LANGUAGE_CHOICES}
|
| 133 |
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
@app.get("/api/auth/status")
|
| 136 |
async def auth_status(authorization: Optional[str] = Header(None)):
|
| 137 |
"""Check authentication status"""
|
|
|
|
| 1 |
"""
|
| 2 |
FastAPI backend for AnyCoder - provides REST API endpoints
|
| 3 |
"""
|
| 4 |
+
from fastapi import FastAPI, HTTPException, Header, WebSocket, WebSocketDisconnect, Request, Response
|
| 5 |
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from fastapi.responses import StreamingResponse, RedirectResponse, JSONResponse
|
| 7 |
from pydantic import BaseModel
|
| 8 |
from typing import Optional, List, Dict, AsyncGenerator
|
| 9 |
import json
|
| 10 |
import asyncio
|
| 11 |
from datetime import datetime
|
| 12 |
+
import secrets
|
| 13 |
+
import base64
|
| 14 |
+
import urllib.parse
|
| 15 |
|
| 16 |
# Import only what we need, avoiding Gradio UI imports
|
| 17 |
import sys
|
| 18 |
import os
|
| 19 |
from huggingface_hub import InferenceClient
|
| 20 |
+
import httpx
|
| 21 |
|
| 22 |
# Define models and languages here to avoid importing Gradio UI
|
| 23 |
AVAILABLE_MODELS = [
|
|
|
|
| 43 |
allow_headers=["*"],
|
| 44 |
)
|
| 45 |
|
| 46 |
+
# OAuth configuration
|
| 47 |
+
OAUTH_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID", "")
|
| 48 |
+
OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET", "")
|
| 49 |
+
OAUTH_SCOPES = os.getenv("OAUTH_SCOPES", "openid profile manage-repos")
|
| 50 |
+
OPENID_PROVIDER_URL = os.getenv("OPENID_PROVIDER_URL", "https://huggingface.co")
|
| 51 |
+
SPACE_HOST = os.getenv("SPACE_HOST", "localhost:7860")
|
| 52 |
+
|
| 53 |
+
# In-memory store for OAuth states (in production, use Redis or similar)
|
| 54 |
+
oauth_states = {}
|
| 55 |
+
|
| 56 |
+
# In-memory store for user sessions
|
| 57 |
+
user_sessions = {}
|
| 58 |
+
|
| 59 |
|
| 60 |
# Pydantic models for request/response
|
| 61 |
class CodeGenerationRequest(BaseModel):
|
|
|
|
| 149 |
return {"languages": LANGUAGE_CHOICES}
|
| 150 |
|
| 151 |
|
| 152 |
+
@app.get("/api/auth/login")
|
| 153 |
+
async def oauth_login(request: Request):
|
| 154 |
+
"""Initiate OAuth login flow"""
|
| 155 |
+
# Generate a random state to prevent CSRF
|
| 156 |
+
state = secrets.token_urlsafe(32)
|
| 157 |
+
oauth_states[state] = {"timestamp": datetime.now()}
|
| 158 |
+
|
| 159 |
+
# Build redirect URI
|
| 160 |
+
protocol = "https" if SPACE_HOST and not SPACE_HOST.startswith("localhost") else "http"
|
| 161 |
+
redirect_uri = f"{protocol}://{SPACE_HOST}/api/auth/callback"
|
| 162 |
+
|
| 163 |
+
# Build authorization URL
|
| 164 |
+
auth_url = (
|
| 165 |
+
f"{OPENID_PROVIDER_URL}/oauth/authorize"
|
| 166 |
+
f"?client_id={OAUTH_CLIENT_ID}"
|
| 167 |
+
f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
|
| 168 |
+
f"&scope={urllib.parse.quote(OAUTH_SCOPES)}"
|
| 169 |
+
f"&state={state}"
|
| 170 |
+
f"&response_type=code"
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
return JSONResponse({"login_url": auth_url, "state": state})
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
@app.get("/api/auth/callback")
|
| 177 |
+
async def oauth_callback(code: str, state: str, request: Request):
|
| 178 |
+
"""Handle OAuth callback"""
|
| 179 |
+
# Verify state to prevent CSRF
|
| 180 |
+
if state not in oauth_states:
|
| 181 |
+
raise HTTPException(status_code=400, detail="Invalid state parameter")
|
| 182 |
+
|
| 183 |
+
# Clean up old states
|
| 184 |
+
oauth_states.pop(state, None)
|
| 185 |
+
|
| 186 |
+
# Exchange code for tokens
|
| 187 |
+
protocol = "https" if SPACE_HOST and not SPACE_HOST.startswith("localhost") else "http"
|
| 188 |
+
redirect_uri = f"{protocol}://{SPACE_HOST}/api/auth/callback"
|
| 189 |
+
|
| 190 |
+
# Prepare authorization header
|
| 191 |
+
auth_string = f"{OAUTH_CLIENT_ID}:{OAUTH_CLIENT_SECRET}"
|
| 192 |
+
auth_bytes = auth_string.encode('utf-8')
|
| 193 |
+
auth_b64 = base64.b64encode(auth_bytes).decode('utf-8')
|
| 194 |
+
|
| 195 |
+
async with httpx.AsyncClient() as client:
|
| 196 |
+
try:
|
| 197 |
+
token_response = await client.post(
|
| 198 |
+
f"{OPENID_PROVIDER_URL}/oauth/token",
|
| 199 |
+
data={
|
| 200 |
+
"client_id": OAUTH_CLIENT_ID,
|
| 201 |
+
"code": code,
|
| 202 |
+
"grant_type": "authorization_code",
|
| 203 |
+
"redirect_uri": redirect_uri,
|
| 204 |
+
},
|
| 205 |
+
headers={
|
| 206 |
+
"Authorization": f"Basic {auth_b64}",
|
| 207 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 208 |
+
},
|
| 209 |
+
)
|
| 210 |
+
token_response.raise_for_status()
|
| 211 |
+
token_data = token_response.json()
|
| 212 |
+
|
| 213 |
+
# Get user info
|
| 214 |
+
access_token = token_data.get("access_token")
|
| 215 |
+
userinfo_response = await client.get(
|
| 216 |
+
f"{OPENID_PROVIDER_URL}/oauth/userinfo",
|
| 217 |
+
headers={"Authorization": f"Bearer {access_token}"},
|
| 218 |
+
)
|
| 219 |
+
userinfo_response.raise_for_status()
|
| 220 |
+
user_info = userinfo_response.json()
|
| 221 |
+
|
| 222 |
+
# Create session
|
| 223 |
+
session_token = secrets.token_urlsafe(32)
|
| 224 |
+
user_sessions[session_token] = {
|
| 225 |
+
"access_token": access_token,
|
| 226 |
+
"user_info": user_info,
|
| 227 |
+
"timestamp": datetime.now(),
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
# Redirect to frontend with session token
|
| 231 |
+
frontend_url = f"{protocol}://{SPACE_HOST}/?session={session_token}"
|
| 232 |
+
return RedirectResponse(url=frontend_url)
|
| 233 |
+
|
| 234 |
+
except httpx.HTTPError as e:
|
| 235 |
+
print(f"OAuth error: {e}")
|
| 236 |
+
raise HTTPException(status_code=500, detail=f"OAuth failed: {str(e)}")
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@app.get("/api/auth/session")
|
| 240 |
+
async def get_session(session: str):
|
| 241 |
+
"""Get user info from session token"""
|
| 242 |
+
if session not in user_sessions:
|
| 243 |
+
raise HTTPException(status_code=401, detail="Invalid session")
|
| 244 |
+
|
| 245 |
+
session_data = user_sessions[session]
|
| 246 |
+
return {
|
| 247 |
+
"access_token": session_data["access_token"],
|
| 248 |
+
"user_info": session_data["user_info"],
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
|
| 252 |
@app.get("/api/auth/status")
|
| 253 |
async def auth_status(authorization: Optional[str] = Header(None)):
|
| 254 |
"""Check authentication status"""
|
frontend/package.json
CHANGED
|
@@ -14,7 +14,6 @@
|
|
| 14 |
"react-dom": "^18.3.1",
|
| 15 |
"axios": "^1.7.2",
|
| 16 |
"@monaco-editor/react": "^4.6.0",
|
| 17 |
-
"@huggingface/hub": "^0.15.1",
|
| 18 |
"prismjs": "^1.29.0",
|
| 19 |
"react-markdown": "^9.0.1",
|
| 20 |
"remark-gfm": "^4.0.0"
|
|
|
|
| 14 |
"react-dom": "^18.3.1",
|
| 15 |
"axios": "^1.7.2",
|
| 16 |
"@monaco-editor/react": "^4.6.0",
|
|
|
|
| 17 |
"prismjs": "^1.29.0",
|
| 18 |
"react-markdown": "^9.0.1",
|
| 19 |
"remark-gfm": "^4.0.0"
|
frontend/src/lib/auth.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
| 1 |
-
// HuggingFace OAuth authentication utilities
|
| 2 |
-
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from "@huggingface/hub";
|
| 3 |
|
| 4 |
const STORAGE_KEY = 'hf_oauth_token';
|
| 5 |
const USER_INFO_KEY = 'hf_user_info';
|
| 6 |
const DEV_MODE_KEY = 'hf_dev_mode';
|
|
|
|
| 7 |
|
| 8 |
// Check if we're in development mode (localhost)
|
| 9 |
const isDevelopment = typeof window !== 'undefined' &&
|
| 10 |
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
| 11 |
|
| 12 |
export interface OAuthUserInfo {
|
| 13 |
-
id
|
|
|
|
| 14 |
name: string;
|
|
|
|
| 15 |
preferredUsername?: string;
|
|
|
|
| 16 |
avatarUrl?: string;
|
| 17 |
}
|
| 18 |
|
|
@@ -43,13 +46,42 @@ export async function initializeOAuth(): Promise<OAuthResult | null> {
|
|
| 43 |
return null;
|
| 44 |
}
|
| 45 |
|
| 46 |
-
// Check if we're handling an OAuth
|
| 47 |
-
const
|
|
|
|
| 48 |
|
| 49 |
-
if (
|
| 50 |
-
//
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
|
| 55 |
// Check if we have stored credentials
|
|
@@ -59,7 +91,7 @@ export async function initializeOAuth(): Promise<OAuthResult | null> {
|
|
| 59 |
if (storedToken && storedUserInfo) {
|
| 60 |
return {
|
| 61 |
accessToken: storedToken,
|
| 62 |
-
accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
| 63 |
userInfo: storedUserInfo,
|
| 64 |
};
|
| 65 |
}
|
|
@@ -72,18 +104,19 @@ export async function initializeOAuth(): Promise<OAuthResult | null> {
|
|
| 72 |
}
|
| 73 |
|
| 74 |
/**
|
| 75 |
-
* Redirect to HuggingFace OAuth login page
|
| 76 |
*/
|
| 77 |
export async function loginWithHuggingFace(): Promise<void> {
|
| 78 |
try {
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
});
|
| 85 |
|
| 86 |
-
|
|
|
|
|
|
|
| 87 |
} catch (error) {
|
| 88 |
console.error('Failed to initiate OAuth login:', error);
|
| 89 |
throw new Error('Failed to start login process');
|
|
|
|
| 1 |
+
// HuggingFace OAuth authentication utilities (Server-side flow for Docker Spaces)
|
|
|
|
| 2 |
|
| 3 |
const STORAGE_KEY = 'hf_oauth_token';
|
| 4 |
const USER_INFO_KEY = 'hf_user_info';
|
| 5 |
const DEV_MODE_KEY = 'hf_dev_mode';
|
| 6 |
+
const API_BASE = '/api';
|
| 7 |
|
| 8 |
// Check if we're in development mode (localhost)
|
| 9 |
const isDevelopment = typeof window !== 'undefined' &&
|
| 10 |
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
| 11 |
|
| 12 |
export interface OAuthUserInfo {
|
| 13 |
+
id?: string;
|
| 14 |
+
sub?: string;
|
| 15 |
name: string;
|
| 16 |
+
preferred_username?: string;
|
| 17 |
preferredUsername?: string;
|
| 18 |
+
picture?: string;
|
| 19 |
avatarUrl?: string;
|
| 20 |
}
|
| 21 |
|
|
|
|
| 46 |
return null;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
// Check if we're handling an OAuth callback (session parameter in URL)
|
| 50 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 51 |
+
const sessionToken = urlParams.get('session');
|
| 52 |
|
| 53 |
+
if (sessionToken) {
|
| 54 |
+
// Fetch session data from backend
|
| 55 |
+
try {
|
| 56 |
+
const response = await fetch(`${API_BASE}/auth/session?session=${sessionToken}`);
|
| 57 |
+
if (response.ok) {
|
| 58 |
+
const data = await response.json();
|
| 59 |
+
|
| 60 |
+
// Normalize user info
|
| 61 |
+
const userInfo: OAuthUserInfo = {
|
| 62 |
+
id: data.user_info.sub || data.user_info.id,
|
| 63 |
+
name: data.user_info.name,
|
| 64 |
+
preferredUsername: data.user_info.preferred_username || data.user_info.preferredUsername,
|
| 65 |
+
avatarUrl: data.user_info.picture || data.user_info.avatarUrl,
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
const oauthResult: OAuthResult = {
|
| 69 |
+
accessToken: data.access_token,
|
| 70 |
+
accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
| 71 |
+
userInfo,
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
// Store the OAuth result
|
| 75 |
+
storeOAuthData(oauthResult);
|
| 76 |
+
|
| 77 |
+
// Clean up URL
|
| 78 |
+
window.history.replaceState({}, document.title, window.location.pathname);
|
| 79 |
+
|
| 80 |
+
return oauthResult;
|
| 81 |
+
}
|
| 82 |
+
} catch (error) {
|
| 83 |
+
console.error('Failed to fetch session:', error);
|
| 84 |
+
}
|
| 85 |
}
|
| 86 |
|
| 87 |
// Check if we have stored credentials
|
|
|
|
| 91 |
if (storedToken && storedUserInfo) {
|
| 92 |
return {
|
| 93 |
accessToken: storedToken,
|
| 94 |
+
accessTokenExpiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
|
| 95 |
userInfo: storedUserInfo,
|
| 96 |
};
|
| 97 |
}
|
|
|
|
| 104 |
}
|
| 105 |
|
| 106 |
/**
|
| 107 |
+
* Redirect to HuggingFace OAuth login page (via backend)
|
| 108 |
*/
|
| 109 |
export async function loginWithHuggingFace(): Promise<void> {
|
| 110 |
try {
|
| 111 |
+
// Call backend to get OAuth URL
|
| 112 |
+
const response = await fetch(`${API_BASE}/auth/login`);
|
| 113 |
+
if (!response.ok) {
|
| 114 |
+
throw new Error('Failed to get login URL');
|
| 115 |
+
}
|
|
|
|
| 116 |
|
| 117 |
+
const data = await response.json();
|
| 118 |
+
// Redirect to the OAuth authorization URL
|
| 119 |
+
window.location.href = data.login_url;
|
| 120 |
} catch (error) {
|
| 121 |
console.error('Failed to initiate OAuth login:', error);
|
| 122 |
throw new Error('Failed to start login process');
|