akhaliq HF Staff commited on
Commit
c6abd4e
·
1 Parent(s): f4952e1

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

Files changed (3) hide show
  1. backend_api.py +119 -2
  2. frontend/package.json +0 -1
  3. 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: string;
 
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 redirect
47
- const oauthResult = await oauthHandleRedirectIfPresent();
 
48
 
49
- if (oauthResult) {
50
- // Store the OAuth result
51
- storeOAuthData(oauthResult);
52
- return oauthResult;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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), // Assume 24h
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
- const loginUrl = await oauthLoginUrl({
80
- // Redirect back to the current page
81
- redirectUrl: window.location.href,
82
- // Request scopes - adjust as needed
83
- scopes: "openid profile inference-api",
84
- });
85
 
86
- window.location.href = loginUrl;
 
 
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');