lovelymango commited on
Commit
df7f87f
·
verified ·
1 Parent(s): f6c65c7

Update app/oauth_proxy/routes.py

Browse files
Files changed (1) hide show
  1. app/oauth_proxy/routes.py +36 -22
app/oauth_proxy/routes.py CHANGED
@@ -3,11 +3,6 @@ GPTs OAuth Proxy Routes
3
  =======================
4
 
5
  GPTs Actions OAuth 플로우를 Seats.aero OAuth로 프록시
6
-
7
- 플로우:
8
- 1. GPTs -> /oauth2/authorize -> Seats.aero consent
9
- 2. Seats.aero -> /oauth2/callback -> GPTs callback
10
- 3. GPTs -> /oauth2/token -> Seats.aero token (프록시)
11
  """
12
 
13
  import logging
@@ -35,6 +30,7 @@ from .config import (
35
  logger = logging.getLogger("seats-proxy.oauth-proxy")
36
 
37
  # State 저장소 (메모리 기반)
 
38
  _state_store: Dict[str, dict] = {}
39
  STATE_TTL = 600 # 10분
40
  MAX_STATES = 1000
@@ -69,12 +65,22 @@ def _create_proxy_state(gpts_redirect_uri: str, gpts_state: str) -> str:
69
  return proxy_state
70
 
71
 
72
- def _get_and_consume_state(proxy_state: str) -> Optional[dict]:
 
 
 
 
 
 
73
  """State 조회 및 삭제 (일회용)"""
74
  _cleanup_expired_states()
75
  return _state_store.pop(proxy_state, None)
76
 
77
 
 
 
 
 
78
  async def oauth2_authorize(request: Request):
79
  """GPTs OAuth 시작점"""
80
  if not is_configured():
@@ -150,7 +156,7 @@ async def oauth2_callback(request: Request):
150
  if error:
151
  logger.error(f"OAuth error from Seats.aero: {error} - {error_description}")
152
  if proxy_state:
153
- state_data = _get_and_consume_state(proxy_state)
154
  if state_data:
155
  error_params = urlencode({
156
  "error": error,
@@ -169,18 +175,21 @@ async def oauth2_callback(request: Request):
169
  if not proxy_state:
170
  return _error_html_response("missing_state", "State parameter not provided")
171
 
172
- state_data = _get_and_consume_state(proxy_state)
173
  if not state_data:
174
  logger.warning(f"Invalid or expired proxy_state")
175
  return _error_html_response("invalid_state", "State expired or invalid")
176
 
 
 
 
177
  gpts_redirect_uri = state_data["gpts_redirect_uri"]
178
  gpts_state = state_data["gpts_state"]
179
 
180
  redirect_params = urlencode({"code": code, "state": gpts_state})
181
  redirect_url = f"{gpts_redirect_uri}?{redirect_params}"
182
 
183
- logger.info(f"Redirecting to GPTs callback")
184
  return RedirectResponse(redirect_url, status_code=302)
185
 
186
 
@@ -197,14 +206,12 @@ async def oauth2_token(request: Request) -> JSONResponse:
197
  content_type = request.headers.get("content-type", "")
198
 
199
  logger.info(f"Token request: content-type={content_type}")
200
- logger.debug(f"Token request body: {body_bytes[:500]}")
201
 
202
  # 파싱 시도
203
  data = {}
204
 
205
  try:
206
  if "application/x-www-form-urlencoded" in content_type:
207
- # Form 데이터 파싱
208
  body_str = body_bytes.decode("utf-8")
209
  parsed = parse_qs(body_str)
210
  data = {k: v[0] if len(v) == 1 else v for k, v in parsed.items()}
@@ -214,7 +221,6 @@ async def oauth2_token(request: Request) -> JSONResponse:
214
  data = json.loads(body_bytes)
215
  logger.info(f"Parsed as JSON: {list(data.keys())}")
216
  else:
217
- # 둘 다 시도
218
  body_str = body_bytes.decode("utf-8")
219
  try:
220
  import json
@@ -231,14 +237,13 @@ async def oauth2_token(request: Request) -> JSONResponse:
231
  "error_description": f"Could not parse request body: {str(e)}"
232
  }, status_code=400)
233
 
234
- # 파라미터 추출
235
  grant_type = data.get("grant_type")
236
  client_id = data.get("client_id")
237
  client_secret = data.get("client_secret")
238
 
239
  logger.info(f"Token params: grant_type={grant_type}, client_id={client_id}")
240
 
241
- # 클라이언트 인증 - client_id 검증
242
  if client_id and client_id != PROXY_CLIENT_ID:
243
  logger.warning(f"Invalid client_id: {client_id}")
244
  return JSONResponse({
@@ -246,7 +251,6 @@ async def oauth2_token(request: Request) -> JSONResponse:
246
  "error_description": "Unknown client_id"
247
  }, status_code=401)
248
 
249
- # 클라이언트 인증 - client_secret 검증
250
  if client_secret and client_secret != PROXY_CLIENT_SECRET:
251
  logger.warning("Invalid client_secret")
252
  return JSONResponse({
@@ -254,7 +258,6 @@ async def oauth2_token(request: Request) -> JSONResponse:
254
  "error_description": "Invalid client_secret"
255
  }, status_code=401)
256
 
257
- # grant_type 검증
258
  if not grant_type:
259
  return JSONResponse({
260
  "error": "invalid_request",
@@ -276,14 +279,24 @@ async def oauth2_token(request: Request) -> JSONResponse:
276
  "error_description": "code is required"
277
  }, status_code=400)
278
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  seats_data["code"] = code
280
  seats_data["redirect_uri"] = PROXY_CALLBACK_URL
 
281
  seats_data["scope"] = "openid"
282
 
283
- # state 있으면 전달
284
- state = data.get("state")
285
- if state:
286
- seats_data["state"] = state
287
 
288
  elif grant_type == "refresh_token":
289
  refresh_token = data.get("refresh_token")
@@ -301,8 +314,8 @@ async def oauth2_token(request: Request) -> JSONResponse:
301
  "error_description": f"grant_type '{grant_type}' is not supported"
302
  }, status_code=400)
303
 
304
- logger.info(f"Calling Seats.aero token endpoint with grant_type={grant_type}")
305
- logger.debug(f"Seats.aero request data keys: {list(seats_data.keys())}")
306
 
307
  # Seats.aero 토큰 엔드포인트 호출
308
  async with httpx.AsyncClient(timeout=30) as client:
@@ -314,6 +327,7 @@ async def oauth2_token(request: Request) -> JSONResponse:
314
  )
315
 
316
  logger.info(f"Seats.aero response: {response.status_code}")
 
317
 
318
  if response.status_code != 200:
319
  logger.error(f"Seats.aero token error: {response.status_code} - {response.text[:500]}")
 
3
  =======================
4
 
5
  GPTs Actions OAuth 플로우를 Seats.aero OAuth로 프록시
 
 
 
 
 
6
  """
