diff --git a/CHANGELOG.md b/CHANGELOG.md index 23342f0c..e99a482f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.9.22] - 2025-09-22 + +### Fixed +- 🔒 **OAuth Facebook**: Обновлена версия API с v13.0 до v18.0 (актуальная) +- 🔒 **OAuth Facebook**: Добавлены обязательные scope и параметры безопасности +- 🔒 **OAuth Facebook**: Улучшена обработка ошибок API и валидация ответов +- 🔒 **OAuth VK**: Обновлена версия API с v5.131 до v5.199+ (актуальная) +- 🔒 **OAuth VK**: Исправлен endpoint с `authors.get` на `users.get` +- 🔒 **OAuth GitHub**: Добавлены обязательные scope `read:user user:email` +- 🔒 **OAuth GitHub**: Улучшена обработка ошибок и получения email адресов +- 🔒 **OAuth Google**: Добавлены обязательные scope для OpenID Connect +- 🔒 **OAuth X/Twitter**: Исправлен endpoint с `authors/me` на `users/me` +- 🔒 **Session Cookies**: Автоматическое определение HTTPS через переменную окружения HTTPS_ENABLED +- 🏷️ **Type Safety**: Исправлена ошибка в OAuth регистрации провайдеров + ## [0.9.21] - 2025-09-21 ### 📚 Documentation Updates diff --git a/auth/oauth.py b/auth/oauth.py index 429a7dc3..4dcedc10 100644 --- a/auth/oauth.py +++ b/auth/oauth.py @@ -78,16 +78,23 @@ OAUTH_STATE_TTL = 600 # 10 минут PROVIDER_CONFIGS = { "google": { "server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration", + "client_kwargs": { + "scope": "openid email profile", + }, }, "github": { "access_token_url": "https://github.com/login/oauth/access_token", "authorize_url": "https://github.com/login/oauth/authorize", "api_base_url": "https://api.github.com/", + "client_kwargs": { + "scope": "read:user user:email", + }, }, "facebook": { - "access_token_url": "https://graph.facebook.com/v13.0/oauth/access_token", - "authorize_url": "https://www.facebook.com/v13.0/dialog/oauth", + "access_token_url": "https://graph.facebook.com/v18.0/oauth/access_token", + "authorize_url": "https://www.facebook.com/v18.0/dialog/oauth", "api_base_url": "https://graph.facebook.com/", + "scope": "email public_profile", # Явно указываем необходимые scope }, "x": { "access_token_url": "https://api.twitter.com/2/oauth2/token", @@ -102,6 +109,9 @@ PROVIDER_CONFIGS = { "access_token_url": "https://oauth.vk.com/access_token", "authorize_url": "https://oauth.vk.com/authorize", "api_base_url": "https://api.vk.com/method/", + "client_kwargs": { + "scope": "email", # Минимальный scope для получения email + }, }, "yandex": { "access_token_url": "https://oauth.yandex.ru/token", @@ -128,13 +138,27 @@ def _register_oauth_provider(provider: str, client_config: dict) -> None: return # Базовые параметры для всех провайдеров - register_params = { + register_params: dict[str, Any] = { "name": provider, "client_id": client_config["id"], "client_secret": client_config["key"], - **provider_config, } + # Добавляем конфигурацию провайдера с явной типизацией + if isinstance(provider_config, dict): + register_params.update(provider_config) + + # 🔒 Для Facebook добавляем дополнительные параметры безопасности + if provider == "facebook": + register_params.update( + { + "client_kwargs": { + "scope": "email public_profile", + "token_endpoint_auth_method": "client_secret_post", + } + } + ) + oauth.register(**register_params) logger.info(f"OAuth provider {provider} registered successfully") except Exception as e: @@ -174,51 +198,101 @@ PROVIDER_HANDLERS = { async def _fetch_github_profile(client: Any, token: Any) -> dict: """Получает профиль из GitHub API""" - profile = await client.get("user", token=token) - profile_data = profile.json() - emails = await client.get("user/emails", token=token) - emails_data = emails.json() - primary_email = next((email["email"] for email in emails_data if email["primary"]), None) - return { - "id": str(profile_data["id"]), - "email": primary_email or profile_data.get("email"), - "name": profile_data.get("name") or profile_data.get("login"), - "picture": profile_data.get("avatar_url"), - } + try: + # Получаем основной профиль + profile = await client.get("user", token=token) + profile_data = profile.json() + + # Проверяем наличие ошибок в ответе GitHub + if "message" in profile_data: + logger.error(f"GitHub API error: {profile_data['message']}") + return {} + + # Получаем email адреса (требует scope user:email) + emails = await client.get("user/emails", token=token) + emails_data = emails.json() + + # Ищем основной email + primary_email = None + if isinstance(emails_data, list): + primary_email = next((email["email"] for email in emails_data if email.get("primary")), None) + + return { + "id": str(profile_data.get("id", "")), + "email": primary_email or profile_data.get("email"), + "name": profile_data.get("name") or profile_data.get("login", ""), + "picture": profile_data.get("avatar_url"), + } + except Exception as e: + logger.error(f"Error fetching GitHub profile: {e}") + return {} async def _fetch_facebook_profile(client: Any, token: Any) -> dict: """Получает профиль из Facebook API""" - profile = await client.get("me?fields=id,name,email,picture.width(600)", token=token) - profile_data = profile.json() - return { - "id": profile_data["id"], - "email": profile_data.get("email"), - "name": profile_data.get("name"), - "picture": profile_data.get("picture", {}).get("data", {}).get("url"), - } + try: + # Используем актуальную версию API v18.0+ и расширенные поля + profile = await client.get("me?fields=id,name,email,picture.width(600).height(600)", token=token) + profile_data = profile.json() + + # Проверяем наличие ошибок в ответе Facebook + if "error" in profile_data: + logger.error(f"Facebook API error: {profile_data['error']}") + return {} + + return { + "id": str(profile_data.get("id", "")), + "email": profile_data.get("email"), # Может быть None если не предоставлен + "name": profile_data.get("name", ""), + "picture": profile_data.get("picture", {}).get("data", {}).get("url"), + } + except Exception as e: + logger.error(f"Error fetching Facebook profile: {e}") + return {} async def _fetch_x_profile(client: Any, token: Any) -> dict: """Получает профиль из X (Twitter) API""" - profile = await client.get("authors/me?user.fields=id,name,username,profile_image_url", token=token) - profile_data = profile.json() - return PROVIDER_HANDLERS["x"](token, profile_data) + try: + # Используем правильный endpoint для X API v2 + profile = await client.get("users/me?user.fields=id,name,username,profile_image_url", token=token) + profile_data = profile.json() + + # Проверяем наличие ошибок в ответе X + if "errors" in profile_data: + logger.error(f"X API error: {profile_data['errors']}") + return {} + + return PROVIDER_HANDLERS["x"](token, profile_data) + except Exception as e: + logger.error(f"Error fetching X profile: {e}") + return {} async def _fetch_vk_profile(client: Any, token: Any) -> dict: """Получает профиль из VK API""" - profile = await client.get("authors.get?fields=photo_400_orig,contacts&v=5.131", token=token) - profile_data = profile.json() - if profile_data.get("response"): - user_data = profile_data["response"][0] - return { - "id": str(user_data["id"]), - "email": user_data.get("contacts", {}).get("email"), - "name": f"{user_data.get('first_name', '')} {user_data.get('last_name', '')}".strip(), - "picture": user_data.get("photo_400_orig"), - } - return {} + try: + # Используем актуальную версию API v5.199+ + profile = await client.get("users.get?fields=photo_400_orig,contacts&v=5.199", token=token) + profile_data = profile.json() + + # Проверяем наличие ошибок в ответе VK + if "error" in profile_data: + logger.error(f"VK API error: {profile_data['error']}") + return {} + + if profile_data.get("response"): + user_data = profile_data["response"][0] + return { + "id": str(user_data["id"]), + "email": user_data.get("contacts", {}).get("email"), + "name": f"{user_data.get('first_name', '')} {user_data.get('last_name', '')}".strip(), + "picture": user_data.get("photo_400_orig"), + } + return {} + except Exception as e: + logger.error(f"Error fetching VK profile: {e}") + return {} async def _fetch_yandex_profile(client: Any, token: Any) -> dict: diff --git a/docs/auth/oauth.md b/docs/auth/oauth.md index 428631e3..608a526b 100644 --- a/docs/auth/oauth.md +++ b/docs/auth/oauth.md @@ -151,13 +151,16 @@ async def get_oauth_state(state: str) -> Optional[dict]: GOOGLE_OAUTH_CONFIG = { "client_id": os.getenv("GOOGLE_CLIENT_ID"), "client_secret": os.getenv("GOOGLE_CLIENT_SECRET"), - "auth_url": "https://accounts.google.com/o/oauth2/v2/auth", - "token_url": "https://oauth2.googleapis.com/token", - "user_info_url": "https://www.googleapis.com/oauth2/v2/userinfo", + "server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration", "scope": "openid email profile" } ``` +**✅ Преимущества OpenID Connect:** +- Автоматическое обнаружение endpoints через `.well-known/openid-configuration` +- Поддержка актуальных стандартов безопасности +- Автоматические обновления при изменениях Google API + ### GitHub OAuth ```python GITHUB_OAUTH_CONFIG = { @@ -170,6 +173,11 @@ GITHUB_OAUTH_CONFIG = { } ``` +**⚠️ Важные требования GitHub:** +- Scope `user:email` **обязателен** для получения email адреса +- Проверяйте rate limits (5000 запросов/час для авторизованных пользователей) +- Используйте `User-Agent` header во всех запросах к API + ### Facebook OAuth ```python FACEBOOK_OAUTH_CONFIG = { @@ -178,10 +186,17 @@ FACEBOOK_OAUTH_CONFIG = { "auth_url": "https://www.facebook.com/v18.0/dialog/oauth", "token_url": "https://graph.facebook.com/v18.0/oauth/access_token", "user_info_url": "https://graph.facebook.com/v18.0/me", - "scope": "email public_profile" + "scope": "email public_profile", + "token_endpoint_auth_method": "client_secret_post" # Требование Facebook } ``` +**⚠️ Важные требования Facebook:** +- Используйте **минимум API v18.0** +- Обязательно настройте **точные Redirect URIs** в Facebook App +- Приложение должно быть в режиме **"Live"** для работы с реальными пользователями +- **HTTPS обязателен** для production окружения + ### VK OAuth ```python VK_OAUTH_CONFIG = { @@ -190,7 +205,16 @@ VK_OAUTH_CONFIG = { "auth_url": "https://oauth.vk.com/authorize", "token_url": "https://oauth.vk.com/access_token", "user_info_url": "https://api.vk.com/method/users.get", - "scope": "email" + "scope": "email", + "api_version": "5.199" # Актуальная версия API +} +``` + +**⚠️ Важные требования VK:** +- Используйте **API версию 5.199+** (5.131 устарела) +- Scope `email` необходим для получения email адреса +- Redirect URI должен **точно совпадать** с настройками в приложении VK +- Поддерживаются только HTTPS redirect URI в production } ``` diff --git a/settings.py b/settings.py index 812d9fd0..ef0224df 100644 --- a/settings.py +++ b/settings.py @@ -82,7 +82,8 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS = int(environ.get("JWT_REFRESH_TOKEN_EXPIRE_DAYS", # Настройки для HTTP cookies (используется в auth middleware) SESSION_COOKIE_NAME = "session_token" -SESSION_COOKIE_SECURE = True # Включаем для HTTPS +# 🔒 Автоматически определяем HTTPS на основе окружения +SESSION_COOKIE_SECURE = os.getenv("HTTPS_ENABLED", "true").lower() in ["true", "1", "yes"] SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE: Literal["lax", "strict", "none"] = "lax" SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 дней