### 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 регистрации провайдеров
This commit is contained in:
15
CHANGELOG.md
15
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
|
||||
|
||||
148
auth/oauth.py
148
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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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 дней
|
||||
|
||||
Reference in New Issue
Block a user