7
 
8
  import logging
 
30
  logger = logging.getLogger("seats-proxy.oauth-proxy")
31
 
32
  # State 저장소 (메모리 기반)
33
+ # 구조: {proxy_state: {gpts_redirect_uri, gpts_state, seats_state, created_at}}
34
  _state_store: Dict[str, dict] = {}
35
  STATE_TTL = 600 # 10분
36
  MAX_STATES = 1000
 
65
  return proxy_state
66
 
67
 
68
+ def _get_state_data(proxy_state: str) -> Optional[dict]:
69
+ """State 조회 (삭제하지 않음 - 토큰 교환에도 필요)"""
70
+ _cleanup_expired_states()
71
+ return _state_store.get(proxy_state)
72
+
73
+
74
+ def _consume_state(proxy_state: str) -> Optional[dict]:
75
  """State 조회 및 삭제 (일회용)"""
76
  _cleanup_expired_states()
77
  return _state_store.pop(proxy_state, None)
78
 
79
 
80
+ # Authorization code와 state 매핑 저장 (토큰 교환 시 state 찾기용)
81
+ _code_to_state: Dict[str, str] = {}
82
+
83
+
84
  async def oauth2_authorize(request: Request):
85
  """GPTs OAuth 시작점"""
86
  if not is_configured():
 
156
  if error:
157
  logger.error(f"OAuth error from Seats.aero: {error} - {error_description}")
158
  if proxy_state:
159
+ state_data = _consume_state(proxy_state)
160
  if state_data:
