diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5e0656..9405ac65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - 🚫 **OAuth Error**: Исправлена ошибка "Provider not configured" при пустых переменных окружения OAuth - 🔐 **OAuth Session-Free**: Убрана зависимость от SessionMiddleware - OAuth использует только Redis для состояния - 🏷️ **Type Safety**: Исправлена MyPy ошибка с request.client.host - добавлена проверка на None +- 🔑 **VK OAuth PKCE**: Убрана поддержка PKCE для VK/Yandex/Telegram - эти провайдеры не поддерживают code_challenge - 🔒 **OAuth Facebook**: Обновлена версия API с v13.0 до v18.0 (актуальная) - 🔒 **OAuth Facebook**: Добавлены обязательные scope и параметры безопасности - 🔒 **OAuth Facebook**: Улучшена обработка ошибок API и валидация ответов diff --git a/auth/oauth.py b/auth/oauth.py index ab411de4..cacbd242 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -549,13 +549,22 @@ async def oauth_login_http(request: Request) -> JSONResponse | RedirectResponse: callback_uri = f"{FRONTEND_URL}oauth/{provider}/callback" # 🔍 Создаем redirect URL вручную (обходим использование request.session в authlib) - authorization_url = await client.create_authorization_url( - callback_uri, - code_challenge=code_challenge, - code_challenge_method="S256", - state=state, - ) - + # VK не поддерживает PKCE, используем code_challenge только для поддерживающих провайдеров + if provider in ["vk", "yandex", "telegram"]: + # Провайдеры без PKCE поддержки + authorization_url = await client.create_authorization_url( + callback_uri, + state=state, + ) + else: + # Провайдеры с PKCE поддержкой (Google, GitHub, Facebook, X) + authorization_url = await client.create_authorization_url( + callback_uri, + code_challenge=code_challenge, + code_challenge_method="S256", + state=state, + ) + return RedirectResponse(url=authorization_url["url"], status_code=302) except Exception as e: @@ -584,21 +593,27 @@ async def oauth_callback_http(request: Request) -> JSONResponse | RedirectRespon if not client: return JSONResponse({"error": "Provider not configured"}, status_code=400) - # 🔍 Получаем code_verifier из Redis вместо request.session - code_verifier = oauth_data.get("code_verifier") - if not code_verifier: - return JSONResponse({"error": "Missing code verifier in OAuth state"}, status_code=400) - # Получаем authorization code из query параметров code = request.query_params.get("code") if not code: return JSONResponse({"error": "Missing authorization code"}, status_code=400) - - # Обмениваем code на токен вручную - token = await client.fetch_access_token( - authorization_response=str(request.url), - code_verifier=code_verifier, - ) + + # 🔍 Обмениваем code на токен - с PKCE или без в зависимости от провайдера + if provider in ["vk", "yandex", "telegram"]: + # Провайдеры без PKCE поддержки + token = await client.fetch_access_token( + authorization_response=str(request.url), + ) + else: + # Провайдеры с PKCE поддержкой + code_verifier = oauth_data.get("code_verifier") + if not code_verifier: + return JSONResponse({"error": "Missing code verifier in OAuth state"}, status_code=400) + + token = await client.fetch_access_token( + authorization_response=str(request.url), + code_verifier=code_verifier, + ) if not token: return JSONResponse({"error": "Failed to get access token"}, status_code=400) diff --git a/tests/auth/test_oauth_functional.py b/tests/auth/test_oauth_functional.py index 4b227833..20772a45 100644 --- a/tests/auth/test_oauth_functional.py +++ b/tests/auth/test_oauth_functional.py @@ -251,6 +251,34 @@ class TestOAuthFunctional: if isinstance(body, memoryview): body = bytes(body) assert b"Missing authorization code" in body + + @pytest.mark.asyncio + async def test_vk_oauth_without_pkce(self): + """Тест VK OAuth без PKCE (VK не поддерживает code_challenge)""" + + request = MagicMock(spec=Request) + request.path_params = {"provider": "vk"} + + mock_client = AsyncMock() + # VK должен вызываться без code_challenge + mock_client.create_authorization_url = AsyncMock(return_value={ + "url": "https://oauth.vk.com/authorize?client_id=test&state=abc123" + }) + + with patch("auth.oauth.oauth.create_client", return_value=mock_client), \ + patch("auth.oauth.store_oauth_state") as mock_store: + + response = await oauth_login_http(request) + + assert isinstance(response, RedirectResponse) + assert response.status_code == 302 + + # Проверяем что create_authorization_url вызван БЕЗ code_challenge для VK + call_args = mock_client.create_authorization_url.call_args + assert "code_challenge" not in call_args.kwargs + assert "code_challenge_method" not in call_args.kwargs + assert "state" in call_args.kwargs + if __name__ == "__main__":