oauth-fix
All checks were successful
Deploy on push / deploy (push) Successful in 7m5s

This commit is contained in:
2025-09-24 13:35:49 +03:00
parent 26f28aa35e
commit 12023d9eda
3 changed files with 34 additions and 11 deletions

View File

@@ -5,6 +5,8 @@
### Fixed ### Fixed
- 🔧 **OAuth Callback URL**: Исправлено формирование callback URL - добавлен отсутствующий слеш между доменом и путем - 🔧 **OAuth Callback URL**: Исправлено формирование callback URL - добавлен отсутствующий слеш между доменом и путем
- 🔒 **OAuth HTTPS**: Принудительное использование HTTPS для callback URL в продакшне (исправляет ошибку "redirect_uri is not associated") - 🔒 **OAuth HTTPS**: Принудительное использование HTTPS для callback URL в продакшне (исправляет ошибку "redirect_uri is not associated")
- 🔧 **OAuth URL Parsing**: Исправлено извлечение базового URL - теперь используется только схема и хост без пути
- 🔄 **OAuth Path Support**: Добавлена поддержка redirect_uri в path параметрах для совместимости с фронтендом
### Changed ### Changed
- 🔄 **OAuth Routes**: Возвращены к стандартному формату `/oauth/{provider}` - провайдеры не передают параметр provider в callback - 🔄 **OAuth Routes**: Возвращены к стандартному формату `/oauth/{provider}` - провайдеры не передают параметр provider в callback

View File

@@ -408,10 +408,15 @@ async def oauth_login(_: None, _info: GraphQLResolveInfo, provider: str, callbac
await store_oauth_state(state, oauth_data) await store_oauth_state(state, oauth_data)
# Callback должен идти на backend с принудительным HTTPS для продакшна # Callback должен идти на backend с принудительным HTTPS для продакшна
base_url = callback_data["base_url"].rstrip("/") # Извлекаем только схему и хост из base_url (убираем путь!)
# Принудительно HTTPS from urllib.parse import urlparse
base_url = base_url.replace("http://", "https://")
oauth_callback_uri = f"{base_url}/oauth/{provider}/callback" parsed_url = urlparse(callback_data["base_url"])
scheme = "https" if parsed_url.netloc != "localhost:8000" else parsed_url.scheme
backend_base_url = f"{scheme}://{parsed_url.netloc}"
oauth_callback_uri = f"{backend_base_url}/oauth/{provider}/callback"
logger.info(f"🔗 GraphQL callback URI: '{oauth_callback_uri}'")
try: try:
return await client.authorize_redirect( return await client.authorize_redirect(
@@ -526,7 +531,12 @@ async def oauth_login_http(request: Request) -> JSONResponse | RedirectResponse:
"""HTTP handler для OAuth login""" """HTTP handler для OAuth login"""
try: try:
provider = request.path_params.get("provider") provider = request.path_params.get("provider")
logger.info(
f"🔍 OAuth login request: provider='{provider}', url='{request.url}', path_params={request.path_params}, query_params={dict(request.query_params)}"
)
if not provider or provider not in PROVIDER_CONFIGS: if not provider or provider not in PROVIDER_CONFIGS:
logger.error(f"❌ Invalid provider: '{provider}', available: {list(PROVIDER_CONFIGS.keys())}")
return JSONResponse({"error": "Invalid provider"}, status_code=400) return JSONResponse({"error": "Invalid provider"}, status_code=400)
client = oauth.create_client(provider) client = oauth.create_client(provider)
@@ -540,8 +550,12 @@ async def oauth_login_http(request: Request) -> JSONResponse | RedirectResponse:
state = token_urlsafe(32) state = token_urlsafe(32)
# 🔍 Сохраняем состояние OAuth только в Redis (убираем зависимость от request.session) # 🔍 Сохраняем состояние OAuth только в Redis (убираем зависимость от request.session)
# Получаем redirect_uri из query параметров или используем FRONTEND_URL по умолчанию # Получаем redirect_uri из query параметров, path параметров или используем FRONTEND_URL по умолчанию
final_redirect_uri = request.query_params.get("redirect_uri", FRONTEND_URL) final_redirect_uri = (
request.query_params.get("redirect_uri") or request.path_params.get("redirect_uri") or FRONTEND_URL
)
logger.info(f"🎯 Final redirect URI: '{final_redirect_uri}'")
oauth_data = { oauth_data = {
"code_verifier": code_verifier, "code_verifier": code_verifier,
"provider": provider, "provider": provider,
@@ -550,10 +564,13 @@ async def oauth_login_http(request: Request) -> JSONResponse | RedirectResponse:
} }
await store_oauth_state(state, oauth_data) await store_oauth_state(state, oauth_data)
# Получаем backend URL из request (принудительно HTTPS для продакшна) # Получаем БАЗОВЫЙ backend URL (только схема + хост, без пути!)
scheme = "https" if request.url.netloc != "localhost:8000" else request.url.scheme scheme = "https" if request.url.netloc != "localhost:8000" else request.url.scheme
backend_url = f"{scheme}://{request.url.netloc}" backend_base_url = f"{scheme}://{request.url.netloc}"
callback_uri = f"{backend_url}/oauth/{provider}/callback" callback_uri = f"{backend_base_url}/oauth/{provider}/callback"
logger.info(f"🔗 Backend base URL: '{backend_base_url}'")
logger.info(f"🔗 Callback URI for GitHub: '{callback_uri}'")
# 🔍 Создаем redirect URL вручную (обходим использование request.session в authlib) # 🔍 Создаем redirect URL вручную (обходим использование request.session в authlib)
# VK не поддерживает PKCE, используем code_challenge только для поддерживающих провайдеров # VK не поддерживает PKCE, используем code_challenge только для поддерживающих провайдеров
@@ -572,6 +589,7 @@ async def oauth_login_http(request: Request) -> JSONResponse | RedirectResponse:
state=state, state=state,
) )
logger.info(f"🚀 GitHub authorization URL: '{authorization_url['url']}'")
return RedirectResponse(url=authorization_url["url"], status_code=302) return RedirectResponse(url=authorization_url["url"], status_code=302)
except Exception as e: except Exception as e:

View File

@@ -302,9 +302,12 @@ async def lifespan(app: Starlette):
app = Starlette( app = Starlette(
routes=[ routes=[
Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]), Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]),
# OAuth маршруты # OAuth маршруты - порядок важен! Более специфичные маршруты должны быть первыми
Route("/oauth/{provider}", oauth_login_http, methods=["GET"]),
Route("/oauth/{provider}/callback", oauth_callback_http, methods=["GET"]), Route("/oauth/{provider}/callback", oauth_callback_http, methods=["GET"]),
Route(
"/oauth/{provider}/{redirect_uri:path}", oauth_login_http, methods=["GET"]
), # Поддержка старого формата фронтенда
Route("/oauth/{provider}", oauth_login_http, methods=["GET"]),
# Health check endpoint # Health check endpoint
Route("/health", health_handler, methods=["GET"]), Route("/health", health_handler, methods=["GET"]),
# Статические файлы (CSS, JS, изображения) # Статические файлы (CSS, JS, изображения)