161
  error_params = urlencode({
162
  "error": error,
 
175
  if not proxy_state:
176
  return _error_html_response("missing_state", "State parameter not provided")
177
 
178
+ state_data = _get_state_data(proxy_state)
179
  if not state_data:
180
  logger.warning(f"Invalid or expired proxy_state")
181
  return _error_html_response("invalid_state", "State expired or invalid")
182
 
183
+ # code와 proxy_state 매핑 저장 (토큰 교환 시 사용)
184
+ _code_to_state[code] = proxy_state
185
+
186
  gpts_redirect_uri = state_data["gpts_redirect_uri"]
187
  gpts_state = state_data["gpts_state"]
188
 
189
  redirect_params = urlencode({"code": code, "state": gpts_state})
190
  redirect_url = f"{gpts_redirect_uri}?{redirect_params}"
191
 
192
+ logger.info(f"Redirecting to GPTs callback, code mapped to proxy_state")
193
  return RedirectResponse(redirect_url, status_code=302)
194
 
195
 
 
206
  content_type = request.headers.get("content-type", "")
207
 
208
  logger.info(f"Token request: content-type={content_type}")
 
209
 
210
  # 파싱 시도
211
  data = {}
212
 
213
  try:
214
  if "application/x-www-form-urlencoded" in content_type:
 
215
  body_str = body_bytes.decode("utf-8")
216
  parsed = parse_qs(body_str)
217
  data = {k: v[0] if len(v) == 1 else v for k, v in parsed.items()}
 
221
  data = json.loads(body_bytes)
222
  logger.info(f"Parsed as JSON: {list(data.keys())}")
223
  else:
 
224
  body_str = body_bytes.decode("utf-8")
225
  try:
226
  import json
 
237
  "error_description": f"Could not parse request body: {str(e)}"
238
  }, status_code=400)
239
 
 
240
  grant_type = data.get("grant_type")
241
  client_id = data.get("client_id")
242
  client_secret = data.get("client_secret")
243
 
244
  logger.info(f"Token params: grant_type={grant_type}, client_id={client_id}")
245
 
246
+ # 클라이언트 인증
247
  if client_id and client_id != PROXY_CLIENT_ID:
248
  logger.warning(f"Invalid client_id: {client_id}")
249
  return JSONResponse({
 
251
  "error_description": "Unknown client_id"
252
  }, status_code=401)
253
 
 
254
  if client_secret and client_secret != PROXY_CLIENT_SECRET:
255
  logger.warning("Invalid client_secret")
256
  return JSONResponse({
 
258
  "error_description": "Invalid client_secret"
259
  }, status_code=401)
260
 
 
261
  if not grant_type:
262
  return JSONResponse({
263
  "error": "invalid_request",
 
279
  "error_description": "code is required"
280
  }, status_code=400)
281
 
282
+ # code에서 proxy_state 찾기
283
+ proxy_state = _code_to_state.pop(code, None)
284
+ if not proxy_state:
285
+ logger.warning(f"No proxy_state found for code")
286
+ # proxy_state 없이도 시도해봄
287
+ proxy_state = ""
288
+
289
+ # state_data에서 정보 가져오기 (있으면)
290
+ if proxy_state:
291
+ state_data = _consume_state(proxy_state)
292
+ logger.info(f"Found state_data for token exchange")
293
+
294
  seats_data["code"] = code
295
  seats_data["redirect_uri"] = PROXY_CALLBACK_URL
296
+ seats_data["state"] = proxy_state # Seats.aero에 보냈던 원래 state
297
  seats_data["scope"] = "openid"
298
 
299
+ logger.info(f"Token exchange: code={code[:20]}..., state={proxy_state[:10] if proxy_state else 'none'}...")
 
 
 
300
 
301
  elif grant_type == "refresh_token":
302
  refresh_token = data.get("refresh_token")
 
314
  "error_description": f"grant_type '{grant_type}' is not supported"
315
  }, status_code=400)
316
 
317
+ logger.info(f"Calling Seats.aero token endpoint")
318
+ logger.debug(f"Seats.aero request: {seats_data}")
319
 
320
  # Seats.aero 토큰 엔드포인트 호출
321
  async with httpx.AsyncClient(timeout=30) as client:
 
327
  )
328
 
329
  logger.info(f"Seats.aero response: {response.status_code}")
330
+ logger.debug(f"Seats.aero response body: {response.text[:500]}")
331
 
332
  if response.status_code != 200:
333
  logger.error(f"Seats.aero token error: {response.status_code} - {response.text[:500]}")