### 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
|
# 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
|
## [0.9.21] - 2025-09-21
|
||||||
|
|
||||||
### 📚 Documentation Updates
|
### 📚 Documentation Updates
|
||||||
|
|||||||
148
auth/oauth.py
148
auth/oauth.py
@@ -78,16 +78,23 @@ OAUTH_STATE_TTL = 600 # 10 минут
|
|||||||
PROVIDER_CONFIGS = {
|
PROVIDER_CONFIGS = {
|
||||||
"google": {
|
"google": {
|
||||||
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
||||||
|
"client_kwargs": {
|
||||||
|
"scope": "openid email profile",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"github": {
|
"github": {
|
||||||
"access_token_url": "https://github.com/login/oauth/access_token",
|
"access_token_url": "https://github.com/login/oauth/access_token",
|
||||||
"authorize_url": "https://github.com/login/oauth/authorize",
|
"authorize_url": "https://github.com/login/oauth/authorize",
|
||||||
"api_base_url": "https://api.github.com/",
|
"api_base_url": "https://api.github.com/",
|
||||||
|
"client_kwargs": {
|
||||||
|
"scope": "read:user user:email",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"facebook": {
|
"facebook": {
|
||||||
"access_token_url": "https://graph.facebook.com/v13.0/oauth/access_token",
|
"access_token_url": "https://graph.facebook.com/v18.0/oauth/access_token",
|
||||||
"authorize_url": "https://www.facebook.com/v13.0/dialog/oauth",
|
"authorize_url": "https://www.facebook.com/v18.0/dialog/oauth",
|
||||||
"api_base_url": "https://graph.facebook.com/",
|
"api_base_url": "https://graph.facebook.com/",
|
||||||
|
"scope": "email public_profile", # Явно указываем необходимые scope
|
||||||
},
|
},
|
||||||
"x": {
|
"x": {
|
||||||
"access_token_url": "https://api.twitter.com/2/oauth2/token",
|
"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",
|
"access_token_url": "https://oauth.vk.com/access_token",
|
||||||
"authorize_url": "https://oauth.vk.com/authorize",
|
"authorize_url": "https://oauth.vk.com/authorize",
|
||||||
"api_base_url": "https://api.vk.com/method/",
|
"api_base_url": "https://api.vk.com/method/",
|
||||||
|
"client_kwargs": {
|
||||||
|
"scope": "email", # Минимальный scope для получения email
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"yandex": {
|
"yandex": {
|
||||||
"access_token_url": "https://oauth.yandex.ru/token",
|
"access_token_url": "https://oauth.yandex.ru/token",
|
||||||
@@ -128,13 +138,27 @@ def _register_oauth_provider(provider: str, client_config: dict) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Базовые параметры для всех провайдеров
|
# Базовые параметры для всех провайдеров
|
||||||
register_params = {
|
register_params: dict[str, Any] = {
|
||||||
"name": provider,
|
"name": provider,
|
||||||
"client_id": client_config["id"],
|
"client_id": client_config["id"],
|
||||||
"client_secret": client_config["key"],
|
"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)
|
oauth.register(**register_params)
|
||||||
logger.info(f"OAuth provider {provider} registered successfully")
|
logger.info(f"OAuth provider {provider} registered successfully")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -174,51 +198,101 @@ PROVIDER_HANDLERS = {
|
|||||||
|
|
||||||
async def _fetch_github_profile(client: Any, token: Any) -> dict:
|
async def _fetch_github_profile(client: Any, token: Any) -> dict:
|
||||||
"""Получает профиль из GitHub API"""
|
"""Получает профиль из GitHub API"""
|
||||||
profile = await client.get("user", token=token)
|
try:
|
||||||
profile_data = profile.json()
|
# Получаем основной профиль
|
||||||
emails = await client.get("user/emails", token=token)
|
profile = await client.get("user", token=token)
|
||||||
emails_data = emails.json()
|
profile_data = profile.json()
|
||||||
primary_email = next((email["email"] for email in emails_data if email["primary"]), None)
|
|
||||||
return {
|
# Проверяем наличие ошибок в ответе GitHub
|
||||||
"id": str(profile_data["id"]),
|
if "message" in profile_data:
|
||||||
"email": primary_email or profile_data.get("email"),
|
logger.error(f"GitHub API error: {profile_data['message']}")
|
||||||
"name": profile_data.get("name") or profile_data.get("login"),
|
return {}
|
||||||
"picture": profile_data.get("avatar_url"),
|
|
||||||
}
|
# Получаем 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:
|
async def _fetch_facebook_profile(client: Any, token: Any) -> dict:
|
||||||
"""Получает профиль из Facebook API"""
|
"""Получает профиль из Facebook API"""
|
||||||
profile = await client.get("me?fields=id,name,email,picture.width(600)", token=token)
|
try:
|
||||||
profile_data = profile.json()
|
# Используем актуальную версию API v18.0+ и расширенные поля
|
||||||
return {
|
profile = await client.get("me?fields=id,name,email,picture.width(600).height(600)", token=token)
|
||||||
"id": profile_data["id"],
|
profile_data = profile.json()
|
||||||
"email": profile_data.get("email"),
|
|
||||||
"name": profile_data.get("name"),
|
# Проверяем наличие ошибок в ответе Facebook
|
||||||
"picture": profile_data.get("picture", {}).get("data", {}).get("url"),
|
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:
|
async def _fetch_x_profile(client: Any, token: Any) -> dict:
|
||||||
"""Получает профиль из X (Twitter) API"""
|
"""Получает профиль из X (Twitter) API"""
|
||||||
profile = await client.get("authors/me?user.fields=id,name,username,profile_image_url", token=token)
|
try:
|
||||||
profile_data = profile.json()
|
# Используем правильный endpoint для X API v2
|
||||||
return PROVIDER_HANDLERS["x"](token, profile_data)
|
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:
|
async def _fetch_vk_profile(client: Any, token: Any) -> dict:
|
||||||
"""Получает профиль из VK API"""
|
"""Получает профиль из VK API"""
|
||||||
profile = await client.get("authors.get?fields=photo_400_orig,contacts&v=5.131", token=token)
|
try:
|
||||||
profile_data = profile.json()
|
# Используем актуальную версию API v5.199+
|
||||||
if profile_data.get("response"):
|
profile = await client.get("users.get?fields=photo_400_orig,contacts&v=5.199", token=token)
|
||||||
user_data = profile_data["response"][0]
|
profile_data = profile.json()
|
||||||
return {
|
|
||||||
"id": str(user_data["id"]),
|
# Проверяем наличие ошибок в ответе VK
|
||||||
"email": user_data.get("contacts", {}).get("email"),
|
if "error" in profile_data:
|
||||||
"name": f"{user_data.get('first_name', '')} {user_data.get('last_name', '')}".strip(),
|
logger.error(f"VK API error: {profile_data['error']}")
|
||||||
"picture": user_data.get("photo_400_orig"),
|
return {}
|
||||||
}
|
|
||||||
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:
|
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 = {
|
GOOGLE_OAUTH_CONFIG = {
|
||||||
"client_id": os.getenv("GOOGLE_CLIENT_ID"),
|
"client_id": os.getenv("GOOGLE_CLIENT_ID"),
|
||||||
"client_secret": os.getenv("GOOGLE_CLIENT_SECRET"),
|
"client_secret": os.getenv("GOOGLE_CLIENT_SECRET"),
|
||||||
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
||||||
"token_url": "https://oauth2.googleapis.com/token",
|
|
||||||
"user_info_url": "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
||||||
"scope": "openid email profile"
|
"scope": "openid email profile"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**✅ Преимущества OpenID Connect:**
|
||||||
|
- Автоматическое обнаружение endpoints через `.well-known/openid-configuration`
|
||||||
|
- Поддержка актуальных стандартов безопасности
|
||||||
|
- Автоматические обновления при изменениях Google API
|
||||||
|
|
||||||
### GitHub OAuth
|
### GitHub OAuth
|
||||||
```python
|
```python
|
||||||
GITHUB_OAUTH_CONFIG = {
|
GITHUB_OAUTH_CONFIG = {
|
||||||
@@ -170,6 +173,11 @@ GITHUB_OAUTH_CONFIG = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**⚠️ Важные требования GitHub:**
|
||||||
|
- Scope `user:email` **обязателен** для получения email адреса
|
||||||
|
- Проверяйте rate limits (5000 запросов/час для авторизованных пользователей)
|
||||||
|
- Используйте `User-Agent` header во всех запросах к API
|
||||||
|
|
||||||
### Facebook OAuth
|
### Facebook OAuth
|
||||||
```python
|
```python
|
||||||
FACEBOOK_OAUTH_CONFIG = {
|
FACEBOOK_OAUTH_CONFIG = {
|
||||||
@@ -178,10 +186,17 @@ FACEBOOK_OAUTH_CONFIG = {
|
|||||||
"auth_url": "https://www.facebook.com/v18.0/dialog/oauth",
|
"auth_url": "https://www.facebook.com/v18.0/dialog/oauth",
|
||||||
"token_url": "https://graph.facebook.com/v18.0/oauth/access_token",
|
"token_url": "https://graph.facebook.com/v18.0/oauth/access_token",
|
||||||
"user_info_url": "https://graph.facebook.com/v18.0/me",
|
"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
|
### VK OAuth
|
||||||
```python
|
```python
|
||||||
VK_OAUTH_CONFIG = {
|
VK_OAUTH_CONFIG = {
|
||||||
@@ -190,7 +205,16 @@ VK_OAUTH_CONFIG = {
|
|||||||
"auth_url": "https://oauth.vk.com/authorize",
|
"auth_url": "https://oauth.vk.com/authorize",
|
||||||
"token_url": "https://oauth.vk.com/access_token",
|
"token_url": "https://oauth.vk.com/access_token",
|
||||||
"user_info_url": "https://api.vk.com/method/users.get",
|
"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)
|
# Настройки для HTTP cookies (используется в auth middleware)
|
||||||
SESSION_COOKIE_NAME = "session_token"
|
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_HTTPONLY = True
|
||||||
SESSION_COOKIE_SAMESITE: Literal["lax", "strict", "none"] = "lax"
|
SESSION_COOKIE_SAMESITE: Literal["lax", "strict", "none"] = "lax"
|
||||||
SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 дней
|
SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 дней
|
||||||
|
|||||||
Reference in New Issue
Block a user