From 5d0ad2a2e38a2614e20e871761e4637f2e45a882 Mon Sep 17 00:00:00 2001 From: Untone Date: Wed, 24 Sep 2025 23:11:01 +0300 Subject: [PATCH] oauth-fix3 --- CHANGELOG.md | 3 +++ auth/oauth.py | 36 +++++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ada6fd2..da0cb9ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,12 @@ - 🔧 **OAuth Token Redirect**: Исправлена передача JWT токена - теперь токен передается через URL параметры (`access_token`) вместо cookie для корректной обработки фронтендом - 🔒 **OAuth State Security**: Добавлена обязательная передача `state` параметра в редиректе для CSRF защиты - 🔗 **OAuth URL Parameters**: Реализована поддержка передачи токена через URL query parameters согласно OAuth 2.0 спецификации +- 🔧 **Facebook OAuth PKCE**: Отключена поддержка PKCE для Facebook - провайдер не поддерживает Code Challenge +- 🔍 **OAuth Error Logging**: Добавлено детальное логирование ошибок OAuth для диагностики проблем с провайдерами ### Changed - 🍪 **OAuth Cookie Compatibility**: Cookie с сессией оставлена для обратной совместимости, но основной способ передачи токена - URL параметры +- 🔧 **OAuth PKCE Support**: Facebook добавлен в список провайдеров без PKCE поддержки ## [0.9.23] - 2025-09-24 diff --git a/auth/oauth.py b/auth/oauth.py index df6bdcf7..f45ba10e 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -523,10 +523,12 @@ async def oauth_callback(request: Any) -> JSONResponse | RedirectResponse: return response except Exception as e: - logger.error(f"OAuth callback error: {e!s}") + logger.error(f"OAuth callback error for {provider}: {e!s}", exc_info=True) + logger.error(f"OAuth callback request URL: {request.url}") + logger.error(f"OAuth callback query params: {dict(request.query_params)}") # В случае ошибки редиректим на фронтенд с ошибкой fallback_redirect = request.query_params.get("redirect_uri", FRONTEND_URL) - return RedirectResponse(url=f"{fallback_redirect}?error=auth_failed") + return RedirectResponse(url=f"{fallback_redirect}?error=auth_failed&provider={provider}") async def store_oauth_state(state: str, data: dict) -> None: @@ -589,18 +591,20 @@ async def oauth_login_http(request: Request) -> JSONResponse | RedirectResponse: 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}'") + logger.info(f"🔗 Callback URI for {provider}: '{callback_uri}'") # 🔍 Создаем redirect URL вручную (обходим использование request.session в authlib) - # VK не поддерживает PKCE, используем code_challenge только для поддерживающих провайдеров - if provider in ["vk", "yandex", "telegram"]: + # VK, Facebook не поддерживают PKCE, используем code_challenge только для поддерживающих провайдеров + if provider in ["vk", "yandex", "telegram", "facebook"]: # Провайдеры без PKCE поддержки + logger.info(f"🔧 Creating authorization URL without PKCE for {provider}") authorization_url = await client.create_authorization_url( callback_uri, state=state, ) else: - # Провайдеры с PKCE поддержкой (Google, GitHub, Facebook, X) + # Провайдеры с PKCE поддержкой (Google, GitHub, X) + logger.info(f"🔧 Creating authorization URL with PKCE for {provider}") authorization_url = await client.create_authorization_url( callback_uri, code_challenge=code_challenge, @@ -608,7 +612,7 @@ async def oauth_login_http(request: Request) -> JSONResponse | RedirectResponse: state=state, ) - logger.info(f"🚀 GitHub authorization URL: '{authorization_url['url']}'") + logger.info(f"🚀 {provider.title()} authorization URL: '{authorization_url['url']}'") return RedirectResponse(url=authorization_url["url"], status_code=302) except Exception as e: @@ -648,8 +652,9 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon return JSONResponse({"error": "Missing authorization code"}, status_code=400) # 🔍 Обмениваем code на токен - с PKCE или без в зависимости от провайдера - if provider in ["vk", "yandex", "telegram"]: - # Провайдеры без PKCE поддержки + if provider in ["vk", "yandex", "telegram", "facebook"]: + # Провайдеры без PKCE поддержки (Facebook может иметь проблемы с PKCE) + logger.info(f"🔧 Using OAuth without PKCE for {provider}") token = await client.fetch_access_token( authorization_response=str(request.url), ) @@ -659,17 +664,24 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon if not code_verifier: return JSONResponse({"error": "Missing code verifier in OAuth state"}, status_code=400) + logger.info(f"🔧 Using OAuth with PKCE for {provider}") token = await client.fetch_access_token( authorization_response=str(request.url), code_verifier=code_verifier, ) if not token: + logger.error(f"❌ Failed to get access token for {provider}") return JSONResponse({"error": "Failed to get access token"}, status_code=400) + logger.info(f"✅ Got access token for {provider}: {bool(token)}") + profile = await get_user_profile(provider, client, token) if not profile: + logger.error(f"❌ Failed to get user profile for {provider}") return JSONResponse({"error": "Failed to get user profile"}, status_code=400) + logger.info(f"✅ Got user profile for {provider}: id={profile.get('id')}, email={profile.get('email')}, name={profile.get('name')}") + # Создаем или обновляем пользователя используя helper функцию author = await _create_or_update_user(provider, profile) if not author: @@ -734,10 +746,12 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon return response except Exception as e: - logger.error(f"OAuth callback error: {e!s}") + logger.error(f"OAuth callback error for {provider}: {e!s}", exc_info=True) + logger.error(f"OAuth callback request URL: {request.url}") + logger.error(f"OAuth callback query params: {dict(request.query_params)}") # В случае ошибки редиректим на фронтенд с ошибкой fallback_redirect = request.query_params.get("redirect_uri", FRONTEND_URL) - return RedirectResponse(url=f"{fallback_redirect}?error=auth_failed") + return RedirectResponse(url=f"{fallback_redirect}?error=auth_failed&provider={provider}") async def _create_or_update_user(provider: str, profile: dict) -> Author: