📚 Documentation Updates
All checks were successful
Deploy on push / deploy (push) Successful in 5m47s
All checks were successful
Deploy on push / deploy (push) Successful in 5m47s
- **🔍 Comprehensive authentication documentation refactoring**: Полная переработка документации аутентификации
- Обновлена таблица содержания в README.md
- Исправлены архитектурные диаграммы - токены хранятся только в Redis
- Добавлены практические примеры кода для микросервисов
- Консолидирована OAuth документация
This commit is contained in:
105
docs/auth/README.md
Normal file
105
docs/auth/README.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# 🔐 Система аутентификации Discours Core
|
||||
|
||||
## 📚 Обзор
|
||||
|
||||
Модульная система аутентификации с JWT токенами, Redis-сессиями, OAuth интеграцией и RBAC авторизацией. Поддерживает httpOnly cookies и Bearer токены для веб и API клиентов.
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Для микросервисов
|
||||
|
||||
```python
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
from auth.utils import extract_token_from_request
|
||||
|
||||
# Проверка токена
|
||||
sessions = SessionTokenManager()
|
||||
token = await extract_token_from_request(request)
|
||||
payload = await sessions.verify_session(token)
|
||||
|
||||
if payload:
|
||||
user_id = payload.get("user_id")
|
||||
print(f"Пользователь авторизован: {user_id}")
|
||||
```
|
||||
|
||||
### Redis ключи для поиска
|
||||
|
||||
```bash
|
||||
# Сессии пользователей
|
||||
session:{user_id}:{token} # Данные сессии (hash)
|
||||
user_sessions:{user_id} # Список активных токенов (set)
|
||||
|
||||
# OAuth токены
|
||||
oauth_access:{user_id}:{provider} # Access токен
|
||||
oauth_refresh:{user_id}:{provider} # Refresh токен
|
||||
```
|
||||
|
||||
## 📖 Документация
|
||||
|
||||
### 🏗️ Архитектура
|
||||
- **[Обзор системы](system.md)** - Компоненты и менеджеры токенов
|
||||
- **[Архитектура](architecture.md)** - Диаграммы и потоки данных
|
||||
- **[Миграция](migration.md)** - Обновление с предыдущих версий
|
||||
|
||||
### 🔑 Аутентификация
|
||||
- **[Управление сессиями](sessions.md)** - JWT токены и Redis хранение
|
||||
- **[OAuth интеграция](oauth.md)** - Социальные провайдеры
|
||||
- **[Микросервисы](microservices.md)** - 🎯 **Интеграция с другими сервисами**
|
||||
|
||||
### 🛠️ Разработка
|
||||
- **[API Reference](api.md)** - Методы и примеры кода
|
||||
- **[Безопасность](security.md)** - Лучшие практики
|
||||
- **[Тестирование](testing.md)** - Unit и E2E тесты
|
||||
|
||||
### 🔗 Связанные системы
|
||||
- **[RBAC System](../rbac-system.md)** - Система ролей и разрешений
|
||||
- **[Security System](../security.md)** - Управление паролями и email
|
||||
- **[Redis Schema](../redis-schema.md)** - Схема данных и кеширование
|
||||
|
||||
## 🔍 Для микросервисов
|
||||
|
||||
### Подключение к Redis
|
||||
|
||||
```python
|
||||
# Используйте тот же Redis connection pool
|
||||
from storage.redis import redis
|
||||
|
||||
# Проверка сессии
|
||||
async def check_user_session(token: str) -> dict | None:
|
||||
sessions = SessionTokenManager()
|
||||
return await sessions.verify_session(token)
|
||||
|
||||
# Массовая проверка токенов
|
||||
from auth.tokens.batch import BatchTokenOperations
|
||||
batch = BatchTokenOperations()
|
||||
results = await batch.batch_validate_tokens(token_list)
|
||||
```
|
||||
|
||||
### HTTP заголовки
|
||||
|
||||
```python
|
||||
# Извлечение токена из запроса
|
||||
from auth.utils import extract_token_from_request, get_safe_headers
|
||||
|
||||
token = await extract_token_from_request(request)
|
||||
|
||||
# Или вручную
|
||||
headers = get_safe_headers(request)
|
||||
token = headers.get("authorization", "").replace("Bearer ", "")
|
||||
```
|
||||
|
||||
## 🎯 Основные компоненты
|
||||
|
||||
- **SessionTokenManager** - JWT сессии с Redis хранением
|
||||
- **OAuthTokenManager** - OAuth access/refresh токены
|
||||
- **BatchTokenOperations** - Массовые операции с токенами
|
||||
- **TokenMonitoring** - Мониторинг и статистика
|
||||
- **AuthMiddleware** - HTTP middleware для автоматической обработки
|
||||
|
||||
## ⚡ Производительность
|
||||
|
||||
- **Connection pooling** для Redis
|
||||
- **Batch операции** для массовых действий (100-1000 токенов)
|
||||
- **Pipeline использование** для атомарности
|
||||
- **SCAN** вместо KEYS для безопасности
|
||||
- **TTL** автоматическая очистка истекших токенов
|
||||
657
docs/auth/api.md
Normal file
657
docs/auth/api.md
Normal file
@@ -0,0 +1,657 @@
|
||||
# 🔧 Auth API Reference
|
||||
|
||||
## 🎯 Обзор
|
||||
|
||||
Полный справочник по API системы аутентификации с примерами кода и использования.
|
||||
|
||||
## 📚 Token Managers
|
||||
|
||||
### SessionTokenManager
|
||||
|
||||
```python
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
|
||||
sessions = SessionTokenManager()
|
||||
```
|
||||
|
||||
#### Методы
|
||||
|
||||
##### `create_session(user_id, auth_data=None, username=None, device_info=None)`
|
||||
Создает новую сессию для пользователя.
|
||||
|
||||
**Параметры:**
|
||||
- `user_id` (str): ID пользователя
|
||||
- `auth_data` (dict, optional): Данные аутентификации
|
||||
- `username` (str, optional): Имя пользователя
|
||||
- `device_info` (dict, optional): Информация об устройстве
|
||||
|
||||
**Возвращает:** `str` - JWT токен
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
token = await sessions.create_session(
|
||||
user_id="123",
|
||||
username="john_doe",
|
||||
device_info={"ip": "192.168.1.1", "user_agent": "Mozilla/5.0"}
|
||||
)
|
||||
```
|
||||
|
||||
##### `verify_session(token)`
|
||||
Проверяет валидность JWT токена и Redis сессии.
|
||||
|
||||
**Параметры:**
|
||||
- `token` (str): JWT токен
|
||||
|
||||
**Возвращает:** `dict | None` - Payload токена или None
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
payload = await sessions.verify_session(token)
|
||||
if payload:
|
||||
user_id = payload.get("user_id")
|
||||
username = payload.get("username")
|
||||
```
|
||||
|
||||
##### `validate_session_token(token)`
|
||||
Валидирует токен сессии с дополнительными проверками.
|
||||
|
||||
**Параметры:**
|
||||
- `token` (str): JWT токен
|
||||
|
||||
**Возвращает:** `tuple[bool, dict]` - (валидность, данные)
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
valid, data = await sessions.validate_session_token(token)
|
||||
if valid:
|
||||
print(f"Session valid for user: {data.get('user_id')}")
|
||||
```
|
||||
|
||||
##### `get_session_data(token, user_id)`
|
||||
Получает данные сессии из Redis.
|
||||
|
||||
**Параметры:**
|
||||
- `token` (str): JWT токен
|
||||
- `user_id` (str): ID пользователя
|
||||
|
||||
**Возвращает:** `dict | None` - Данные сессии
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
session_data = await sessions.get_session_data(token, user_id)
|
||||
if session_data:
|
||||
last_activity = session_data.get("last_activity")
|
||||
```
|
||||
|
||||
##### `refresh_session(user_id, old_token, device_info=None)`
|
||||
Обновляет сессию пользователя.
|
||||
|
||||
**Параметры:**
|
||||
- `user_id` (str): ID пользователя
|
||||
- `old_token` (str): Старый JWT токен
|
||||
- `device_info` (dict, optional): Информация об устройстве
|
||||
|
||||
**Возвращает:** `str` - Новый JWT токен
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
new_token = await sessions.refresh_session(
|
||||
user_id="123",
|
||||
old_token=old_token,
|
||||
device_info={"ip": "192.168.1.1"}
|
||||
)
|
||||
```
|
||||
|
||||
##### `revoke_session_token(token)`
|
||||
Отзывает конкретный токен сессии.
|
||||
|
||||
**Параметры:**
|
||||
- `token` (str): JWT токен
|
||||
|
||||
**Возвращает:** `bool` - Успешность операции
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
revoked = await sessions.revoke_session_token(token)
|
||||
if revoked:
|
||||
print("Session revoked successfully")
|
||||
```
|
||||
|
||||
##### `get_user_sessions(user_id)`
|
||||
Получает все активные сессии пользователя.
|
||||
|
||||
**Параметры:**
|
||||
- `user_id` (str): ID пользователя
|
||||
|
||||
**Возвращает:** `list[dict]` - Список сессий
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
user_sessions = await sessions.get_user_sessions("123")
|
||||
for session in user_sessions:
|
||||
print(f"Token: {session['token'][:20]}...")
|
||||
print(f"Last activity: {session['last_activity']}")
|
||||
```
|
||||
|
||||
##### `revoke_user_sessions(user_id)`
|
||||
Отзывает все сессии пользователя.
|
||||
|
||||
**Параметры:**
|
||||
- `user_id` (str): ID пользователя
|
||||
|
||||
**Возвращает:** `int` - Количество отозванных сессий
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
revoked_count = await sessions.revoke_user_sessions("123")
|
||||
print(f"Revoked {revoked_count} sessions")
|
||||
```
|
||||
|
||||
### OAuthTokenManager
|
||||
|
||||
```python
|
||||
from auth.tokens.oauth import OAuthTokenManager
|
||||
|
||||
oauth = OAuthTokenManager()
|
||||
```
|
||||
|
||||
#### Методы
|
||||
|
||||
##### `store_oauth_tokens(user_id, provider, access_token, refresh_token=None, expires_in=3600, additional_data=None)`
|
||||
Сохраняет OAuth токены в Redis.
|
||||
|
||||
**Параметры:**
|
||||
- `user_id` (str): ID пользователя
|
||||
- `provider` (str): OAuth провайдер (google, github, etc.)
|
||||
- `access_token` (str): Access токен
|
||||
- `refresh_token` (str, optional): Refresh токен
|
||||
- `expires_in` (int): Время жизни в секундах
|
||||
- `additional_data` (dict, optional): Дополнительные данные
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
await oauth.store_oauth_tokens(
|
||||
user_id="123",
|
||||
provider="google",
|
||||
access_token="ya29.a0AfH6SM...",
|
||||
refresh_token="1//04...",
|
||||
expires_in=3600,
|
||||
additional_data={"scope": "read write"}
|
||||
)
|
||||
```
|
||||
|
||||
##### `get_token(user_id, provider, token_type)`
|
||||
Получает OAuth токен.
|
||||
|
||||
**Параметры:**
|
||||
- `user_id` (str): ID пользователя
|
||||
- `provider` (str): OAuth провайдер
|
||||
- `token_type` (str): Тип токена ("oauth_access" или "oauth_refresh")
|
||||
|
||||
**Возвращает:** `dict | None` - Данные токена
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
access_data = await oauth.get_token("123", "google", "oauth_access")
|
||||
if access_data:
|
||||
token = access_data["token"]
|
||||
expires_in = access_data.get("expires_in")
|
||||
```
|
||||
|
||||
##### `revoke_oauth_tokens(user_id, provider)`
|
||||
Отзывает OAuth токены провайдера.
|
||||
|
||||
**Параметры:**
|
||||
- `user_id` (str): ID пользователя
|
||||
- `provider` (str): OAuth провайдер
|
||||
|
||||
**Возвращает:** `bool` - Успешность операции
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
revoked = await oauth.revoke_oauth_tokens("123", "google")
|
||||
if revoked:
|
||||
print("OAuth tokens revoked")
|
||||
```
|
||||
|
||||
### BatchTokenOperations
|
||||
|
||||
```python
|
||||
from auth.tokens.batch import BatchTokenOperations
|
||||
|
||||
batch = BatchTokenOperations()
|
||||
```
|
||||
|
||||
#### Методы
|
||||
|
||||
##### `batch_validate_tokens(tokens)`
|
||||
Массовая валидация токенов.
|
||||
|
||||
**Параметры:**
|
||||
- `tokens` (list[str]): Список JWT токенов
|
||||
|
||||
**Возвращает:** `dict[str, bool]` - Результаты валидации
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
tokens = ["token1", "token2", "token3"]
|
||||
results = await batch.batch_validate_tokens(tokens)
|
||||
# {"token1": True, "token2": False, "token3": True}
|
||||
|
||||
for token, is_valid in results.items():
|
||||
print(f"Token {token[:10]}... is {'valid' if is_valid else 'invalid'}")
|
||||
```
|
||||
|
||||
##### `batch_revoke_tokens(tokens)`
|
||||
Массовый отзыв токенов.
|
||||
|
||||
**Параметры:**
|
||||
- `tokens` (list[str]): Список JWT токенов
|
||||
|
||||
**Возвращает:** `int` - Количество отозванных токенов
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
revoked_count = await batch.batch_revoke_tokens(tokens)
|
||||
print(f"Revoked {revoked_count} tokens")
|
||||
```
|
||||
|
||||
##### `cleanup_expired_tokens()`
|
||||
Очистка истекших токенов.
|
||||
|
||||
**Возвращает:** `int` - Количество очищенных токенов
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
cleaned_count = await batch.cleanup_expired_tokens()
|
||||
print(f"Cleaned {cleaned_count} expired tokens")
|
||||
```
|
||||
|
||||
### TokenMonitoring
|
||||
|
||||
```python
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
|
||||
monitoring = TokenMonitoring()
|
||||
```
|
||||
|
||||
#### Методы
|
||||
|
||||
##### `get_token_statistics()`
|
||||
Получает статистику токенов.
|
||||
|
||||
**Возвращает:** `dict` - Статистика системы
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
stats = await monitoring.get_token_statistics()
|
||||
print(f"Active sessions: {stats['session_tokens']}")
|
||||
print(f"OAuth tokens: {stats['oauth_access_tokens']}")
|
||||
print(f"Memory usage: {stats['memory_usage'] / 1024 / 1024:.2f} MB")
|
||||
```
|
||||
|
||||
##### `health_check()`
|
||||
Проверка здоровья системы токенов.
|
||||
|
||||
**Возвращает:** `dict` - Статус системы
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
health = await monitoring.health_check()
|
||||
if health["status"] == "healthy":
|
||||
print("Token system is healthy")
|
||||
print(f"Redis connected: {health['redis_connected']}")
|
||||
else:
|
||||
print(f"System unhealthy: {health.get('error')}")
|
||||
```
|
||||
|
||||
##### `optimize_memory_usage()`
|
||||
Оптимизация использования памяти.
|
||||
|
||||
**Возвращает:** `dict` - Результаты оптимизации
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
results = await monitoring.optimize_memory_usage()
|
||||
print(f"Cleaned expired: {results['cleaned_expired']}")
|
||||
print(f"Memory freed: {results['memory_freed']} bytes")
|
||||
```
|
||||
|
||||
## 🛠️ Utility Functions
|
||||
|
||||
### Auth Utils
|
||||
|
||||
```python
|
||||
from auth.utils import (
|
||||
extract_token_from_request,
|
||||
get_auth_token,
|
||||
get_auth_token_from_context,
|
||||
get_safe_headers,
|
||||
get_user_data_by_token
|
||||
)
|
||||
```
|
||||
|
||||
#### `extract_token_from_request(request)`
|
||||
Извлекает токен из HTTP запроса.
|
||||
|
||||
**Параметры:**
|
||||
- `request`: HTTP запрос (FastAPI, Starlette, etc.)
|
||||
|
||||
**Возвращает:** `str | None` - JWT токен или None
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
token = await extract_token_from_request(request)
|
||||
if token:
|
||||
print(f"Found token: {token[:20]}...")
|
||||
```
|
||||
|
||||
#### `get_auth_token(request)`
|
||||
Расширенное извлечение токена с логированием.
|
||||
|
||||
**Параметры:**
|
||||
- `request`: HTTP запрос
|
||||
|
||||
**Возвращает:** `str | None` - JWT токен или None
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
token = await get_auth_token(request)
|
||||
if token:
|
||||
# Токен найден и залогирован
|
||||
pass
|
||||
```
|
||||
|
||||
#### `get_auth_token_from_context(info)`
|
||||
Извлечение токена из GraphQL контекста.
|
||||
|
||||
**Параметры:**
|
||||
- `info`: GraphQL Info объект
|
||||
|
||||
**Возвращает:** `str | None` - JWT токен или None
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
@auth_required
|
||||
async def protected_resolver(info, **kwargs):
|
||||
token = await get_auth_token_from_context(info)
|
||||
# Используем токен для дополнительных проверок
|
||||
```
|
||||
|
||||
#### `get_safe_headers(request)`
|
||||
Безопасное получение заголовков запроса.
|
||||
|
||||
**Параметры:**
|
||||
- `request`: HTTP запрос
|
||||
|
||||
**Возвращает:** `dict[str, str]` - Словарь заголовков
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
headers = get_safe_headers(request)
|
||||
auth_header = headers.get("authorization", "")
|
||||
user_agent = headers.get("user-agent", "")
|
||||
```
|
||||
|
||||
#### `get_user_data_by_token(token)`
|
||||
Получение данных пользователя по токену.
|
||||
|
||||
**Параметры:**
|
||||
- `token` (str): JWT токен
|
||||
|
||||
**Возвращает:** `dict | None` - Данные пользователя
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
user_data = await get_user_data_by_token(token)
|
||||
if user_data:
|
||||
print(f"User: {user_data['username']}")
|
||||
print(f"ID: {user_data['user_id']}")
|
||||
```
|
||||
|
||||
## 🎭 Decorators
|
||||
|
||||
### GraphQL Decorators
|
||||
|
||||
```python
|
||||
from auth.decorators import auth_required, permission_required
|
||||
```
|
||||
|
||||
#### `@auth_required`
|
||||
Требует авторизации для выполнения resolver'а.
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
@auth_required
|
||||
async def get_user_profile(info, **kwargs):
|
||||
"""Получение профиля пользователя"""
|
||||
user = info.context.get('user')
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email
|
||||
}
|
||||
```
|
||||
|
||||
#### `@permission_required(permission)`
|
||||
Требует конкретного разрешения.
|
||||
|
||||
**Параметры:**
|
||||
- `permission` (str): Название разрешения
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
@auth_required
|
||||
@permission_required("shout:create")
|
||||
async def create_shout(info, input_data):
|
||||
"""Создание публикации"""
|
||||
user = info.context.get('user')
|
||||
|
||||
shout = Shout(
|
||||
title=input_data['title'],
|
||||
content=input_data['content'],
|
||||
author_id=user.id
|
||||
)
|
||||
|
||||
return shout
|
||||
```
|
||||
|
||||
## 🔧 Middleware
|
||||
|
||||
### AuthMiddleware
|
||||
|
||||
```python
|
||||
from auth.middleware import AuthMiddleware
|
||||
|
||||
middleware = AuthMiddleware()
|
||||
```
|
||||
|
||||
#### Методы
|
||||
|
||||
##### `authenticate_user(request)`
|
||||
Аутентификация пользователя из запроса.
|
||||
|
||||
**Параметры:**
|
||||
- `request`: HTTP запрос
|
||||
|
||||
**Возвращает:** `dict | None` - Данные пользователя
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
user_data = await middleware.authenticate_user(request)
|
||||
if user_data:
|
||||
request.user = user_data
|
||||
```
|
||||
|
||||
##### `set_cookie(response, token)`
|
||||
Установка httpOnly cookie с токеном.
|
||||
|
||||
**Параметры:**
|
||||
- `response`: HTTP ответ
|
||||
- `token` (str): JWT токен
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
await middleware.set_cookie(response, token)
|
||||
```
|
||||
|
||||
##### `delete_cookie(response)`
|
||||
Удаление cookie с токеном.
|
||||
|
||||
**Параметры:**
|
||||
- `response`: HTTP ответ
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
await middleware.delete_cookie(response)
|
||||
```
|
||||
|
||||
## 🔒 Error Handling
|
||||
|
||||
### Исключения
|
||||
|
||||
```python
|
||||
from auth.exceptions import (
|
||||
AuthenticationError,
|
||||
InvalidTokenError,
|
||||
TokenExpiredError,
|
||||
OAuthError
|
||||
)
|
||||
```
|
||||
|
||||
#### `AuthenticationError`
|
||||
Базовое исключение аутентификации.
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
try:
|
||||
payload = await sessions.verify_session(token)
|
||||
if not payload:
|
||||
raise AuthenticationError("Invalid session token")
|
||||
except AuthenticationError as e:
|
||||
return {"error": str(e), "status": 401}
|
||||
```
|
||||
|
||||
#### `InvalidTokenError`
|
||||
Невалидный токен.
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
try:
|
||||
valid, data = await sessions.validate_session_token(token)
|
||||
if not valid:
|
||||
raise InvalidTokenError("Token validation failed")
|
||||
except InvalidTokenError as e:
|
||||
return {"error": str(e), "status": 401}
|
||||
```
|
||||
|
||||
#### `TokenExpiredError`
|
||||
Истекший токен.
|
||||
|
||||
**Пример:**
|
||||
```python
|
||||
try:
|
||||
# Проверка токена
|
||||
pass
|
||||
except TokenExpiredError as e:
|
||||
return {"error": "Token expired", "status": 401}
|
||||
```
|
||||
|
||||
## 📊 Response Formats
|
||||
|
||||
### Успешные ответы
|
||||
|
||||
```python
|
||||
# Успешная аутентификация
|
||||
{
|
||||
"authenticated": True,
|
||||
"user_id": "123",
|
||||
"username": "john_doe",
|
||||
"expires_at": 1640995200
|
||||
}
|
||||
|
||||
# Статистика токенов
|
||||
{
|
||||
"session_tokens": 150,
|
||||
"oauth_access_tokens": 25,
|
||||
"oauth_refresh_tokens": 25,
|
||||
"verification_tokens": 5,
|
||||
"memory_usage": 1048576
|
||||
}
|
||||
|
||||
# Health check
|
||||
{
|
||||
"status": "healthy",
|
||||
"redis_connected": True,
|
||||
"token_count": 205,
|
||||
"timestamp": 1640995200
|
||||
}
|
||||
```
|
||||
|
||||
### Ошибки
|
||||
|
||||
```python
|
||||
# Ошибка аутентификации
|
||||
{
|
||||
"authenticated": False,
|
||||
"error": "Invalid or expired token",
|
||||
"status": 401
|
||||
}
|
||||
|
||||
# Ошибка системы
|
||||
{
|
||||
"status": "error",
|
||||
"error": "Redis connection failed",
|
||||
"timestamp": 1640995200
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Testing Helpers
|
||||
|
||||
### Mock Utilities
|
||||
|
||||
```python
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
# Mock SessionTokenManager
|
||||
@patch('auth.tokens.sessions.SessionTokenManager')
|
||||
async def test_auth(mock_sessions):
|
||||
mock_sessions.return_value.verify_session.return_value = {
|
||||
"user_id": "123",
|
||||
"username": "testuser"
|
||||
}
|
||||
|
||||
# Ваш тест здесь
|
||||
pass
|
||||
|
||||
# Mock Redis
|
||||
@patch('storage.redis.redis')
|
||||
async def test_redis_operations(mock_redis):
|
||||
mock_redis.get.return_value = b'{"user_id": "123"}'
|
||||
mock_redis.exists.return_value = True
|
||||
|
||||
# Ваш тест здесь
|
||||
pass
|
||||
```
|
||||
|
||||
### Test Fixtures
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_token():
|
||||
"""Фикстура для создания тестового токена"""
|
||||
sessions = SessionTokenManager()
|
||||
return await sessions.create_session(
|
||||
user_id="test_user",
|
||||
username="testuser"
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
async def authenticated_request(auth_token):
|
||||
"""Фикстура для аутентифицированного запроса"""
|
||||
mock_request = AsyncMock()
|
||||
mock_request.headers = {"authorization": f"Bearer {auth_token}"}
|
||||
return mock_request
|
||||
```
|
||||
276
docs/auth/architecture.md
Normal file
276
docs/auth/architecture.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# Архитектура системы авторизации Discours Core
|
||||
|
||||
## 🎯 Обзор архитектуры
|
||||
|
||||
Модульная система авторизации с разделением ответственности между компонентами.
|
||||
|
||||
**Хранение данных:**
|
||||
- **Токены** → Redis (сессии, OAuth, verification)
|
||||
- **Пользователи** → PostgreSQL (основные данные + OAuth в JSON поле)
|
||||
|
||||
## 📊 Схема потоков данных
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Frontend"
|
||||
FE[Web Frontend]
|
||||
MOB[Mobile App]
|
||||
API[API Clients]
|
||||
end
|
||||
|
||||
subgraph "Auth Layer"
|
||||
MW[AuthMiddleware]
|
||||
DEC[GraphQL Decorators]
|
||||
UTILS[Auth Utils]
|
||||
end
|
||||
|
||||
subgraph "Token Managers"
|
||||
STM[SessionTokenManager]
|
||||
VTM[VerificationTokenManager]
|
||||
OTM[OAuthTokenManager]
|
||||
BTM[BatchTokenOperations]
|
||||
MON[TokenMonitoring]
|
||||
end
|
||||
|
||||
subgraph "Storage"
|
||||
REDIS[(Redis)]
|
||||
DB[(PostgreSQL)]
|
||||
end
|
||||
|
||||
subgraph "External OAuth"
|
||||
GOOGLE[Google]
|
||||
GITHUB[GitHub]
|
||||
FACEBOOK[Facebook]
|
||||
VK[VK]
|
||||
YANDEX[Yandex]
|
||||
end
|
||||
|
||||
FE --> MW
|
||||
MOB --> MW
|
||||
API --> MW
|
||||
|
||||
MW --> STM
|
||||
MW --> UTILS
|
||||
|
||||
DEC --> STM
|
||||
UTILS --> STM
|
||||
|
||||
STM --> REDIS
|
||||
VTM --> REDIS
|
||||
OTM --> REDIS
|
||||
BTM --> REDIS
|
||||
MON --> REDIS
|
||||
|
||||
STM --> DB
|
||||
|
||||
OTM --> GOOGLE
|
||||
OTM --> GITHUB
|
||||
OTM --> FACEBOOK
|
||||
OTM --> VK
|
||||
OTM --> YANDEX
|
||||
```
|
||||
|
||||
## 🏗️ Диаграмма компонентов
|
||||
|
||||
**Примечание:** Токены хранятся только в Redis, PostgreSQL используется только для пользовательских данных и OAuth связей.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "HTTP Layer"
|
||||
REQ[HTTP Request]
|
||||
RESP[HTTP Response]
|
||||
end
|
||||
|
||||
subgraph "Middleware Layer"
|
||||
AUTH_MW[AuthMiddleware]
|
||||
UTILS[Auth Utils]
|
||||
end
|
||||
|
||||
subgraph "Token Management"
|
||||
STM[SessionTokenManager]
|
||||
VTM[VerificationTokenManager]
|
||||
OTM[OAuthTokenManager]
|
||||
BTM[BatchTokenOperations]
|
||||
MON[TokenMonitoring]
|
||||
end
|
||||
|
||||
subgraph "Storage"
|
||||
REDIS[(Redis)]
|
||||
DB[(PostgreSQL)]
|
||||
end
|
||||
|
||||
subgraph "External"
|
||||
OAUTH_PROV[OAuth Providers]
|
||||
end
|
||||
|
||||
REQ --> AUTH_MW
|
||||
AUTH_MW --> UTILS
|
||||
UTILS --> STM
|
||||
|
||||
STM --> REDIS
|
||||
VTM --> REDIS
|
||||
OTM --> REDIS
|
||||
BTM --> REDIS
|
||||
MON --> REDIS
|
||||
|
||||
STM --> DB
|
||||
OTM --> OAUTH_PROV
|
||||
|
||||
STM --> RESP
|
||||
VTM --> RESP
|
||||
OTM --> RESP
|
||||
```
|
||||
|
||||
## 🔐 OAuth Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant F as Frontend
|
||||
participant A as Auth Service
|
||||
participant R as Redis
|
||||
participant P as OAuth Provider
|
||||
|
||||
U->>F: Click "Login with Provider"
|
||||
F->>A: GET /oauth/{provider}?state={csrf}
|
||||
A->>R: Store OAuth state (TTL: 10 min)
|
||||
A->>P: Redirect to Provider
|
||||
P->>U: Show authorization page
|
||||
U->>P: Grant permission
|
||||
P->>A: GET /oauth/{provider}/callback?code={code}&state={state}
|
||||
A->>R: Verify state
|
||||
A->>P: Exchange code for token
|
||||
P->>A: Return access token + user data
|
||||
A->>R: Store OAuth tokens
|
||||
A->>A: Generate JWT session token
|
||||
A->>R: Store session in Redis
|
||||
A->>F: Redirect with JWT token
|
||||
F->>U: User logged in
|
||||
```
|
||||
|
||||
## 🔄 Session Management
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Anonymous
|
||||
Anonymous --> Authenticating: Login attempt
|
||||
Authenticating --> Authenticated: Valid JWT + Redis session
|
||||
Authenticating --> Anonymous: Invalid credentials
|
||||
Authenticated --> Refreshing: Token near expiry
|
||||
Refreshing --> Authenticated: Successful refresh
|
||||
Refreshing --> Anonymous: Refresh failed
|
||||
Authenticated --> Anonymous: Logout/Revoke
|
||||
Authenticated --> Anonymous: Token expired
|
||||
```
|
||||
|
||||
## 🗄️ Redis структура данных
|
||||
|
||||
```bash
|
||||
# JWT Sessions
|
||||
session:{user_id}:{token} # Hash: {user_id, username, device_info, last_activity}
|
||||
user_sessions:{user_id} # Set: {token1, token2, ...}
|
||||
|
||||
# Verification Tokens
|
||||
verification_token:{token} # JSON: {user_id, type, data, created_at}
|
||||
|
||||
# OAuth Tokens
|
||||
oauth_access:{user_id}:{provider} # JSON: {token, expires_in, scope}
|
||||
oauth_refresh:{user_id}:{provider} # JSON: {token, provider_data}
|
||||
oauth_state:{state} # JSON: {provider, redirect_uri, code_verifier}
|
||||
|
||||
# Legacy (для совместимости)
|
||||
{user_id}-{username}-{token} # Hash: legacy format
|
||||
```
|
||||
|
||||
### Примеры Redis команд
|
||||
|
||||
```bash
|
||||
# Поиск сессий пользователя
|
||||
redis-cli --scan --pattern "session:123:*"
|
||||
|
||||
# Получение данных сессии
|
||||
redis-cli HGETALL "session:123:your_token_here"
|
||||
|
||||
# Проверка TTL
|
||||
redis-cli TTL "session:123:your_token_here"
|
||||
|
||||
# Поиск OAuth токенов
|
||||
redis-cli --scan --pattern "oauth_access:123:*"
|
||||
```
|
||||
|
||||
## 🔒 Security Components
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Input Validation"
|
||||
EMAIL[Email Format]
|
||||
PASS[Password Strength]
|
||||
TOKEN[JWT Validation]
|
||||
end
|
||||
|
||||
subgraph "Authentication"
|
||||
BCRYPT[bcrypt + SHA256]
|
||||
JWT_SIGN[JWT Signing]
|
||||
OAUTH_VERIFY[OAuth Verification]
|
||||
end
|
||||
|
||||
subgraph "Authorization"
|
||||
RBAC[RBAC System]
|
||||
PERM[Permission Checks]
|
||||
RESOURCE[Resource Access]
|
||||
end
|
||||
|
||||
subgraph "Session Security"
|
||||
TTL[Redis TTL]
|
||||
REVOKE[Token Revocation]
|
||||
REFRESH[Secure Refresh]
|
||||
end
|
||||
|
||||
EMAIL --> BCRYPT
|
||||
PASS --> BCRYPT
|
||||
TOKEN --> JWT_SIGN
|
||||
|
||||
BCRYPT --> RBAC
|
||||
JWT_SIGN --> RBAC
|
||||
OAUTH_VERIFY --> RBAC
|
||||
|
||||
RBAC --> PERM
|
||||
PERM --> RESOURCE
|
||||
|
||||
RESOURCE --> TTL
|
||||
RESOURCE --> REVOKE
|
||||
RESOURCE --> REFRESH
|
||||
```
|
||||
|
||||
## ⚡ Performance & Scaling
|
||||
|
||||
### Горизонтальное масштабирование
|
||||
- **Stateless JWT** токены
|
||||
- **Redis Cluster** для высокой доступности
|
||||
- **Load Balancer** aware session management
|
||||
|
||||
### Оптимизации
|
||||
- **Connection pooling** для Redis
|
||||
- **Batch operations** для массовых операций (100-1000 токенов)
|
||||
- **Pipeline использование** для атомарности
|
||||
- **SCAN** вместо KEYS для безопасности
|
||||
|
||||
### Мониторинг производительности
|
||||
```python
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
|
||||
monitoring = TokenMonitoring()
|
||||
|
||||
# Статистика токенов
|
||||
stats = await monitoring.get_token_statistics()
|
||||
# {
|
||||
# "session_tokens": 150,
|
||||
# "verification_tokens": 5,
|
||||
# "oauth_access_tokens": 25,
|
||||
# "memory_usage": 1048576
|
||||
# }
|
||||
|
||||
# Health check
|
||||
health = await monitoring.health_check()
|
||||
# {"status": "healthy", "redis_connected": True}
|
||||
```
|
||||
546
docs/auth/microservices.md
Normal file
546
docs/auth/microservices.md
Normal file
@@ -0,0 +1,546 @@
|
||||
# 🔍 Аутентификация для микросервисов
|
||||
|
||||
## 🎯 Обзор
|
||||
|
||||
Руководство по интеграции системы аутентификации Discours Core с другими микросервисами через общий Redis connection pool.
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Подключение к Redis
|
||||
|
||||
```python
|
||||
# Используйте тот же Redis connection pool
|
||||
from storage.redis import redis
|
||||
|
||||
# Или создайте свой с теми же настройками
|
||||
import aioredis
|
||||
|
||||
redis_client = aioredis.from_url(
|
||||
"redis://localhost:6379/0",
|
||||
max_connections=20,
|
||||
retry_on_timeout=True,
|
||||
socket_keepalive=True,
|
||||
socket_keepalive_options={},
|
||||
health_check_interval=30
|
||||
)
|
||||
```
|
||||
|
||||
### Проверка токена сессии
|
||||
|
||||
```python
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
from auth.utils import extract_token_from_request
|
||||
|
||||
async def check_user_session(request) -> dict | None:
|
||||
"""Проверка сессии пользователя в микросервисе"""
|
||||
|
||||
# 1. Извлекаем токен из запроса
|
||||
token = await extract_token_from_request(request)
|
||||
if not token:
|
||||
return None
|
||||
|
||||
# 2. Проверяем сессию через SessionTokenManager
|
||||
sessions = SessionTokenManager()
|
||||
payload = await sessions.verify_session(token)
|
||||
|
||||
if payload:
|
||||
return {
|
||||
"authenticated": True,
|
||||
"user_id": payload.get("user_id"),
|
||||
"username": payload.get("username"),
|
||||
"expires_at": payload.get("exp")
|
||||
}
|
||||
|
||||
return {"authenticated": False, "error": "Invalid token"}
|
||||
```
|
||||
|
||||
## 🔑 Redis ключи для поиска
|
||||
|
||||
### Структура данных
|
||||
|
||||
```bash
|
||||
# Сессии пользователей
|
||||
session:{user_id}:{token} # Hash: {user_id, username, device_info, last_activity}
|
||||
user_sessions:{user_id} # Set: {token1, token2, ...}
|
||||
|
||||
# OAuth токены
|
||||
oauth_access:{user_id}:{provider} # JSON: {token, expires_in, scope}
|
||||
oauth_refresh:{user_id}:{provider} # JSON: {token, provider_data}
|
||||
|
||||
# Токены подтверждения
|
||||
verification_token:{token} # JSON: {user_id, type, data, created_at}
|
||||
|
||||
# OAuth состояние
|
||||
oauth_state:{state} # JSON: {provider, redirect_uri, code_verifier}
|
||||
```
|
||||
|
||||
### Примеры поиска
|
||||
|
||||
```python
|
||||
from storage.redis import redis
|
||||
|
||||
# 1. Поиск всех сессий пользователя
|
||||
async def get_user_sessions(user_id: int) -> list[str]:
|
||||
"""Получить все активные токены пользователя"""
|
||||
session_key = f"user_sessions:{user_id}"
|
||||
tokens = await redis.smembers(session_key)
|
||||
return [token.decode() for token in tokens] if tokens else []
|
||||
|
||||
# 2. Получение данных конкретной сессии
|
||||
async def get_session_data(user_id: int, token: str) -> dict | None:
|
||||
"""Получить данные сессии"""
|
||||
session_key = f"session:{user_id}:{token}"
|
||||
data = await redis.hgetall(session_key)
|
||||
|
||||
if data:
|
||||
return {k.decode(): v.decode() for k, v in data.items()}
|
||||
return None
|
||||
|
||||
# 3. Проверка существования токена
|
||||
async def token_exists(user_id: int, token: str) -> bool:
|
||||
"""Проверить существование токена"""
|
||||
session_key = f"session:{user_id}:{token}"
|
||||
return await redis.exists(session_key)
|
||||
|
||||
# 4. Получение TTL токена
|
||||
async def get_token_ttl(user_id: int, token: str) -> int:
|
||||
"""Получить время жизни токена в секундах"""
|
||||
session_key = f"session:{user_id}:{token}"
|
||||
return await redis.ttl(session_key)
|
||||
```
|
||||
|
||||
## 🛠️ Методы интеграции
|
||||
|
||||
### 1. Прямая проверка токена
|
||||
|
||||
```python
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
|
||||
async def authenticate_request(request) -> dict:
|
||||
"""Аутентификация запроса в микросервисе"""
|
||||
|
||||
sessions = SessionTokenManager()
|
||||
|
||||
# Извлекаем токен
|
||||
token = await extract_token_from_request(request)
|
||||
if not token:
|
||||
return {"authenticated": False, "error": "No token provided"}
|
||||
|
||||
try:
|
||||
# Проверяем JWT и Redis сессию
|
||||
payload = await sessions.verify_session(token)
|
||||
|
||||
if payload:
|
||||
user_id = payload.get("user_id")
|
||||
|
||||
# Дополнительно получаем данные сессии из Redis
|
||||
session_data = await sessions.get_session_data(token, user_id)
|
||||
|
||||
return {
|
||||
"authenticated": True,
|
||||
"user_id": user_id,
|
||||
"username": payload.get("username"),
|
||||
"session_data": session_data,
|
||||
"expires_at": payload.get("exp")
|
||||
}
|
||||
else:
|
||||
return {"authenticated": False, "error": "Invalid or expired token"}
|
||||
|
||||
except Exception as e:
|
||||
return {"authenticated": False, "error": f"Authentication error: {str(e)}"}
|
||||
```
|
||||
|
||||
### 2. Массовая проверка токенов
|
||||
|
||||
```python
|
||||
from auth.tokens.batch import BatchTokenOperations
|
||||
|
||||
async def validate_multiple_tokens(tokens: list[str]) -> dict[str, bool]:
|
||||
"""Массовая проверка токенов для API gateway"""
|
||||
|
||||
batch = BatchTokenOperations()
|
||||
return await batch.batch_validate_tokens(tokens)
|
||||
|
||||
# Использование
|
||||
async def api_gateway_auth(request_tokens: list[str]):
|
||||
"""Пример использования в API Gateway"""
|
||||
|
||||
results = await validate_multiple_tokens(request_tokens)
|
||||
|
||||
authenticated_requests = []
|
||||
for token, is_valid in results.items():
|
||||
if is_valid:
|
||||
# Получаем данные пользователя для валидных токенов
|
||||
sessions = SessionTokenManager()
|
||||
payload = await sessions.verify_session(token)
|
||||
if payload:
|
||||
authenticated_requests.append({
|
||||
"token": token,
|
||||
"user_id": payload.get("user_id"),
|
||||
"username": payload.get("username")
|
||||
})
|
||||
|
||||
return authenticated_requests
|
||||
```
|
||||
|
||||
### 3. Получение данных пользователя
|
||||
|
||||
```python
|
||||
from auth.utils import get_user_data_by_token
|
||||
|
||||
async def get_user_info(token: str) -> dict | None:
|
||||
"""Получить информацию о пользователе по токену"""
|
||||
|
||||
try:
|
||||
user_data = await get_user_data_by_token(token)
|
||||
return user_data
|
||||
except Exception as e:
|
||||
print(f"Ошибка получения данных пользователя: {e}")
|
||||
return None
|
||||
|
||||
# Использование
|
||||
async def protected_endpoint(request):
|
||||
"""Пример защищенного endpoint в микросервисе"""
|
||||
|
||||
token = await extract_token_from_request(request)
|
||||
user_info = await get_user_info(token)
|
||||
|
||||
if not user_info:
|
||||
return {"error": "Unauthorized", "status": 401}
|
||||
|
||||
return {
|
||||
"message": f"Hello, {user_info.get('username')}!",
|
||||
"user_id": user_info.get("user_id"),
|
||||
"status": 200
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 HTTP заголовки и извлечение токенов
|
||||
|
||||
### Поддерживаемые форматы
|
||||
|
||||
```python
|
||||
from auth.utils import extract_token_from_request, get_safe_headers
|
||||
|
||||
async def extract_auth_token(request) -> str | None:
|
||||
"""Извлечение токена из различных источников"""
|
||||
|
||||
# 1. Автоматическое извлечение (рекомендуется)
|
||||
token = await extract_token_from_request(request)
|
||||
if token:
|
||||
return token
|
||||
|
||||
# 2. Ручное извлечение из заголовков
|
||||
headers = get_safe_headers(request)
|
||||
|
||||
# Bearer токен в Authorization
|
||||
auth_header = headers.get("authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
return auth_header[7:].strip()
|
||||
|
||||
# Кастомный заголовок X-Session-Token
|
||||
session_token = headers.get("x-session-token")
|
||||
if session_token:
|
||||
return session_token.strip()
|
||||
|
||||
# Cookie (для веб-приложений)
|
||||
if hasattr(request, "cookies"):
|
||||
cookie_token = request.cookies.get("session_token")
|
||||
if cookie_token:
|
||||
return cookie_token
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
### Примеры HTTP запросов
|
||||
|
||||
```bash
|
||||
# 1. Bearer токен в Authorization header
|
||||
curl -H "Authorization: Bearer your_jwt_token_here" \
|
||||
http://localhost:8000/api/protected
|
||||
|
||||
# 2. Кастомный заголовок
|
||||
curl -H "X-Session-Token: your_jwt_token_here" \
|
||||
http://localhost:8000/api/protected
|
||||
|
||||
# 3. Cookie (автоматически для веб-приложений)
|
||||
curl -b "session_token=your_jwt_token_here" \
|
||||
http://localhost:8000/api/protected
|
||||
```
|
||||
|
||||
## 📊 Мониторинг и статистика
|
||||
|
||||
### Health Check
|
||||
|
||||
```python
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
|
||||
async def auth_health_check() -> dict:
|
||||
"""Health check системы аутентификации"""
|
||||
|
||||
monitoring = TokenMonitoring()
|
||||
|
||||
try:
|
||||
# Проверяем состояние системы токенов
|
||||
health = await monitoring.health_check()
|
||||
|
||||
# Получаем статистику
|
||||
stats = await monitoring.get_token_statistics()
|
||||
|
||||
return {
|
||||
"status": health.get("status", "unknown"),
|
||||
"redis_connected": health.get("redis_connected", False),
|
||||
"active_sessions": stats.get("session_tokens", 0),
|
||||
"oauth_tokens": stats.get("oauth_access_tokens", 0) + stats.get("oauth_refresh_tokens", 0),
|
||||
"memory_usage_mb": stats.get("memory_usage", 0) / 1024 / 1024,
|
||||
"timestamp": int(time.time())
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"timestamp": int(time.time())
|
||||
}
|
||||
|
||||
# Использование в endpoint
|
||||
async def health_endpoint():
|
||||
"""Endpoint для мониторинга"""
|
||||
health_data = await auth_health_check()
|
||||
|
||||
if health_data["status"] == "healthy":
|
||||
return {"health": health_data, "status": 200}
|
||||
else:
|
||||
return {"health": health_data, "status": 503}
|
||||
```
|
||||
|
||||
### Статистика использования
|
||||
|
||||
```python
|
||||
async def get_auth_statistics() -> dict:
|
||||
"""Получить статистику использования аутентификации"""
|
||||
|
||||
monitoring = TokenMonitoring()
|
||||
stats = await monitoring.get_token_statistics()
|
||||
|
||||
return {
|
||||
"sessions": {
|
||||
"active": stats.get("session_tokens", 0),
|
||||
"total_memory": stats.get("memory_usage", 0)
|
||||
},
|
||||
"oauth": {
|
||||
"access_tokens": stats.get("oauth_access_tokens", 0),
|
||||
"refresh_tokens": stats.get("oauth_refresh_tokens", 0)
|
||||
},
|
||||
"verification": {
|
||||
"pending": stats.get("verification_tokens", 0)
|
||||
},
|
||||
"redis": {
|
||||
"connected": stats.get("redis_connected", False),
|
||||
"memory_usage_mb": stats.get("memory_usage", 0) / 1024 / 1024
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔒 Безопасность для микросервисов
|
||||
|
||||
### Валидация токенов
|
||||
|
||||
```python
|
||||
async def secure_token_validation(token: str) -> dict:
|
||||
"""Безопасная валидация токена с дополнительными проверками"""
|
||||
|
||||
if not token or len(token) < 10:
|
||||
return {"valid": False, "error": "Invalid token format"}
|
||||
|
||||
try:
|
||||
sessions = SessionTokenManager()
|
||||
|
||||
# 1. Проверяем JWT структуру и подпись
|
||||
payload = await sessions.verify_session(token)
|
||||
if not payload:
|
||||
return {"valid": False, "error": "Invalid JWT token"}
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
if not user_id:
|
||||
return {"valid": False, "error": "Missing user_id in token"}
|
||||
|
||||
# 2. Проверяем существование сессии в Redis
|
||||
session_exists = await redis.exists(f"session:{user_id}:{token}")
|
||||
if not session_exists:
|
||||
return {"valid": False, "error": "Session not found in Redis"}
|
||||
|
||||
# 3. Проверяем TTL
|
||||
ttl = await redis.ttl(f"session:{user_id}:{token}")
|
||||
if ttl <= 0:
|
||||
return {"valid": False, "error": "Session expired"}
|
||||
|
||||
# 4. Обновляем last_activity
|
||||
await redis.hset(f"session:{user_id}:{token}", "last_activity", int(time.time()))
|
||||
|
||||
return {
|
||||
"valid": True,
|
||||
"user_id": user_id,
|
||||
"username": payload.get("username"),
|
||||
"expires_in": ttl,
|
||||
"last_activity": int(time.time())
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"valid": False, "error": f"Validation error: {str(e)}"}
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```python
|
||||
from collections import defaultdict
|
||||
import time
|
||||
|
||||
# Простой in-memory rate limiter (для production используйте Redis)
|
||||
request_counts = defaultdict(list)
|
||||
|
||||
async def rate_limit_check(user_id: str, max_requests: int = 100, window_seconds: int = 60) -> bool:
|
||||
"""Проверка rate limiting для пользователя"""
|
||||
|
||||
current_time = time.time()
|
||||
user_requests = request_counts[user_id]
|
||||
|
||||
# Удаляем старые запросы
|
||||
user_requests[:] = [req_time for req_time in user_requests if current_time - req_time < window_seconds]
|
||||
|
||||
# Проверяем лимит
|
||||
if len(user_requests) >= max_requests:
|
||||
return False
|
||||
|
||||
# Добавляем текущий запрос
|
||||
user_requests.append(current_time)
|
||||
return True
|
||||
|
||||
# Использование в middleware
|
||||
async def auth_with_rate_limiting(request):
|
||||
"""Аутентификация с rate limiting"""
|
||||
|
||||
auth_result = await authenticate_request(request)
|
||||
|
||||
if auth_result["authenticated"]:
|
||||
user_id = str(auth_result["user_id"])
|
||||
|
||||
if not await rate_limit_check(user_id):
|
||||
return {"error": "Rate limit exceeded", "status": 429}
|
||||
|
||||
return auth_result
|
||||
```
|
||||
|
||||
## 🧪 Тестирование интеграции
|
||||
|
||||
### Unit тесты
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_microservice_auth():
|
||||
"""Тест аутентификации в микросервисе"""
|
||||
|
||||
# Mock request с токеном
|
||||
mock_request = AsyncMock()
|
||||
mock_request.headers = {"authorization": "Bearer valid_token"}
|
||||
|
||||
# Mock SessionTokenManager
|
||||
with patch('auth.tokens.sessions.SessionTokenManager') as mock_sessions:
|
||||
mock_sessions.return_value.verify_session.return_value = {
|
||||
"user_id": "123",
|
||||
"username": "testuser",
|
||||
"exp": int(time.time()) + 3600
|
||||
}
|
||||
|
||||
result = await authenticate_request(mock_request)
|
||||
|
||||
assert result["authenticated"] is True
|
||||
assert result["user_id"] == "123"
|
||||
assert result["username"] == "testuser"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_token_validation():
|
||||
"""Тест массовой валидации токенов"""
|
||||
|
||||
tokens = ["token1", "token2", "token3"]
|
||||
|
||||
with patch('auth.tokens.batch.BatchTokenOperations') as mock_batch:
|
||||
mock_batch.return_value.batch_validate_tokens.return_value = {
|
||||
"token1": True,
|
||||
"token2": False,
|
||||
"token3": True
|
||||
}
|
||||
|
||||
results = await validate_multiple_tokens(tokens)
|
||||
|
||||
assert results["token1"] is True
|
||||
assert results["token2"] is False
|
||||
assert results["token3"] is True
|
||||
```
|
||||
|
||||
### Integration тесты
|
||||
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_redis_integration():
|
||||
"""Тест интеграции с Redis"""
|
||||
|
||||
from storage.redis import redis
|
||||
|
||||
# Тестируем подключение
|
||||
ping_result = await redis.ping()
|
||||
assert ping_result is True
|
||||
|
||||
# Тестируем операции с сессиями
|
||||
test_key = "session:test:token123"
|
||||
test_data = {"user_id": "123", "username": "testuser"}
|
||||
|
||||
# Сохраняем данные
|
||||
await redis.hset(test_key, mapping=test_data)
|
||||
await redis.expire(test_key, 3600)
|
||||
|
||||
# Проверяем данные
|
||||
stored_data = await redis.hgetall(test_key)
|
||||
assert stored_data[b"user_id"].decode() == "123"
|
||||
assert stored_data[b"username"].decode() == "testuser"
|
||||
|
||||
# Проверяем TTL
|
||||
ttl = await redis.ttl(test_key)
|
||||
assert ttl > 0
|
||||
|
||||
# Очищаем
|
||||
await redis.delete(test_key)
|
||||
```
|
||||
|
||||
## 📋 Checklist для интеграции
|
||||
|
||||
### Подготовка
|
||||
- [ ] Настроен Redis connection pool с теми же параметрами
|
||||
- [ ] Установлены зависимости: `auth.tokens.*`, `auth.utils`
|
||||
- [ ] Настроены environment variables (JWT_SECRET, REDIS_URL)
|
||||
|
||||
### Реализация
|
||||
- [ ] Реализована функция извлечения токенов из запросов
|
||||
- [ ] Добавлена проверка сессий через SessionTokenManager
|
||||
- [ ] Настроена обработка ошибок аутентификации
|
||||
- [ ] Добавлен health check endpoint
|
||||
|
||||
### Безопасность
|
||||
- [ ] Валидация токенов включает проверку Redis сессий
|
||||
- [ ] Настроен rate limiting (опционально)
|
||||
- [ ] Логирование событий аутентификации
|
||||
- [ ] Обработка истекших токенов
|
||||
|
||||
### Мониторинг
|
||||
- [ ] Health check интегрирован в систему мониторинга
|
||||
- [ ] Метрики аутентификации собираются
|
||||
- [ ] Алерты настроены для проблем с Redis/JWT
|
||||
|
||||
### Тестирование
|
||||
- [ ] Unit тесты для функций аутентификации
|
||||
- [ ] Integration тесты с Redis
|
||||
- [ ] E2E тесты с реальными токенами
|
||||
- [ ] Load тесты для проверки производительности
|
||||
322
docs/auth/migration.md
Normal file
322
docs/auth/migration.md
Normal file
@@ -0,0 +1,322 @@
|
||||
# Миграция системы авторизации
|
||||
|
||||
## Обзор изменений
|
||||
|
||||
Система авторизации была полностью переработана для улучшения производительности, безопасности и поддерживаемости:
|
||||
|
||||
### Основные изменения
|
||||
- ✅ Упрощена архитектура токенов (убрана прокси-логика)
|
||||
- ✅ Исправлены проблемы с типами (mypy clean)
|
||||
- ✅ Оптимизированы Redis операции
|
||||
- ✅ Добавлена система мониторинга токенов
|
||||
- ✅ Улучшена производительность OAuth
|
||||
- ✅ Удалены deprecated компоненты
|
||||
|
||||
## Миграция кода
|
||||
|
||||
### TokenStorage API
|
||||
|
||||
#### Было (deprecated):
|
||||
```python
|
||||
# Старый универсальный API
|
||||
await TokenStorage.create_token("session", user_id, data, ttl)
|
||||
await TokenStorage.get_token_data("session", token)
|
||||
await TokenStorage.validate_token(token, "session")
|
||||
await TokenStorage.revoke_token("session", token)
|
||||
```
|
||||
|
||||
#### Стало (рекомендуется):
|
||||
```python
|
||||
# Прямое использование менеджеров
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
from auth.tokens.verification import VerificationTokenManager
|
||||
from auth.tokens.oauth import OAuthTokenManager
|
||||
|
||||
# Сессии
|
||||
sessions = SessionTokenManager()
|
||||
token = await sessions.create_session(user_id, username=username)
|
||||
valid, data = await sessions.validate_session_token(token)
|
||||
await sessions.revoke_session_token(token)
|
||||
|
||||
# Токены подтверждения
|
||||
verification = VerificationTokenManager()
|
||||
token = await verification.create_verification_token(user_id, "email_change", data)
|
||||
valid, data = await verification.validate_verification_token(token)
|
||||
|
||||
# OAuth токены
|
||||
oauth = OAuthTokenManager()
|
||||
await oauth.store_oauth_tokens(user_id, "google", access_token, refresh_token)
|
||||
```
|
||||
|
||||
#### Фасад TokenStorage (для совместимости):
|
||||
```python
|
||||
# Упрощенный фасад для основных операций
|
||||
await TokenStorage.create_session(user_id, username=username)
|
||||
await TokenStorage.verify_session(token)
|
||||
await TokenStorage.refresh_session(user_id, old_token, device_info)
|
||||
await TokenStorage.revoke_session(token)
|
||||
```
|
||||
|
||||
### Redis Service
|
||||
|
||||
#### Обновленный API:
|
||||
```python
|
||||
from storage.redis import redis
|
||||
|
||||
# Базовые операции
|
||||
await redis.get(key)
|
||||
await redis.set(key, value, ex=ttl)
|
||||
await redis.delete(key)
|
||||
await redis.exists(key)
|
||||
|
||||
# Pipeline операции
|
||||
async with redis.pipeline(transaction=True) as pipe:
|
||||
await pipe.hset(key, field, value)
|
||||
await pipe.expire(key, seconds)
|
||||
results = await pipe.execute()
|
||||
|
||||
# Новые методы
|
||||
await redis.scan(cursor, match=pattern, count=100)
|
||||
await redis.scard(key)
|
||||
await redis.ttl(key)
|
||||
await redis.info(section="memory")
|
||||
```
|
||||
|
||||
### Мониторинг токенов
|
||||
|
||||
#### Новые возможности:
|
||||
```python
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
|
||||
monitoring = TokenMonitoring()
|
||||
|
||||
# Статистика токенов
|
||||
stats = await monitoring.get_token_statistics()
|
||||
print(f"Active sessions: {stats['session_tokens']}")
|
||||
print(f"Memory usage: {stats['memory_usage']} bytes")
|
||||
|
||||
# Health check
|
||||
health = await monitoring.health_check()
|
||||
if health["status"] == "healthy":
|
||||
print("Token system is healthy")
|
||||
|
||||
# Оптимизация памяти
|
||||
results = await monitoring.optimize_memory_usage()
|
||||
print(f"Cleaned {results['cleaned_expired']} expired tokens")
|
||||
```
|
||||
|
||||
### Пакетные операции
|
||||
|
||||
#### Новые возможности:
|
||||
```python
|
||||
from auth.tokens.batch import BatchTokenOperations
|
||||
|
||||
batch = BatchTokenOperations()
|
||||
|
||||
# Массовая валидация
|
||||
tokens = ["token1", "token2", "token3"]
|
||||
results = await batch.batch_validate_tokens(tokens)
|
||||
# {"token1": True, "token2": False, "token3": True}
|
||||
|
||||
# Массовый отзыв
|
||||
revoked_count = await batch.batch_revoke_tokens(tokens)
|
||||
print(f"Revoked {revoked_count} tokens")
|
||||
|
||||
# Очистка истекших
|
||||
cleaned = await batch.cleanup_expired_tokens()
|
||||
print(f"Cleaned {cleaned} expired tokens")
|
||||
```
|
||||
|
||||
## Изменения в конфигурации
|
||||
|
||||
### Переменные окружения
|
||||
|
||||
#### Добавлены:
|
||||
```bash
|
||||
# Новые OAuth провайдеры
|
||||
VK_APP_ID=your_vk_app_id
|
||||
VK_APP_SECRET=your_vk_app_secret
|
||||
YANDEX_CLIENT_ID=your_yandex_client_id
|
||||
YANDEX_CLIENT_SECRET=your_yandex_client_secret
|
||||
|
||||
# Расширенные настройки Redis
|
||||
REDIS_SOCKET_KEEPALIVE=true
|
||||
REDIS_HEALTH_CHECK_INTERVAL=30
|
||||
REDIS_SOCKET_TIMEOUT=5
|
||||
```
|
||||
|
||||
#### Удалены:
|
||||
```bash
|
||||
# Больше не используются
|
||||
OLD_TOKEN_FORMAT_SUPPORT=true # автоматически определяется
|
||||
TOKEN_CLEANUP_INTERVAL=3600 # заменено на on-demand cleanup
|
||||
```
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
### 1. Убраны deprecated методы
|
||||
|
||||
#### Удалено:
|
||||
```python
|
||||
# Эти методы больше не существуют
|
||||
TokenStorage.create_token() # -> используйте конкретные менеджеры
|
||||
TokenStorage.get_token_data() # -> используйте конкретные менеджеры
|
||||
TokenStorage.validate_token() # -> используйте конкретные менеджеры
|
||||
TokenStorage.revoke_user_tokens() # -> используйте конкретные менеджеры
|
||||
```
|
||||
|
||||
#### Альтернативы:
|
||||
```python
|
||||
# Для сессий
|
||||
sessions = SessionTokenManager()
|
||||
await sessions.create_session(user_id)
|
||||
await sessions.revoke_user_sessions(user_id)
|
||||
|
||||
# Для verification
|
||||
verification = VerificationTokenManager()
|
||||
await verification.create_verification_token(user_id, "email", data)
|
||||
await verification.revoke_user_verification_tokens(user_id)
|
||||
```
|
||||
|
||||
### 2. Изменения в compat.py
|
||||
|
||||
Файл `auth/tokens/compat.py` удален. Если вы использовали `CompatibilityMethods`:
|
||||
|
||||
#### Миграция:
|
||||
```python
|
||||
# Было
|
||||
from auth.tokens.compat import CompatibilityMethods
|
||||
compat = CompatibilityMethods()
|
||||
await compat.get(token_key)
|
||||
|
||||
# Стало
|
||||
from storage.redis import redis
|
||||
result = await redis.get(token_key)
|
||||
```
|
||||
|
||||
### 3. Изменения в типах
|
||||
|
||||
#### Обновленные импорты:
|
||||
```python
|
||||
# Было
|
||||
from auth.tokens.storage import TokenType, TokenData
|
||||
|
||||
# Стало
|
||||
from auth.tokens.types import TokenType, TokenData
|
||||
```
|
||||
|
||||
## Рекомендации по миграции
|
||||
|
||||
### Поэтапная миграция
|
||||
|
||||
#### Шаг 1: Обновите импорты
|
||||
```python
|
||||
# Замените старые импорты
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
from auth.tokens.verification import VerificationTokenManager
|
||||
from auth.tokens.oauth import OAuthTokenManager
|
||||
```
|
||||
|
||||
#### Шаг 2: Используйте конкретные менеджеры
|
||||
```python
|
||||
# Вместо универсального TokenStorage
|
||||
# используйте специализированные менеджеры
|
||||
sessions = SessionTokenManager()
|
||||
```
|
||||
|
||||
#### Шаг 3: Добавьте мониторинг
|
||||
```python
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
|
||||
# Добавьте health checks в ваши endpoints
|
||||
monitoring = TokenMonitoring()
|
||||
health = await monitoring.health_check()
|
||||
```
|
||||
|
||||
#### Шаг 4: Оптимизируйте батчевые операции
|
||||
```python
|
||||
from auth.tokens.batch import BatchTokenOperations
|
||||
|
||||
# Используйте batch операции для массовых действий
|
||||
batch = BatchTokenOperations()
|
||||
results = await batch.batch_validate_tokens(token_list)
|
||||
```
|
||||
|
||||
### Тестирование миграции
|
||||
|
||||
#### Checklist:
|
||||
- [ ] Все auth тесты проходят
|
||||
- [ ] mypy проверки без ошибок
|
||||
- [ ] OAuth провайдеры работают
|
||||
- [ ] Session management функционирует
|
||||
- [ ] Redis операции оптимизированы
|
||||
- [ ] Мониторинг настроен
|
||||
|
||||
#### Команды для тестирования:
|
||||
```bash
|
||||
# Проверка типов
|
||||
mypy .
|
||||
|
||||
# Запуск auth тестов
|
||||
pytest tests/auth/ -v
|
||||
|
||||
# Проверка Redis подключения
|
||||
python -c "
|
||||
import asyncio
|
||||
from storage.redis import redis
|
||||
async def test():
|
||||
result = await redis.ping()
|
||||
print(f'Redis connection: {result}')
|
||||
asyncio.run(test())
|
||||
"
|
||||
|
||||
# Health check системы токенов
|
||||
python -c "
|
||||
import asyncio
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
async def test():
|
||||
health = await TokenMonitoring().health_check()
|
||||
print(f'Token system health: {health}')
|
||||
asyncio.run(test())
|
||||
"
|
||||
```
|
||||
|
||||
## Производительность
|
||||
|
||||
### Ожидаемые улучшения
|
||||
- **50%** ускорение Redis операций (pipeline использование)
|
||||
- **30%** снижение memory usage (оптимизированные структуры)
|
||||
- **Elimination** of proxy overhead (прямое обращение к менеджерам)
|
||||
- **Real-time** мониторинг и статистика
|
||||
|
||||
### Мониторинг после миграции
|
||||
```python
|
||||
# Регулярно проверяйте статистику
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
|
||||
async def check_performance():
|
||||
monitoring = TokenMonitoring()
|
||||
stats = await monitoring.get_token_statistics()
|
||||
|
||||
print(f"Session tokens: {stats['session_tokens']}")
|
||||
print(f"Memory usage: {stats['memory_usage'] / 1024 / 1024:.2f} MB")
|
||||
|
||||
# Оптимизация при необходимости
|
||||
if stats['memory_usage'] > 100 * 1024 * 1024: # 100MB
|
||||
results = await monitoring.optimize_memory_usage()
|
||||
print(f"Optimized: {results}")
|
||||
```
|
||||
|
||||
## Поддержка
|
||||
|
||||
Если возникли проблемы при миграции:
|
||||
|
||||
1. **Проверьте логи** - все изменения логируются
|
||||
2. **Запустите health check** - `TokenMonitoring().health_check()`
|
||||
3. **Проверьте Redis** - подключение и память
|
||||
4. **Откатитесь к TokenStorage фасаду** при необходимости
|
||||
|
||||
### Контакты
|
||||
- **Issues**: GitHub Issues
|
||||
- **Документация**: `/docs/auth/system.md`
|
||||
- **Архитектура**: `/docs/auth/architecture.md`
|
||||
466
docs/auth/oauth.md
Normal file
466
docs/auth/oauth.md
Normal file
@@ -0,0 +1,466 @@
|
||||
# OAuth Integration Guide
|
||||
|
||||
## 🎯 Обзор
|
||||
|
||||
Система OAuth интеграции с поддержкой популярных провайдеров. Токены хранятся в Redis с автоматическим TTL и поддержкой refresh.
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### Поддерживаемые провайдеры
|
||||
- **Google** - OpenID Connect
|
||||
- **GitHub** - OAuth 2.0
|
||||
- **Facebook** - Facebook Login
|
||||
- **VK** - VK OAuth
|
||||
- **Yandex** - Yandex OAuth
|
||||
- **X (Twitter)** - OAuth 2.0
|
||||
- **Telegram** - Telegram Login
|
||||
|
||||
### Redis структура
|
||||
```bash
|
||||
oauth_access:{user_id}:{provider} # Access токены
|
||||
oauth_refresh:{user_id}:{provider} # Refresh токены
|
||||
oauth_state:{state} # OAuth state с TTL 10 минут
|
||||
```
|
||||
|
||||
### Основные операции
|
||||
```python
|
||||
from auth.tokens.oauth import OAuthTokenManager
|
||||
|
||||
oauth = OAuthTokenManager()
|
||||
|
||||
# Сохранение токенов
|
||||
await oauth.store_oauth_tokens(
|
||||
user_id="123",
|
||||
provider="google",
|
||||
access_token="ya29.a0AfH6SM...",
|
||||
refresh_token="1//04...",
|
||||
expires_in=3600
|
||||
)
|
||||
|
||||
# Получение токена
|
||||
access_data = await oauth.get_token(user_id, "google", "oauth_access")
|
||||
|
||||
# Отзыв токенов
|
||||
await oauth.revoke_oauth_tokens(user_id, "google")
|
||||
```
|
||||
|
||||
## 🔧 OAuth Flow
|
||||
|
||||
### 1. Инициация OAuth
|
||||
```python
|
||||
# Frontend
|
||||
const oauth = (provider: string) => {
|
||||
const state = crypto.randomUUID()
|
||||
localStorage.setItem('oauth_state', state)
|
||||
|
||||
const oauthUrl = `${coreApiUrl}/auth/oauth/${provider}?state=${state}&redirect_uri=${encodeURIComponent(window.location.origin)}`
|
||||
window.location.href = oauthUrl
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Backend Endpoints
|
||||
|
||||
#### GET `/auth/oauth/{provider}`
|
||||
```python
|
||||
@router.get("/auth/oauth/{provider}")
|
||||
async def oauth_redirect(
|
||||
provider: str,
|
||||
state: str,
|
||||
redirect_uri: str,
|
||||
request: Request
|
||||
):
|
||||
# Валидация провайдера
|
||||
if provider not in SUPPORTED_PROVIDERS:
|
||||
raise HTTPException(status_code=400, detail="Unsupported OAuth provider")
|
||||
|
||||
# Сохранение state в Redis
|
||||
await store_oauth_state(state, redirect_uri)
|
||||
|
||||
# Генерация URL провайдера
|
||||
oauth_url = generate_provider_url(provider, state, redirect_uri)
|
||||
|
||||
return RedirectResponse(url=oauth_url)
|
||||
```
|
||||
|
||||
#### GET `/auth/oauth/{provider}/callback`
|
||||
```python
|
||||
@router.get("/auth/oauth/{provider}/callback")
|
||||
async def oauth_callback(
|
||||
provider: str,
|
||||
code: str,
|
||||
state: str,
|
||||
request: Request
|
||||
):
|
||||
# Проверка state
|
||||
stored_data = await get_oauth_state(state)
|
||||
if not stored_data:
|
||||
raise HTTPException(status_code=400, detail="Invalid or expired state")
|
||||
|
||||
# Обмен code на access_token
|
||||
try:
|
||||
user_data = await exchange_code_for_user_data(provider, code)
|
||||
except OAuthException as e:
|
||||
logger.error(f"OAuth error for {provider}: {e}")
|
||||
return RedirectResponse(url=f"{stored_data['redirect_uri']}?error=oauth_failed")
|
||||
|
||||
# Поиск/создание пользователя
|
||||
user = await get_or_create_user_from_oauth(provider, user_data)
|
||||
|
||||
# Генерация JWT токена
|
||||
access_token = generate_jwt_token(user.id)
|
||||
|
||||
# Редирект обратно на фронтенд
|
||||
redirect_url = f"{stored_data['redirect_uri']}?state={state}&access_token={access_token}"
|
||||
return RedirectResponse(url=redirect_url)
|
||||
```
|
||||
|
||||
### 3. OAuth State Management
|
||||
```python
|
||||
import redis
|
||||
from datetime import timedelta
|
||||
|
||||
redis_client = redis.Redis()
|
||||
|
||||
async def store_oauth_state(
|
||||
state: str,
|
||||
redirect_uri: str,
|
||||
ttl: timedelta = timedelta(minutes=10)
|
||||
):
|
||||
"""Сохранение OAuth state с TTL"""
|
||||
key = f"oauth_state:{state}"
|
||||
data = {
|
||||
"redirect_uri": redirect_uri,
|
||||
"created_at": datetime.utcnow().isoformat()
|
||||
}
|
||||
await redis_client.setex(key, ttl, json.dumps(data))
|
||||
|
||||
async def get_oauth_state(state: str) -> Optional[dict]:
|
||||
"""Получение и удаление OAuth state"""
|
||||
key = f"oauth_state:{state}"
|
||||
data = await redis_client.get(key)
|
||||
if data:
|
||||
await redis_client.delete(key) # One-time use
|
||||
return json.loads(data)
|
||||
return None
|
||||
```
|
||||
|
||||
## 🔐 Провайдеры
|
||||
|
||||
### Google OAuth
|
||||
```python
|
||||
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",
|
||||
"scope": "openid email profile"
|
||||
}
|
||||
```
|
||||
|
||||
### GitHub OAuth
|
||||
```python
|
||||
GITHUB_OAUTH_CONFIG = {
|
||||
"client_id": os.getenv("GITHUB_CLIENT_ID"),
|
||||
"client_secret": os.getenv("GITHUB_CLIENT_SECRET"),
|
||||
"auth_url": "https://github.com/login/oauth/authorize",
|
||||
"token_url": "https://github.com/login/oauth/access_token",
|
||||
"user_info_url": "https://api.github.com/user",
|
||||
"scope": "read:user user:email"
|
||||
}
|
||||
```
|
||||
|
||||
### Facebook OAuth
|
||||
```python
|
||||
FACEBOOK_OAUTH_CONFIG = {
|
||||
"client_id": os.getenv("FACEBOOK_APP_ID"),
|
||||
"client_secret": os.getenv("FACEBOOK_APP_SECRET"),
|
||||
"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"
|
||||
}
|
||||
```
|
||||
|
||||
### VK OAuth
|
||||
```python
|
||||
VK_OAUTH_CONFIG = {
|
||||
"client_id": os.getenv("VK_APP_ID"),
|
||||
"client_secret": os.getenv("VK_APP_SECRET"),
|
||||
"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"
|
||||
}
|
||||
```
|
||||
|
||||
### Yandex OAuth
|
||||
```python
|
||||
YANDEX_OAUTH_CONFIG = {
|
||||
"client_id": os.getenv("YANDEX_CLIENT_ID"),
|
||||
"client_secret": os.getenv("YANDEX_CLIENT_SECRET"),
|
||||
"auth_url": "https://oauth.yandex.ru/authorize",
|
||||
"token_url": "https://oauth.yandex.ru/token",
|
||||
"user_info_url": "https://login.yandex.ru/info",
|
||||
"scope": "login:email login:info"
|
||||
}
|
||||
```
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
### TTL и истечение токенов
|
||||
- **Access tokens**: 1 час (настраивается)
|
||||
- **Refresh tokens**: 30 дней
|
||||
- **OAuth state**: 10 минут
|
||||
- **Автоматическая очистка**: Redis удаляет истекшие токены
|
||||
- **Изоляция провайдеров**: Токены разных провайдеров хранятся отдельно
|
||||
|
||||
### CSRF Protection
|
||||
```python
|
||||
def validate_oauth_state(stored_state: str, received_state: str) -> bool:
|
||||
"""Проверка OAuth state для защиты от CSRF"""
|
||||
return stored_state == received_state
|
||||
|
||||
def validate_redirect_uri(uri: str) -> bool:
|
||||
"""Валидация redirect_uri для предотвращения открытых редиректов"""
|
||||
allowed_domains = [
|
||||
"localhost:3000",
|
||||
"discours.io",
|
||||
"new.discours.io"
|
||||
]
|
||||
|
||||
parsed = urlparse(uri)
|
||||
return any(domain in parsed.netloc for domain in allowed_domains)
|
||||
```
|
||||
|
||||
## 💡 Практические примеры
|
||||
|
||||
### OAuth Login Flow
|
||||
```python
|
||||
from auth.oauth import oauth_login, oauth_callback, _create_or_update_user
|
||||
from auth.oauth import oauth_login_http, oauth_callback_http
|
||||
from auth.oauth import store_oauth_state, get_oauth_state
|
||||
|
||||
# GraphQL resolver для OAuth login
|
||||
async def handle_oauth_login(provider: str, callback_data: dict):
|
||||
"""Инициация OAuth авторизации"""
|
||||
return await oauth_login(None, info, provider, callback_data)
|
||||
|
||||
# HTTP handler для OAuth login
|
||||
async def handle_oauth_login_http(request):
|
||||
"""HTTP инициация OAuth авторизации"""
|
||||
return await oauth_login_http(request)
|
||||
|
||||
# HTTP handler для OAuth callback
|
||||
async def handle_oauth_callback_http(request):
|
||||
"""HTTP обработка OAuth callback"""
|
||||
return await oauth_callback_http(request)
|
||||
|
||||
# Создание/обновление пользователя
|
||||
async def create_user_from_oauth(provider: str, profile: dict):
|
||||
"""Создание пользователя из OAuth профиля"""
|
||||
return await _create_or_update_user(provider, profile)
|
||||
|
||||
# Управление OAuth состоянием
|
||||
await store_oauth_state(state, oauth_data)
|
||||
state_data = await get_oauth_state(state)
|
||||
```
|
||||
|
||||
### API Integration
|
||||
```python
|
||||
async def make_oauth_request(user_id: int, provider: str, endpoint: str):
|
||||
"""Запрос к API провайдера"""
|
||||
oauth = OAuthTokenManager()
|
||||
|
||||
# Получаем access token
|
||||
token_data = await oauth.get_token(str(user_id), provider, "oauth_access")
|
||||
if not token_data:
|
||||
raise OAuthTokenMissing()
|
||||
|
||||
# Делаем запрос
|
||||
headers = {"Authorization": f"Bearer {token_data['token']}"}
|
||||
response = await httpx.get(endpoint, headers=headers)
|
||||
|
||||
if response.status_code == 401:
|
||||
# Токен истек, требуется повторная авторизация
|
||||
raise OAuthTokenExpired()
|
||||
|
||||
return response.json()
|
||||
```
|
||||
|
||||
### Мониторинг токенов
|
||||
```python
|
||||
async def check_oauth_health():
|
||||
"""Проверка здоровья OAuth системы"""
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
|
||||
monitoring = TokenMonitoring()
|
||||
stats = await monitoring.get_token_statistics()
|
||||
|
||||
return {
|
||||
"oauth_tokens": stats["oauth_access_tokens"] + stats["oauth_refresh_tokens"],
|
||||
"memory_usage": stats["memory_usage"]
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Настройка и деплой
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# Google OAuth
|
||||
GOOGLE_CLIENT_ID=your_google_client_id
|
||||
GOOGLE_CLIENT_SECRET=your_google_client_secret
|
||||
|
||||
# GitHub OAuth
|
||||
GITHUB_CLIENT_ID=your_github_client_id
|
||||
GITHUB_CLIENT_SECRET=your_github_client_secret
|
||||
|
||||
# Facebook OAuth
|
||||
FACEBOOK_APP_ID=your_facebook_app_id
|
||||
FACEBOOK_APP_SECRET=your_facebook_app_secret
|
||||
|
||||
# VK OAuth
|
||||
VK_APP_ID=your_vk_app_id
|
||||
VK_APP_SECRET=your_vk_app_secret
|
||||
|
||||
# Yandex OAuth
|
||||
YANDEX_CLIENT_ID=your_yandex_client_id
|
||||
YANDEX_CLIENT_SECRET=your_yandex_client_secret
|
||||
|
||||
# Redis для state management
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your_jwt_secret_key
|
||||
JWT_EXPIRATION_HOURS=24
|
||||
```
|
||||
|
||||
### Настройка провайдеров
|
||||
|
||||
#### Google OAuth
|
||||
1. Перейти в [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Создать новый проект или выбрать существующий
|
||||
3. Включить Google+ API
|
||||
4. Настроить OAuth consent screen
|
||||
5. Создать OAuth 2.0 credentials
|
||||
6. Добавить redirect URIs:
|
||||
- `https://your-domain.com/auth/oauth/google/callback`
|
||||
- `http://localhost:3000/auth/oauth/google/callback` (для разработки)
|
||||
|
||||
#### GitHub OAuth
|
||||
1. Перейти в [GitHub Settings](https://github.com/settings/applications/new)
|
||||
2. Создать новое OAuth App
|
||||
3. Настроить Authorization callback URL:
|
||||
- `https://your-domain.com/auth/oauth/github/callback`
|
||||
|
||||
#### Facebook OAuth
|
||||
1. Перейти в [Facebook Developers](https://developers.facebook.com/)
|
||||
2. Создать новое приложение
|
||||
3. Добавить продукт "Facebook Login"
|
||||
4. Настроить Valid OAuth Redirect URIs:
|
||||
- `https://your-domain.com/auth/oauth/facebook/callback`
|
||||
|
||||
### Redis команды для отладки
|
||||
```bash
|
||||
# Поиск OAuth токенов пользователя
|
||||
redis-cli --scan --pattern "oauth_access:123:*"
|
||||
redis-cli --scan --pattern "oauth_refresh:123:*"
|
||||
|
||||
# Получение данных токена
|
||||
redis-cli GET "oauth_access:123:google"
|
||||
|
||||
# Проверка TTL
|
||||
redis-cli TTL "oauth_access:123:google"
|
||||
|
||||
# Поиск OAuth state
|
||||
redis-cli --scan --pattern "oauth_state:*"
|
||||
```
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
### Unit Tests
|
||||
```python
|
||||
def test_oauth_redirect():
|
||||
response = client.get("/auth/oauth/google?state=test&redirect_uri=http://localhost:3000")
|
||||
assert response.status_code == 307
|
||||
assert "accounts.google.com" in response.headers["location"]
|
||||
|
||||
def test_oauth_callback():
|
||||
# Mock provider response
|
||||
with mock.patch('oauth.exchange_code_for_user_data') as mock_exchange:
|
||||
mock_exchange.return_value = OAuthUser(
|
||||
provider="google",
|
||||
provider_id="123456",
|
||||
email="test@example.com",
|
||||
name="Test User"
|
||||
)
|
||||
|
||||
response = client.get("/auth/oauth/google/callback?code=test_code&state=test_state")
|
||||
assert response.status_code == 307
|
||||
assert "access_token=" in response.headers["location"]
|
||||
```
|
||||
|
||||
### E2E Tests
|
||||
```typescript
|
||||
// tests/oauth.spec.ts
|
||||
test('OAuth flow with Google', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
|
||||
// Click Google OAuth button
|
||||
await page.click('[data-testid="oauth-google"]')
|
||||
|
||||
// Should redirect to Google
|
||||
await page.waitForURL(/accounts\.google\.com/)
|
||||
|
||||
// Mock successful OAuth (in test environment)
|
||||
await page.goto('/?state=test&access_token=mock_token')
|
||||
|
||||
// Should be logged in
|
||||
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Частые ошибки
|
||||
|
||||
1. **"OAuth state mismatch"**
|
||||
- Проверьте TTL Redis
|
||||
- Убедитесь, что state генерируется правильно
|
||||
|
||||
2. **"Provider authentication failed"**
|
||||
- Проверьте client_id и client_secret
|
||||
- Убедитесь, что redirect_uri совпадает с настройками провайдера
|
||||
|
||||
3. **"Invalid redirect URI"**
|
||||
- Добавьте все возможные redirect URIs в настройки приложения
|
||||
- Проверьте HTTPS/HTTP в production/development
|
||||
|
||||
### Логи для отладки
|
||||
```bash
|
||||
# Backend логи
|
||||
tail -f /var/log/app/oauth.log | grep "oauth"
|
||||
|
||||
# Frontend логи (browser console)
|
||||
# Фильтр: "[oauth]" или "[SessionProvider]"
|
||||
```
|
||||
|
||||
## 📊 Мониторинг
|
||||
|
||||
```python
|
||||
# Добавить метрики для мониторинга
|
||||
from prometheus_client import Counter, Histogram
|
||||
|
||||
oauth_requests = Counter('oauth_requests_total', 'OAuth requests', ['provider', 'status'])
|
||||
oauth_duration = Histogram('oauth_duration_seconds', 'OAuth request duration')
|
||||
|
||||
@router.get("/{provider}")
|
||||
async def oauth_redirect(provider: str, state: str, redirect_uri: str):
|
||||
with oauth_duration.time():
|
||||
try:
|
||||
# OAuth logic
|
||||
oauth_requests.labels(provider=provider, status='success').inc()
|
||||
except Exception as e:
|
||||
oauth_requests.labels(provider=provider, status='error').inc()
|
||||
raise
|
||||
```
|
||||
579
docs/auth/security.md
Normal file
579
docs/auth/security.md
Normal file
@@ -0,0 +1,579 @@
|
||||
# 🔒 Безопасность системы аутентификации
|
||||
|
||||
## 🎯 Обзор
|
||||
|
||||
Комплексная система безопасности с многоуровневой защитой от различных типов атак.
|
||||
|
||||
## 🛡️ Основные принципы безопасности
|
||||
|
||||
### 1. Defense in Depth
|
||||
- **Многоуровневая защита**: JWT + Redis + RBAC + Rate Limiting
|
||||
- **Fail Secure**: При ошибках система блокирует доступ
|
||||
- **Principle of Least Privilege**: Минимальные необходимые права
|
||||
|
||||
### 2. Zero Trust Architecture
|
||||
- **Verify Everything**: Каждый запрос проверяется
|
||||
- **Never Trust, Always Verify**: Нет доверенных зон
|
||||
- **Continuous Validation**: Постоянная проверка токенов
|
||||
|
||||
## 🔐 JWT Security
|
||||
|
||||
### Алгоритм и ключи
|
||||
```python
|
||||
# settings.py
|
||||
JWT_ALGORITHM = "HS256" # HMAC with SHA-256
|
||||
JWT_SECRET = os.getenv("JWT_SECRET") # Минимум 256 бит
|
||||
JWT_EXPIRATION_DELTA = 30 * 24 * 60 * 60 # 30 дней
|
||||
```
|
||||
|
||||
### Структура токена
|
||||
```python
|
||||
# JWT Payload
|
||||
{
|
||||
"user_id": "123",
|
||||
"username": "john_doe",
|
||||
"iat": 1640995200, # Issued At
|
||||
"exp": 1643587200 # Expiration
|
||||
}
|
||||
```
|
||||
|
||||
### Лучшие практики JWT
|
||||
- **Короткое время жизни**: Максимум 30 дней
|
||||
- **Secure Secret**: Криптографически стойкий ключ
|
||||
- **No Sensitive Data**: Только необходимые данные в payload
|
||||
- **Revocation Support**: Redis для отзыва токенов
|
||||
|
||||
## 🍪 Cookie Security
|
||||
|
||||
### httpOnly Cookies
|
||||
```python
|
||||
# Настройки cookie
|
||||
SESSION_COOKIE_NAME = "session_token"
|
||||
SESSION_COOKIE_HTTPONLY = True # Защита от XSS
|
||||
SESSION_COOKIE_SECURE = True # Только HTTPS
|
||||
SESSION_COOKIE_SAMESITE = "lax" # CSRF защита
|
||||
SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60
|
||||
```
|
||||
|
||||
### Защита от атак
|
||||
- **XSS Protection**: httpOnly cookies недоступны JavaScript
|
||||
- **CSRF Protection**: SameSite=lax предотвращает CSRF
|
||||
- **Secure Flag**: Передача только по HTTPS
|
||||
- **Path Restriction**: Ограничение области действия
|
||||
|
||||
## 🔑 Password Security
|
||||
|
||||
### Хеширование паролей
|
||||
```python
|
||||
from passlib.context import CryptContext
|
||||
|
||||
pwd_context = CryptContext(
|
||||
schemes=["bcrypt"],
|
||||
deprecated="auto",
|
||||
bcrypt__rounds=12 # Увеличенная сложность
|
||||
)
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""Хеширует пароль с использованием bcrypt"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Проверяет пароль"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
```
|
||||
|
||||
### Требования к паролям
|
||||
```python
|
||||
import re
|
||||
|
||||
def validate_password_strength(password: str) -> bool:
|
||||
"""Проверка силы пароля"""
|
||||
if len(password) < 8:
|
||||
return False
|
||||
|
||||
# Проверки
|
||||
has_upper = re.search(r'[A-Z]', password)
|
||||
has_lower = re.search(r'[a-z]', password)
|
||||
has_digit = re.search(r'\d', password)
|
||||
has_special = re.search(r'[!@#$%^&*(),.?":{}|<>]', password)
|
||||
|
||||
return all([has_upper, has_lower, has_digit, has_special])
|
||||
```
|
||||
|
||||
## 🚫 Защита от брутфорса
|
||||
|
||||
### Account Lockout
|
||||
```python
|
||||
async def handle_login_attempt(author: Author, success: bool) -> None:
|
||||
"""Обрабатывает попытку входа"""
|
||||
if not success:
|
||||
# Увеличиваем счетчик неудачных попыток
|
||||
author.failed_login_attempts += 1
|
||||
|
||||
if author.failed_login_attempts >= 5:
|
||||
# Блокируем аккаунт на 30 минут
|
||||
author.account_locked_until = int(time.time()) + 1800
|
||||
logger.warning(f"Аккаунт {author.email} заблокирован")
|
||||
else:
|
||||
# Сбрасываем счетчик при успешном входе
|
||||
author.failed_login_attempts = 0
|
||||
author.account_locked_until = None
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
```python
|
||||
from collections import defaultdict
|
||||
import time
|
||||
|
||||
# Rate limiter
|
||||
request_counts = defaultdict(list)
|
||||
|
||||
async def rate_limit_check(
|
||||
identifier: str,
|
||||
max_requests: int = 10,
|
||||
window_seconds: int = 60
|
||||
) -> bool:
|
||||
"""Проверка rate limiting"""
|
||||
current_time = time.time()
|
||||
user_requests = request_counts[identifier]
|
||||
|
||||
# Удаляем старые запросы
|
||||
user_requests[:] = [
|
||||
req_time for req_time in user_requests
|
||||
if current_time - req_time < window_seconds
|
||||
]
|
||||
|
||||
# Проверяем лимит
|
||||
if len(user_requests) >= max_requests:
|
||||
return False
|
||||
|
||||
# Добавляем текущий запрос
|
||||
user_requests.append(current_time)
|
||||
return True
|
||||
```
|
||||
|
||||
## 🔒 Redis Security
|
||||
|
||||
### Secure Configuration
|
||||
```python
|
||||
# Redis настройки безопасности
|
||||
REDIS_CONFIG = {
|
||||
"socket_keepalive": True,
|
||||
"socket_keepalive_options": {},
|
||||
"health_check_interval": 30,
|
||||
"retry_on_timeout": True,
|
||||
"socket_timeout": 5,
|
||||
"socket_connect_timeout": 5
|
||||
}
|
||||
```
|
||||
|
||||
### TTL для всех ключей
|
||||
```python
|
||||
async def secure_redis_set(key: str, value: str, ttl: int = 3600):
|
||||
"""Безопасная установка значения с обязательным TTL"""
|
||||
await redis.setex(key, ttl, value)
|
||||
|
||||
# Проверяем, что TTL установлен
|
||||
actual_ttl = await redis.ttl(key)
|
||||
if actual_ttl <= 0:
|
||||
logger.error(f"TTL не установлен для ключа: {key}")
|
||||
await redis.delete(key)
|
||||
```
|
||||
|
||||
### Атомарные операции
|
||||
```python
|
||||
async def atomic_session_update(user_id: str, token: str, data: dict):
|
||||
"""Атомарное обновление сессии"""
|
||||
async with redis.pipeline(transaction=True) as pipe:
|
||||
try:
|
||||
# Начинаем транзакцию
|
||||
await pipe.multi()
|
||||
|
||||
# Обновляем данные сессии
|
||||
session_key = f"session:{user_id}:{token}"
|
||||
await pipe.hset(session_key, mapping=data)
|
||||
await pipe.expire(session_key, 30 * 24 * 60 * 60)
|
||||
|
||||
# Обновляем список активных сессий
|
||||
sessions_key = f"user_sessions:{user_id}"
|
||||
await pipe.sadd(sessions_key, token)
|
||||
await pipe.expire(sessions_key, 30 * 24 * 60 * 60)
|
||||
|
||||
# Выполняем транзакцию
|
||||
await pipe.execute()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка атомарной операции: {e}")
|
||||
raise
|
||||
```
|
||||
|
||||
## 🛡️ OAuth Security
|
||||
|
||||
### State Parameter Protection
|
||||
```python
|
||||
import secrets
|
||||
|
||||
def generate_oauth_state() -> str:
|
||||
"""Генерация криптографически стойкого state"""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
async def validate_oauth_state(received_state: str, stored_state: str) -> bool:
|
||||
"""Безопасная проверка state"""
|
||||
if not received_state or not stored_state:
|
||||
return False
|
||||
|
||||
# Используем constant-time comparison
|
||||
return secrets.compare_digest(received_state, stored_state)
|
||||
```
|
||||
|
||||
### PKCE Support
|
||||
```python
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
def generate_code_verifier() -> str:
|
||||
"""Генерация code verifier для PKCE"""
|
||||
return base64.urlsafe_b64encode(secrets.token_bytes(32)).decode('utf-8').rstrip('=')
|
||||
|
||||
def generate_code_challenge(verifier: str) -> str:
|
||||
"""Генерация code challenge"""
|
||||
digest = hashlib.sha256(verifier.encode('utf-8')).digest()
|
||||
return base64.urlsafe_b64encode(digest).decode('utf-8').rstrip('=')
|
||||
```
|
||||
|
||||
### Redirect URI Validation
|
||||
```python
|
||||
from urllib.parse import urlparse
|
||||
|
||||
def validate_redirect_uri(uri: str) -> bool:
|
||||
"""Валидация redirect URI"""
|
||||
allowed_domains = [
|
||||
"localhost:3000",
|
||||
"discours.io",
|
||||
"new.discours.io"
|
||||
]
|
||||
|
||||
try:
|
||||
parsed = urlparse(uri)
|
||||
|
||||
# Проверяем схему
|
||||
if parsed.scheme not in ['http', 'https']:
|
||||
return False
|
||||
|
||||
# Проверяем домен
|
||||
if not any(domain in parsed.netloc for domain in allowed_domains):
|
||||
return False
|
||||
|
||||
# Проверяем на открытые редиректы
|
||||
if parsed.netloc != parsed.netloc.lower():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
```
|
||||
|
||||
## 🔍 Input Validation
|
||||
|
||||
### Request Validation
|
||||
```python
|
||||
from pydantic import BaseModel, EmailStr, validator
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
@validator('password')
|
||||
def validate_password(cls, v):
|
||||
if len(v) < 8:
|
||||
raise ValueError('Password too short')
|
||||
return v
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
name: str
|
||||
|
||||
@validator('name')
|
||||
def validate_name(cls, v):
|
||||
if len(v.strip()) < 2:
|
||||
raise ValueError('Name too short')
|
||||
# Защита от XSS
|
||||
if '<' in v or '>' in v:
|
||||
raise ValueError('Invalid characters in name')
|
||||
return v.strip()
|
||||
```
|
||||
|
||||
### SQL Injection Prevention
|
||||
```python
|
||||
# Используем ORM и параметризованные запросы
|
||||
from sqlalchemy import text
|
||||
|
||||
# ✅ Безопасно
|
||||
async def get_user_by_email(email: str):
|
||||
query = text("SELECT * FROM authors WHERE email = :email")
|
||||
result = await db.execute(query, {"email": email})
|
||||
return result.fetchone()
|
||||
|
||||
# ❌ Небезопасно
|
||||
async def unsafe_query(email: str):
|
||||
query = f"SELECT * FROM authors WHERE email = '{email}'" # SQL Injection!
|
||||
return await db.execute(query)
|
||||
```
|
||||
|
||||
## 🚨 Security Headers
|
||||
|
||||
### HTTP Security Headers
|
||||
```python
|
||||
def add_security_headers(response):
|
||||
"""Добавляет заголовки безопасности"""
|
||||
response.headers.update({
|
||||
# XSS Protection
|
||||
"X-XSS-Protection": "1; mode=block",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"X-Frame-Options": "DENY",
|
||||
|
||||
# HTTPS Enforcement
|
||||
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
||||
|
||||
# Content Security Policy
|
||||
"Content-Security-Policy": (
|
||||
"default-src 'self'; "
|
||||
"script-src 'self' 'unsafe-inline'; "
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
"img-src 'self' data: https:; "
|
||||
"connect-src 'self' https://api.discours.io"
|
||||
),
|
||||
|
||||
# Referrer Policy
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
|
||||
# Permissions Policy
|
||||
"Permissions-Policy": "geolocation=(), microphone=(), camera=()"
|
||||
})
|
||||
```
|
||||
|
||||
## 📊 Security Monitoring
|
||||
|
||||
### Audit Logging
|
||||
```python
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
async def log_security_event(
|
||||
event_type: str,
|
||||
user_id: str = None,
|
||||
ip_address: str = None,
|
||||
user_agent: str = None,
|
||||
success: bool = True,
|
||||
details: dict = None
|
||||
):
|
||||
"""Логирование событий безопасности"""
|
||||
|
||||
event = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"event_type": event_type,
|
||||
"user_id": user_id,
|
||||
"ip_address": ip_address,
|
||||
"user_agent": user_agent,
|
||||
"success": success,
|
||||
"details": details or {}
|
||||
}
|
||||
|
||||
# Логируем в файл аудита
|
||||
logger.info("security_event", extra=event)
|
||||
|
||||
# Отправляем критические события в SIEM
|
||||
if event_type in ["login_failed", "account_locked", "token_stolen"]:
|
||||
await send_to_siem(event)
|
||||
```
|
||||
|
||||
### Anomaly Detection
|
||||
```python
|
||||
from collections import defaultdict
|
||||
import asyncio
|
||||
|
||||
# Детектор аномалий
|
||||
anomaly_tracker = defaultdict(list)
|
||||
|
||||
async def detect_anomalies(user_id: str, event_type: str, ip_address: str):
|
||||
"""Детекция аномальной активности"""
|
||||
|
||||
current_time = time.time()
|
||||
user_events = anomaly_tracker[user_id]
|
||||
|
||||
# Добавляем событие
|
||||
user_events.append({
|
||||
"type": event_type,
|
||||
"ip": ip_address,
|
||||
"time": current_time
|
||||
})
|
||||
|
||||
# Очищаем старые события (последний час)
|
||||
user_events[:] = [
|
||||
event for event in user_events
|
||||
if current_time - event["time"] < 3600
|
||||
]
|
||||
|
||||
# Проверяем аномалии
|
||||
if len(user_events) > 50: # Слишком много событий
|
||||
await log_security_event(
|
||||
"anomaly_detected",
|
||||
user_id=user_id,
|
||||
details={"reason": "too_many_events", "count": len(user_events)}
|
||||
)
|
||||
|
||||
# Проверяем множественные IP
|
||||
unique_ips = set(event["ip"] for event in user_events)
|
||||
if len(unique_ips) > 5: # Слишком много IP адресов
|
||||
await log_security_event(
|
||||
"anomaly_detected",
|
||||
user_id=user_id,
|
||||
details={"reason": "multiple_ips", "ips": list(unique_ips)}
|
||||
)
|
||||
```
|
||||
|
||||
## 🔧 Security Configuration
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# JWT Security
|
||||
JWT_SECRET=your_super_secret_key_minimum_256_bits
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_EXPIRATION_HOURS=720
|
||||
|
||||
# Cookie Security
|
||||
SESSION_COOKIE_SECURE=true
|
||||
SESSION_COOKIE_HTTPONLY=true
|
||||
SESSION_COOKIE_SAMESITE=lax
|
||||
|
||||
# Rate Limiting
|
||||
RATE_LIMIT_ENABLED=true
|
||||
RATE_LIMIT_REQUESTS=100
|
||||
RATE_LIMIT_WINDOW=3600
|
||||
|
||||
# Security Features
|
||||
ACCOUNT_LOCKOUT_ENABLED=true
|
||||
MAX_LOGIN_ATTEMPTS=5
|
||||
LOCKOUT_DURATION=1800
|
||||
|
||||
# HTTPS Enforcement
|
||||
FORCE_HTTPS=true
|
||||
HSTS_MAX_AGE=31536000
|
||||
```
|
||||
|
||||
### Production Checklist
|
||||
|
||||
#### Authentication Security
|
||||
- [ ] JWT secret минимум 256 бит
|
||||
- [ ] Короткое время жизни токенов (≤ 30 дней)
|
||||
- [ ] httpOnly cookies включены
|
||||
- [ ] Secure cookies для HTTPS
|
||||
- [ ] SameSite cookies настроены
|
||||
|
||||
#### Password Security
|
||||
- [ ] bcrypt с rounds ≥ 12
|
||||
- [ ] Требования к сложности паролей
|
||||
- [ ] Защита от брутфорса
|
||||
- [ ] Account lockout настроен
|
||||
|
||||
#### OAuth Security
|
||||
- [ ] State parameter валидация
|
||||
- [ ] PKCE поддержка включена
|
||||
- [ ] Redirect URI валидация
|
||||
- [ ] Secure client secrets
|
||||
|
||||
#### Infrastructure Security
|
||||
- [ ] HTTPS принудительно
|
||||
- [ ] Security headers настроены
|
||||
- [ ] Rate limiting включен
|
||||
- [ ] Audit logging работает
|
||||
|
||||
#### Redis Security
|
||||
- [ ] TTL для всех ключей
|
||||
- [ ] Атомарные операции
|
||||
- [ ] Connection pooling
|
||||
- [ ] Health checks
|
||||
|
||||
## 🚨 Incident Response
|
||||
|
||||
### Security Incident Types
|
||||
1. **Token Compromise**: Подозрение на кражу токенов
|
||||
2. **Brute Force Attack**: Массовые попытки входа
|
||||
3. **Account Takeover**: Несанкционированный доступ
|
||||
4. **Data Breach**: Утечка данных
|
||||
5. **System Compromise**: Компрометация системы
|
||||
|
||||
### Response Procedures
|
||||
|
||||
#### Token Compromise
|
||||
```python
|
||||
async def handle_token_compromise(user_id: str, reason: str):
|
||||
"""Обработка компрометации токена"""
|
||||
|
||||
# 1. Отзываем все токены пользователя
|
||||
sessions = SessionTokenManager()
|
||||
revoked_count = await sessions.revoke_user_sessions(user_id)
|
||||
|
||||
# 2. Блокируем аккаунт
|
||||
author = await Author.get(user_id)
|
||||
author.account_locked_until = int(time.time()) + 3600 # 1 час
|
||||
await author.save()
|
||||
|
||||
# 3. Логируем инцидент
|
||||
await log_security_event(
|
||||
"token_compromise",
|
||||
user_id=user_id,
|
||||
details={
|
||||
"reason": reason,
|
||||
"revoked_tokens": revoked_count,
|
||||
"account_locked": True
|
||||
}
|
||||
)
|
||||
|
||||
# 4. Уведомляем пользователя
|
||||
await send_security_notification(user_id, "token_compromise")
|
||||
```
|
||||
|
||||
#### Brute Force Response
|
||||
```python
|
||||
async def handle_brute_force(ip_address: str, attempts: int):
|
||||
"""Обработка брутфорс атаки"""
|
||||
|
||||
# 1. Блокируем IP
|
||||
await block_ip_address(ip_address, duration=3600)
|
||||
|
||||
# 2. Логируем атаку
|
||||
await log_security_event(
|
||||
"brute_force_attack",
|
||||
ip_address=ip_address,
|
||||
details={"attempts": attempts}
|
||||
)
|
||||
|
||||
# 3. Уведомляем администраторов
|
||||
await notify_admins("brute_force_detected", {
|
||||
"ip": ip_address,
|
||||
"attempts": attempts
|
||||
})
|
||||
```
|
||||
|
||||
## 📚 Security Best Practices
|
||||
|
||||
### Development
|
||||
- **Secure by Default**: Безопасные настройки по умолчанию
|
||||
- **Fail Securely**: При ошибках блокируем доступ
|
||||
- **Defense in Depth**: Многоуровневая защита
|
||||
- **Principle of Least Privilege**: Минимальные права
|
||||
|
||||
### Operations
|
||||
- **Regular Updates**: Обновление зависимостей
|
||||
- **Security Monitoring**: Постоянный мониторинг
|
||||
- **Incident Response**: Готовность к инцидентам
|
||||
- **Regular Audits**: Регулярные аудиты безопасности
|
||||
|
||||
### Compliance
|
||||
- **GDPR**: Защита персональных данных
|
||||
- **OWASP**: Следование рекомендациям OWASP
|
||||
- **Security Standards**: Соответствие стандартам
|
||||
- **Documentation**: Документирование процедур
|
||||
502
docs/auth/sessions.md
Normal file
502
docs/auth/sessions.md
Normal file
@@ -0,0 +1,502 @@
|
||||
# 🔑 Управление сессиями
|
||||
|
||||
## 🎯 Обзор
|
||||
|
||||
Система управления сессиями на основе JWT токенов с Redis хранением для отзыва и мониторинга активности.
|
||||
|
||||
## 🏗️ Архитектура
|
||||
|
||||
### Принцип работы
|
||||
1. **JWT токены** с payload `{user_id, username, iat, exp}`
|
||||
2. **Redis хранение** для отзыва и управления жизненным циклом
|
||||
3. **Множественные сессии** на пользователя
|
||||
4. **Автоматическое обновление** `last_activity` при активности
|
||||
|
||||
### Redis структура
|
||||
```bash
|
||||
session:{user_id}:{token} # Hash: {user_id, username, device_info, last_activity}
|
||||
user_sessions:{user_id} # Set: {token1, token2, ...}
|
||||
```
|
||||
|
||||
### Извлечение токена (приоритет)
|
||||
1. Cookie `session_token` (httpOnly)
|
||||
2. Заголовок `Authorization: Bearer <token>`
|
||||
3. Заголовок `X-Session-Token`
|
||||
4. `scope["auth_token"]` (внутренний)
|
||||
|
||||
## 🔧 SessionTokenManager
|
||||
|
||||
### Основные методы
|
||||
|
||||
```python
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
|
||||
sessions = SessionTokenManager()
|
||||
|
||||
# Создание сессии
|
||||
token = await sessions.create_session(
|
||||
user_id="123",
|
||||
auth_data={"provider": "local"},
|
||||
username="john_doe",
|
||||
device_info={"ip": "192.168.1.1", "user_agent": "Mozilla/5.0"}
|
||||
)
|
||||
|
||||
# Создание JWT токена сессии
|
||||
token = await sessions.create_session_token(
|
||||
user_id="123",
|
||||
token_data={"username": "john_doe", "device_info": "..."}
|
||||
)
|
||||
|
||||
# Проверка сессии
|
||||
payload = await sessions.verify_session(token)
|
||||
# Возвращает: {"user_id": "123", "username": "john_doe", "iat": 1640995200, "exp": 1643587200}
|
||||
|
||||
# Валидация токена сессии
|
||||
valid, data = await sessions.validate_session_token(token)
|
||||
|
||||
# Получение данных сессии
|
||||
session_data = await sessions.get_session_data(token, user_id)
|
||||
|
||||
# Обновление сессии
|
||||
new_token = await sessions.refresh_session(user_id, old_token, device_info)
|
||||
|
||||
# Отзыв сессии
|
||||
await sessions.revoke_session_token(token)
|
||||
|
||||
# Отзыв всех сессий пользователя
|
||||
revoked_count = await sessions.revoke_user_sessions(user_id)
|
||||
|
||||
# Получение всех сессий пользователя
|
||||
user_sessions = await sessions.get_user_sessions(user_id)
|
||||
```
|
||||
|
||||
## 🍪 httpOnly Cookies
|
||||
|
||||
### Принципы работы
|
||||
|
||||
1. **Безопасное хранение**: Токены сессий хранятся в httpOnly cookies, недоступных для JavaScript
|
||||
2. **Автоматическая отправка**: Cookies автоматически отправляются с каждым запросом
|
||||
3. **Защита от XSS**: httpOnly cookies защищены от кражи через JavaScript
|
||||
4. **Двойная поддержка**: Система поддерживает как cookies, так и заголовок Authorization
|
||||
|
||||
### Конфигурация cookies
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
SESSION_COOKIE_NAME = "session_token"
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SECURE = True # для HTTPS
|
||||
SESSION_COOKIE_SAMESITE = "lax"
|
||||
SESSION_COOKIE_MAX_AGE = 30 * 24 * 60 * 60 # 30 дней
|
||||
```
|
||||
|
||||
### Установка cookies
|
||||
|
||||
```python
|
||||
# В AuthMiddleware
|
||||
def set_session_cookie(self, response: Response, token: str) -> None:
|
||||
"""Устанавливает httpOnly cookie с токеном сессии"""
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE_NAME,
|
||||
value=token,
|
||||
httponly=SESSION_COOKIE_HTTPONLY,
|
||||
secure=SESSION_COOKIE_SECURE,
|
||||
samesite=SESSION_COOKIE_SAMESITE,
|
||||
max_age=SESSION_COOKIE_MAX_AGE
|
||||
)
|
||||
```
|
||||
|
||||
## 🔍 Извлечение токенов
|
||||
|
||||
### Автоматическое извлечение
|
||||
|
||||
```python
|
||||
from auth.utils import extract_token_from_request, get_auth_token, get_safe_headers
|
||||
|
||||
# Простое извлечение из cookies/headers
|
||||
token = await extract_token_from_request(request)
|
||||
|
||||
# Расширенное извлечение с логированием
|
||||
token = await get_auth_token(request)
|
||||
|
||||
# Ручная проверка источников
|
||||
headers = get_safe_headers(request)
|
||||
token = headers.get("authorization", "").replace("Bearer ", "")
|
||||
|
||||
# Извлечение из GraphQL контекста
|
||||
from auth.utils import get_auth_token_from_context
|
||||
token = await get_auth_token_from_context(info)
|
||||
```
|
||||
|
||||
### Приоритет источников
|
||||
|
||||
Система проверяет токены в следующем порядке приоритета:
|
||||
|
||||
1. **httpOnly cookies** - основной источник для веб-приложений
|
||||
2. **Заголовок Authorization** - для API клиентов и мобильных приложений
|
||||
|
||||
```python
|
||||
# auth/utils.py
|
||||
async def extract_token_from_request(request) -> str | None:
|
||||
"""DRY функция для извлечения токена из request"""
|
||||
|
||||
# 1. Проверяем cookies
|
||||
if hasattr(request, "cookies") and request.cookies:
|
||||
token = request.cookies.get(SESSION_COOKIE_NAME)
|
||||
if token:
|
||||
return token
|
||||
|
||||
# 2. Проверяем заголовок Authorization
|
||||
headers = get_safe_headers(request)
|
||||
auth_header = headers.get("authorization", "")
|
||||
if auth_header and auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:].strip()
|
||||
return token
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
### Безопасное получение заголовков
|
||||
|
||||
```python
|
||||
# auth/utils.py
|
||||
def get_safe_headers(request: Any) -> dict[str, str]:
|
||||
"""Безопасно получает заголовки запроса"""
|
||||
headers = {}
|
||||
try:
|
||||
# Первый приоритет: scope из ASGI
|
||||
if hasattr(request, "scope") and isinstance(request.scope, dict):
|
||||
scope_headers = request.scope.get("headers", [])
|
||||
if scope_headers:
|
||||
headers.update({k.decode("utf-8").lower(): v.decode("utf-8")
|
||||
for k, v in scope_headers})
|
||||
|
||||
# Второй приоритет: метод headers() или атрибут headers
|
||||
if hasattr(request, "headers"):
|
||||
if callable(request.headers):
|
||||
h = request.headers()
|
||||
if h:
|
||||
headers.update({k.lower(): v for k, v in h.items()})
|
||||
else:
|
||||
h = request.headers
|
||||
if hasattr(h, "items") and callable(h.items):
|
||||
headers.update({k.lower(): v for k, v in h.items()})
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Ошибка при доступе к заголовкам: {e}")
|
||||
|
||||
return headers
|
||||
```
|
||||
|
||||
## 🔄 Жизненный цикл сессии
|
||||
|
||||
### Создание сессии
|
||||
|
||||
```python
|
||||
# auth/tokens/sessions.py
|
||||
async def create_session(author_id: int, email: str, **kwargs) -> str:
|
||||
"""Создает новую сессию для пользователя"""
|
||||
session_data = {
|
||||
"author_id": author_id,
|
||||
"email": email,
|
||||
"created_at": int(time.time()),
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Генерируем уникальный токен
|
||||
token = generate_session_token()
|
||||
|
||||
# Сохраняем в Redis
|
||||
await redis.execute(
|
||||
"SETEX",
|
||||
f"session:{token}",
|
||||
SESSION_TOKEN_LIFE_SPAN,
|
||||
json.dumps(session_data)
|
||||
)
|
||||
|
||||
return token
|
||||
```
|
||||
|
||||
### Верификация сессии
|
||||
|
||||
```python
|
||||
# auth/tokens/storage.py
|
||||
async def verify_session(token: str) -> dict | None:
|
||||
"""Верифицирует токен сессии"""
|
||||
if not token:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Получаем данные сессии из Redis
|
||||
session_data = await redis.execute("GET", f"session:{token}")
|
||||
if not session_data:
|
||||
return None
|
||||
|
||||
return json.loads(session_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка верификации сессии: {e}")
|
||||
return None
|
||||
```
|
||||
|
||||
### Обновление сессии
|
||||
|
||||
```python
|
||||
async def refresh_session(user_id: str, old_token: str, device_info: dict = None) -> str:
|
||||
"""Обновляет сессию пользователя"""
|
||||
|
||||
# Проверяем старую сессию
|
||||
old_payload = await verify_session(old_token)
|
||||
if not old_payload:
|
||||
raise InvalidTokenError("Invalid session token")
|
||||
|
||||
# Отзываем старый токен
|
||||
await revoke_session_token(old_token)
|
||||
|
||||
# Создаем новый токен
|
||||
new_token = await create_session(
|
||||
user_id=user_id,
|
||||
username=old_payload.get("username"),
|
||||
device_info=device_info or old_payload.get("device_info", {})
|
||||
)
|
||||
|
||||
return new_token
|
||||
```
|
||||
|
||||
### Удаление сессии
|
||||
|
||||
```python
|
||||
# auth/tokens/storage.py
|
||||
async def delete_session(token: str) -> bool:
|
||||
"""Удаляет сессию пользователя"""
|
||||
try:
|
||||
result = await redis.execute("DEL", f"session:{token}")
|
||||
return bool(result)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления сессии: {e}")
|
||||
return False
|
||||
```
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
### JWT токены
|
||||
- **Алгоритм**: HS256
|
||||
- **Secret**: Из переменной окружения JWT_SECRET
|
||||
- **Payload**: `{user_id, username, iat, exp}`
|
||||
- **Expiration**: 30 дней (настраивается)
|
||||
|
||||
### Redis security
|
||||
- **TTL** для всех токенов
|
||||
- **Атомарные операции** через pipelines
|
||||
- **SCAN** вместо KEYS для производительности
|
||||
- **Транзакции** для критических операций
|
||||
|
||||
### Защита от атак
|
||||
- **XSS**: httpOnly cookies недоступны для JavaScript
|
||||
- **CSRF**: SameSite cookies и CSRF токены
|
||||
- **Session Hijacking**: Secure cookies и регулярная ротация токенов
|
||||
- **Brute Force**: Ограничение попыток входа и блокировка аккаунтов
|
||||
|
||||
## 📊 Мониторинг сессий
|
||||
|
||||
### Статистика
|
||||
|
||||
```python
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
|
||||
monitoring = TokenMonitoring()
|
||||
|
||||
# Статистика токенов
|
||||
stats = await monitoring.get_token_statistics()
|
||||
print(f"Active sessions: {stats['session_tokens']}")
|
||||
print(f"Memory usage: {stats['memory_usage']} bytes")
|
||||
|
||||
# Health check
|
||||
health = await monitoring.health_check()
|
||||
if health["status"] == "healthy":
|
||||
print("Session system is healthy")
|
||||
```
|
||||
|
||||
### Логирование событий
|
||||
|
||||
```python
|
||||
# auth/middleware.py
|
||||
def log_auth_event(event_type: str, user_id: int | None = None,
|
||||
success: bool = True, **kwargs):
|
||||
"""Логирует события авторизации"""
|
||||
logger.info(
|
||||
"auth_event",
|
||||
event_type=event_type,
|
||||
user_id=user_id,
|
||||
success=success,
|
||||
ip_address=kwargs.get('ip'),
|
||||
user_agent=kwargs.get('user_agent'),
|
||||
**kwargs
|
||||
)
|
||||
```
|
||||
|
||||
### Метрики
|
||||
|
||||
```python
|
||||
# auth/middleware.py
|
||||
from prometheus_client import Counter, Histogram
|
||||
|
||||
# Счетчики
|
||||
login_attempts = Counter('auth_login_attempts_total', 'Number of login attempts', ['success'])
|
||||
session_creations = Counter('auth_sessions_created_total', 'Number of sessions created')
|
||||
session_deletions = Counter('auth_sessions_deleted_total', 'Number of sessions deleted')
|
||||
|
||||
# Гистограммы
|
||||
auth_duration = Histogram('auth_operation_duration_seconds', 'Time spent on auth operations', ['operation'])
|
||||
```
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
### Unit тесты
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(client: AsyncClient):
|
||||
"""Тест успешного входа"""
|
||||
response = await client.post("/auth/login", json={
|
||||
"email": "test@example.com",
|
||||
"password": "password123"
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert "token" in data
|
||||
|
||||
# Проверяем установку cookie
|
||||
cookies = response.cookies
|
||||
assert "session_token" in cookies
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_protected_endpoint_with_cookie(client: AsyncClient):
|
||||
"""Тест защищенного endpoint с cookie"""
|
||||
# Сначала входим в систему
|
||||
login_response = await client.post("/auth/login", json={
|
||||
"email": "test@example.com",
|
||||
"password": "password123"
|
||||
})
|
||||
|
||||
# Получаем cookie
|
||||
session_cookie = login_response.cookies.get("session_token")
|
||||
|
||||
# Делаем запрос к защищенному endpoint
|
||||
response = await client.get("/auth/session", cookies={
|
||||
"session_token": session_cookie
|
||||
})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user"]["email"] == "test@example.com"
|
||||
```
|
||||
|
||||
## 💡 Примеры использования
|
||||
|
||||
### 1. Вход в систему
|
||||
|
||||
```typescript
|
||||
// Frontend - React/SolidJS
|
||||
const handleLogin = async (email: string, password: string) => {
|
||||
try {
|
||||
const response = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
credentials: 'include', // Важно для cookies
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Cookie автоматически установится браузером
|
||||
// Перенаправляем на главную страницу
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
const error = await response.json();
|
||||
console.error('Login failed:', error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Проверка авторизации
|
||||
|
||||
```typescript
|
||||
// Frontend - проверка текущей сессии
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const response = await fetch('/auth/session', {
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.user) {
|
||||
// Пользователь авторизован
|
||||
setUser(data.user);
|
||||
setIsAuthenticated(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Защищенный API endpoint
|
||||
|
||||
```python
|
||||
# Backend - Python
|
||||
from auth.decorators import login_required, require_permission
|
||||
|
||||
@login_required
|
||||
@require_permission("shout:create")
|
||||
async def create_shout(info, input_data):
|
||||
"""Создание публикации с проверкой прав"""
|
||||
user = info.context.get('user')
|
||||
|
||||
# Создаем публикацию
|
||||
shout = Shout(
|
||||
title=input_data['title'],
|
||||
content=input_data['content'],
|
||||
author_id=user.id
|
||||
)
|
||||
|
||||
db.add(shout)
|
||||
db.commit()
|
||||
|
||||
return shout
|
||||
```
|
||||
|
||||
### 4. Выход из системы
|
||||
|
||||
```typescript
|
||||
// Frontend - выход
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch('/auth/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
// Очищаем локальное состояние
|
||||
setUser(null);
|
||||
setIsAuthenticated(false);
|
||||
|
||||
// Перенаправляем на страницу входа
|
||||
window.location.href = '/login';
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
373
docs/auth/system.md
Normal file
373
docs/auth/system.md
Normal file
@@ -0,0 +1,373 @@
|
||||
# Система авторизации Discours Core
|
||||
|
||||
## 🎯 Обзор архитектуры
|
||||
|
||||
Модульная система авторизации с JWT токенами, Redis-сессиями и OAuth интеграцией. Построена на принципах разделения ответственности и высокой производительности.
|
||||
|
||||
```
|
||||
auth/
|
||||
├── tokens/ # 🎯 Система управления токенами
|
||||
│ ├── sessions.py # JWT сессии с Redis
|
||||
│ ├── verification.py # Одноразовые токены
|
||||
│ ├── oauth.py # OAuth токены
|
||||
│ ├── batch.py # Массовые операции
|
||||
│ ├── monitoring.py # Мониторинг и статистика
|
||||
│ ├── storage.py # Фасад для совместимости
|
||||
│ ├── base.py # Базовые классы
|
||||
│ └── types.py # Типы и константы
|
||||
├── middleware.py # HTTP middleware
|
||||
├── decorators.py # GraphQL декораторы
|
||||
├── oauth.py # OAuth провайдеры
|
||||
├── identity.py # Методы идентификации
|
||||
├── jwtcodec.py # JWT кодек
|
||||
├── validations.py # Валидация данных
|
||||
├── credentials.py # Креденшиалы
|
||||
├── exceptions.py # Исключения
|
||||
└── utils.py # Утилиты
|
||||
```
|
||||
|
||||
## 🎯 Система токенов
|
||||
|
||||
### SessionTokenManager
|
||||
|
||||
**Принцип работы:**
|
||||
1. JWT токены с payload `{user_id, username, iat, exp}`
|
||||
2. Redis хранение для отзыва и управления жизненным циклом
|
||||
3. Поддержка множественных сессий на пользователя
|
||||
4. Автоматическое обновление `last_activity` при активности
|
||||
|
||||
**Redis структура:**
|
||||
```bash
|
||||
session:{user_id}:{token} # hash с данными сессии
|
||||
user_sessions:{user_id} # set с активными токенами
|
||||
```
|
||||
|
||||
**Основные методы:**
|
||||
```python
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
|
||||
sessions = SessionTokenManager()
|
||||
|
||||
# Создание сессии
|
||||
token = await sessions.create_session(user_id, username=username)
|
||||
|
||||
# Проверка сессии
|
||||
payload = await sessions.verify_session(token)
|
||||
|
||||
# Обновление сессии
|
||||
new_token = await sessions.refresh_session(user_id, old_token)
|
||||
|
||||
# Отзыв сессии
|
||||
await sessions.revoke_session_token(token)
|
||||
|
||||
# Получение всех сессий пользователя
|
||||
user_sessions = await sessions.get_user_sessions(user_id)
|
||||
```
|
||||
|
||||
### Типы токенов
|
||||
|
||||
| Тип | TTL | Назначение | Менеджер |
|
||||
|-----|-----|------------|----------|
|
||||
| `session` | 30 дней | JWT сессии пользователей | `SessionTokenManager` |
|
||||
| `verification` | 1 час | Одноразовые токены подтверждения | `VerificationTokenManager` |
|
||||
| `oauth_access` | 1 час | OAuth access токены | `OAuthTokenManager` |
|
||||
| `oauth_refresh` | 30 дней | OAuth refresh токены | `OAuthTokenManager` |
|
||||
|
||||
### Менеджеры токенов
|
||||
|
||||
#### 1. **SessionTokenManager** - JWT сессии
|
||||
```python
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
|
||||
sessions = SessionTokenManager()
|
||||
|
||||
# Создание сессии
|
||||
token = await sessions.create_session(
|
||||
user_id="123",
|
||||
auth_data={"provider": "local"},
|
||||
username="john_doe",
|
||||
device_info={"ip": "192.168.1.1", "user_agent": "Mozilla/5.0"}
|
||||
)
|
||||
|
||||
# Создание JWT токена сессии
|
||||
token = await sessions.create_session_token(
|
||||
user_id="123",
|
||||
token_data={"username": "john_doe", "device_info": "..."}
|
||||
)
|
||||
|
||||
# Проверка сессии (совместимость с TokenStorage)
|
||||
payload = await sessions.verify_session(token)
|
||||
# Возвращает: {"user_id": "123", "username": "john_doe", "iat": 1640995200, "exp": 1643587200}
|
||||
|
||||
# Валидация токена сессии
|
||||
valid, data = await sessions.validate_session_token(token)
|
||||
|
||||
# Получение данных сессии
|
||||
session_data = await sessions.get_session_data(token, user_id)
|
||||
|
||||
# Обновление сессии
|
||||
new_token = await sessions.refresh_session(user_id, old_token, device_info)
|
||||
|
||||
# Отзыв сессии
|
||||
await sessions.revoke_session_token(token)
|
||||
|
||||
# Отзыв всех сессий пользователя
|
||||
revoked_count = await sessions.revoke_user_sessions(user_id)
|
||||
|
||||
# Получение всех сессий пользователя
|
||||
user_sessions = await sessions.get_user_sessions(user_id)
|
||||
```
|
||||
|
||||
#### 2. **VerificationTokenManager** - Одноразовые токены
|
||||
```python
|
||||
from auth.tokens.verification import VerificationTokenManager
|
||||
|
||||
verification = VerificationTokenManager()
|
||||
|
||||
# Создание токена подтверждения email
|
||||
token = await verification.create_verification_token(
|
||||
user_id="123",
|
||||
verification_type="email_change",
|
||||
data={"new_email": "new@example.com"},
|
||||
ttl=3600 # 1 час
|
||||
)
|
||||
|
||||
# Проверка токена
|
||||
valid, data = await verification.validate_verification_token(token)
|
||||
|
||||
# Подтверждение (одноразовое использование)
|
||||
confirmed_data = await verification.confirm_verification_token(token)
|
||||
```
|
||||
|
||||
#### 3. **OAuthTokenManager** - OAuth токены
|
||||
```python
|
||||
from auth.tokens.oauth import OAuthTokenManager
|
||||
|
||||
oauth = OAuthTokenManager()
|
||||
|
||||
# Сохранение OAuth токенов
|
||||
await oauth.store_oauth_tokens(
|
||||
user_id="123",
|
||||
provider="google",
|
||||
access_token="ya29.a0AfH6SM...",
|
||||
refresh_token="1//04...",
|
||||
expires_in=3600,
|
||||
additional_data={"scope": "read write"}
|
||||
)
|
||||
|
||||
# Создание OAuth токена (внутренний метод)
|
||||
token_key = await oauth._create_oauth_token(
|
||||
user_id="123",
|
||||
token_data={"token": "ya29.a0AfH6SM...", "provider": "google"},
|
||||
ttl=3600,
|
||||
provider="google",
|
||||
token_type="oauth_access"
|
||||
)
|
||||
|
||||
# Получение access токена
|
||||
access_data = await oauth.get_token(user_id, "google", "oauth_access")
|
||||
|
||||
# Оптимизированное получение OAuth данных
|
||||
oauth_data = await oauth._get_oauth_data_optimized("oauth_access", "123", "google")
|
||||
|
||||
# Отзыв OAuth токенов
|
||||
await oauth.revoke_oauth_tokens(user_id, "google")
|
||||
|
||||
# Оптимизированный отзыв токена
|
||||
revoked = await oauth._revoke_oauth_token_optimized("oauth_access", "123", "google")
|
||||
|
||||
# Отзыв всех OAuth токенов пользователя
|
||||
revoked_count = await oauth.revoke_user_oauth_tokens(user_id, "oauth_access")
|
||||
```
|
||||
|
||||
#### 4. **BatchTokenOperations** - Массовые операции
|
||||
```python
|
||||
from auth.tokens.batch import BatchTokenOperations
|
||||
|
||||
batch = BatchTokenOperations()
|
||||
|
||||
# Массовая проверка токенов
|
||||
tokens = ["token1", "token2", "token3"]
|
||||
results = await batch.batch_validate_tokens(tokens)
|
||||
# {"token1": True, "token2": False, "token3": True}
|
||||
|
||||
# Валидация батча токенов (внутренний метод)
|
||||
batch_results = await batch._validate_token_batch(tokens)
|
||||
|
||||
# Безопасное декодирование токена
|
||||
payload = await batch._safe_decode_token(token)
|
||||
|
||||
# Массовый отзыв токенов
|
||||
revoked_count = await batch.batch_revoke_tokens(tokens)
|
||||
|
||||
# Отзыв батча токенов (внутренний метод)
|
||||
batch_revoked = await batch._revoke_token_batch(tokens)
|
||||
|
||||
# Очистка истекших токенов
|
||||
cleaned_count = await batch.cleanup_expired_tokens()
|
||||
```
|
||||
|
||||
#### 5. **TokenMonitoring** - Мониторинг
|
||||
```python
|
||||
from auth.tokens.monitoring import TokenMonitoring
|
||||
|
||||
monitoring = TokenMonitoring()
|
||||
|
||||
# Статистика токенов
|
||||
stats = await monitoring.get_token_statistics()
|
||||
# {
|
||||
# "session_tokens": 150,
|
||||
# "verification_tokens": 5,
|
||||
# "oauth_access_tokens": 25,
|
||||
# "oauth_refresh_tokens": 25,
|
||||
# "memory_usage": 1048576
|
||||
# }
|
||||
|
||||
# Подсчет ключей по паттерну (внутренний метод)
|
||||
count = await monitoring._count_keys_by_pattern("session:*")
|
||||
|
||||
# Health check
|
||||
health = await monitoring.health_check()
|
||||
# {"status": "healthy", "redis_connected": True, "token_count": 205}
|
||||
|
||||
# Оптимизация памяти
|
||||
optimization = await monitoring.optimize_memory_usage()
|
||||
# {"cleaned_expired": 10, "memory_freed": 102400}
|
||||
|
||||
# Оптимизация структур данных (внутренний метод)
|
||||
optimized = await monitoring._optimize_data_structures()
|
||||
```
|
||||
|
||||
### TokenStorage (Фасад для совместимости)
|
||||
```python
|
||||
from auth.tokens.storage import TokenStorage
|
||||
|
||||
# Упрощенный API для основных операций
|
||||
await TokenStorage.create_session(user_id, username=username)
|
||||
await TokenStorage.verify_session(token)
|
||||
await TokenStorage.refresh_session(user_id, old_token, device_info)
|
||||
await TokenStorage.revoke_session(token)
|
||||
```
|
||||
|
||||
## 🔧 Middleware и декораторы
|
||||
|
||||
### AuthMiddleware
|
||||
```python
|
||||
from auth.middleware import AuthMiddleware
|
||||
|
||||
# Автоматическая обработка токенов
|
||||
middleware = AuthMiddleware()
|
||||
|
||||
# Извлечение токена из запроса
|
||||
token = await extract_token_from_request(request)
|
||||
|
||||
# Проверка сессии
|
||||
payload = await sessions.verify_session(token)
|
||||
```
|
||||
|
||||
### GraphQL декораторы
|
||||
```python
|
||||
from auth.decorators import auth_required, permission_required
|
||||
|
||||
@auth_required
|
||||
async def protected_resolver(info, **kwargs):
|
||||
"""Требует авторизации"""
|
||||
user = info.context.get('user')
|
||||
return f"Hello, {user.username}!"
|
||||
|
||||
@permission_required("shout:create")
|
||||
async def create_shout(info, input_data):
|
||||
"""Требует права на создание публикаций"""
|
||||
pass
|
||||
```
|
||||
|
||||
## ORM модели
|
||||
|
||||
### Author (Пользователь)
|
||||
```python
|
||||
class Author:
|
||||
id: int
|
||||
email: str
|
||||
name: str
|
||||
slug: str
|
||||
password: Optional[str] # bcrypt hash
|
||||
pic: Optional[str] # URL аватара
|
||||
bio: Optional[str]
|
||||
email_verified: bool
|
||||
phone_verified: bool
|
||||
created_at: int
|
||||
updated_at: int
|
||||
last_seen: int
|
||||
|
||||
# OAuth данные в JSON формате
|
||||
oauth: Optional[dict] # {"google": {"id": "123", "email": "user@gmail.com"}}
|
||||
|
||||
# Поля аутентификации
|
||||
failed_login_attempts: int
|
||||
account_locked_until: Optional[int]
|
||||
```
|
||||
|
||||
### OAuth данные
|
||||
OAuth данные хранятся в JSON поле `oauth` модели `Author`:
|
||||
```python
|
||||
# Формат oauth поля
|
||||
{
|
||||
"google": {
|
||||
"id": "123456789",
|
||||
"email": "user@gmail.com",
|
||||
"name": "John Doe"
|
||||
},
|
||||
"github": {
|
||||
"id": "456789",
|
||||
"login": "johndoe",
|
||||
"email": "user@github.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ⚙️ Конфигурация
|
||||
|
||||
### Переменные окружения
|
||||
```bash
|
||||
# JWT настройки
|
||||
JWT_SECRET=your_super_secret_key
|
||||
JWT_EXPIRATION_HOURS=720 # 30 дней
|
||||
|
||||
# Redis подключение
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# OAuth провайдеры
|
||||
GOOGLE_CLIENT_ID=your_google_client_id
|
||||
GOOGLE_CLIENT_SECRET=your_google_client_secret
|
||||
GITHUB_CLIENT_ID=your_github_client_id
|
||||
GITHUB_CLIENT_SECRET=your_github_client_secret
|
||||
FACEBOOK_APP_ID=your_facebook_app_id
|
||||
FACEBOOK_APP_SECRET=your_facebook_app_secret
|
||||
VK_APP_ID=your_vk_app_id
|
||||
VK_APP_SECRET=your_vk_app_secret
|
||||
YANDEX_CLIENT_ID=your_yandex_client_id
|
||||
YANDEX_CLIENT_SECRET=your_yandex_client_secret
|
||||
|
||||
# Session cookies
|
||||
SESSION_COOKIE_NAME=session_token
|
||||
SESSION_COOKIE_SECURE=true
|
||||
SESSION_COOKIE_HTTPONLY=true
|
||||
SESSION_COOKIE_SAMESITE=lax
|
||||
SESSION_COOKIE_MAX_AGE=2592000 # 30 дней
|
||||
|
||||
# Frontend
|
||||
FRONTEND_URL=https://yourdomain.com
|
||||
```
|
||||
|
||||
## Производительность
|
||||
|
||||
### Оптимизации Redis
|
||||
- **Pipeline операции** для атомарности
|
||||
- **Batch обработка** токенов (100-1000 за раз)
|
||||
- **SCAN** вместо KEYS для безопасности
|
||||
- **TTL** автоматическая очистка
|
||||
|
||||
### Кэширование
|
||||
- **@lru_cache** для часто используемых ключей
|
||||
- **Connection pooling** для Redis
|
||||
- **JWT decode caching** в middleware
|
||||
845
docs/auth/testing.md
Normal file
845
docs/auth/testing.md
Normal file
@@ -0,0 +1,845 @@
|
||||
# 🧪 Тестирование системы аутентификации
|
||||
|
||||
## 🎯 Обзор
|
||||
|
||||
Комплексная стратегия тестирования системы аутентификации с unit, integration и E2E тестами.
|
||||
|
||||
## 🏗️ Структура тестов
|
||||
|
||||
```
|
||||
tests/auth/
|
||||
├── unit/
|
||||
│ ├── test_session_manager.py
|
||||
│ ├── test_oauth_manager.py
|
||||
│ ├── test_batch_operations.py
|
||||
│ ├── test_monitoring.py
|
||||
│ └── test_utils.py
|
||||
├── integration/
|
||||
│ ├── test_redis_integration.py
|
||||
│ ├── test_oauth_flow.py
|
||||
│ ├── test_middleware.py
|
||||
│ └── test_decorators.py
|
||||
├── e2e/
|
||||
│ ├── test_login_flow.py
|
||||
│ ├── test_oauth_flow.py
|
||||
│ └── test_session_management.py
|
||||
└── fixtures/
|
||||
├── auth_fixtures.py
|
||||
├── redis_fixtures.py
|
||||
└── oauth_fixtures.py
|
||||
```
|
||||
|
||||
## 🔧 Unit Tests
|
||||
|
||||
### SessionTokenManager Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
|
||||
class TestSessionTokenManager:
|
||||
|
||||
@pytest.fixture
|
||||
def session_manager(self):
|
||||
return SessionTokenManager()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_session(self, session_manager):
|
||||
"""Тест создания сессии"""
|
||||
with patch('auth.tokens.sessions.redis') as mock_redis:
|
||||
mock_redis.hset = AsyncMock()
|
||||
mock_redis.sadd = AsyncMock()
|
||||
mock_redis.expire = AsyncMock()
|
||||
|
||||
token = await session_manager.create_session(
|
||||
user_id="123",
|
||||
username="testuser"
|
||||
)
|
||||
|
||||
assert token is not None
|
||||
assert len(token) > 20
|
||||
mock_redis.hset.assert_called()
|
||||
mock_redis.sadd.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_session_valid(self, session_manager):
|
||||
"""Тест проверки валидной сессии"""
|
||||
with patch('auth.jwtcodec.decode_jwt') as mock_decode:
|
||||
mock_decode.return_value = {
|
||||
"user_id": "123",
|
||||
"username": "testuser",
|
||||
"exp": int(time.time()) + 3600
|
||||
}
|
||||
|
||||
with patch('auth.tokens.sessions.redis') as mock_redis:
|
||||
mock_redis.exists.return_value = True
|
||||
|
||||
payload = await session_manager.verify_session("valid_token")
|
||||
|
||||
assert payload is not None
|
||||
assert payload["user_id"] == "123"
|
||||
assert payload["username"] == "testuser"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_session_invalid(self, session_manager):
|
||||
"""Тест проверки невалидной сессии"""
|
||||
with patch('auth.jwtcodec.decode_jwt') as mock_decode:
|
||||
mock_decode.return_value = None
|
||||
|
||||
payload = await session_manager.verify_session("invalid_token")
|
||||
|
||||
assert payload is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_session_token(self, session_manager):
|
||||
"""Тест отзыва токена сессии"""
|
||||
with patch('auth.tokens.sessions.redis') as mock_redis:
|
||||
mock_redis.delete = AsyncMock(return_value=1)
|
||||
mock_redis.srem = AsyncMock()
|
||||
|
||||
result = await session_manager.revoke_session_token("test_token")
|
||||
|
||||
assert result is True
|
||||
mock_redis.delete.assert_called()
|
||||
mock_redis.srem.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_sessions(self, session_manager):
|
||||
"""Тест получения сессий пользователя"""
|
||||
with patch('auth.tokens.sessions.redis') as mock_redis:
|
||||
mock_redis.smembers.return_value = {b"token1", b"token2"}
|
||||
mock_redis.hgetall.return_value = {
|
||||
b"user_id": b"123",
|
||||
b"username": b"testuser",
|
||||
b"last_activity": b"1640995200"
|
||||
}
|
||||
|
||||
sessions = await session_manager.get_user_sessions("123")
|
||||
|
||||
assert len(sessions) == 2
|
||||
assert sessions[0]["token"] == "token1"
|
||||
assert sessions[0]["user_id"] == "123"
|
||||
```
|
||||
|
||||
### OAuthTokenManager Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from auth.tokens.oauth import OAuthTokenManager
|
||||
|
||||
class TestOAuthTokenManager:
|
||||
|
||||
@pytest.fixture
|
||||
def oauth_manager(self):
|
||||
return OAuthTokenManager()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_store_oauth_tokens(self, oauth_manager):
|
||||
"""Тест сохранения OAuth токенов"""
|
||||
with patch('auth.tokens.oauth.redis') as mock_redis:
|
||||
mock_redis.setex = AsyncMock()
|
||||
|
||||
await oauth_manager.store_oauth_tokens(
|
||||
user_id="123",
|
||||
provider="google",
|
||||
access_token="access_token_123",
|
||||
refresh_token="refresh_token_123",
|
||||
expires_in=3600
|
||||
)
|
||||
|
||||
# Проверяем, что токены сохранены
|
||||
assert mock_redis.setex.call_count == 2 # access + refresh
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_token(self, oauth_manager):
|
||||
"""Тест получения OAuth токена"""
|
||||
with patch('auth.tokens.oauth.redis') as mock_redis:
|
||||
mock_redis.get.return_value = b'{"token": "access_token_123", "expires_in": 3600}'
|
||||
|
||||
token_data = await oauth_manager.get_token("123", "google", "oauth_access")
|
||||
|
||||
assert token_data is not None
|
||||
assert token_data["token"] == "access_token_123"
|
||||
assert token_data["expires_in"] == 3600
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_revoke_oauth_tokens(self, oauth_manager):
|
||||
"""Тест отзыва OAuth токенов"""
|
||||
with patch('auth.tokens.oauth.redis') as mock_redis:
|
||||
mock_redis.delete = AsyncMock(return_value=2)
|
||||
|
||||
result = await oauth_manager.revoke_oauth_tokens("123", "google")
|
||||
|
||||
assert result is True
|
||||
mock_redis.delete.assert_called()
|
||||
```
|
||||
|
||||
### BatchTokenOperations Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from auth.tokens.batch import BatchTokenOperations
|
||||
|
||||
class TestBatchTokenOperations:
|
||||
|
||||
@pytest.fixture
|
||||
def batch_operations(self):
|
||||
return BatchTokenOperations()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_validate_tokens(self, batch_operations):
|
||||
"""Тест массовой валидации токенов"""
|
||||
tokens = ["token1", "token2", "token3"]
|
||||
|
||||
with patch.object(batch_operations, '_validate_token_batch') as mock_validate:
|
||||
mock_validate.return_value = {
|
||||
"token1": True,
|
||||
"token2": False,
|
||||
"token3": True
|
||||
}
|
||||
|
||||
results = await batch_operations.batch_validate_tokens(tokens)
|
||||
|
||||
assert results["token1"] is True
|
||||
assert results["token2"] is False
|
||||
assert results["token3"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_revoke_tokens(self, batch_operations):
|
||||
"""Тест массового отзыва токенов"""
|
||||
tokens = ["token1", "token2", "token3"]
|
||||
|
||||
with patch.object(batch_operations, '_revoke_token_batch') as mock_revoke:
|
||||
mock_revoke.return_value = 2 # 2 токена отозваны
|
||||
|
||||
revoked_count = await batch_operations.batch_revoke_tokens(tokens)
|
||||
|
||||
assert revoked_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleanup_expired_tokens(self, batch_operations):
|
||||
"""Тест очистки истекших токенов"""
|
||||
with patch('auth.tokens.batch.redis') as mock_redis:
|
||||
# Мокаем поиск истекших токенов
|
||||
mock_redis.scan_iter.return_value = [
|
||||
"session:123:expired_token1",
|
||||
"session:456:expired_token2"
|
||||
]
|
||||
mock_redis.ttl.return_value = -1 # Истекший токен
|
||||
mock_redis.delete = AsyncMock(return_value=1)
|
||||
|
||||
cleaned_count = await batch_operations.cleanup_expired_tokens()
|
||||
|
||||
assert cleaned_count >= 0
|
||||
```
|
||||
|
||||
## 🔗 Integration Tests
|
||||
|
||||
### Redis Integration Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import asyncio
|
||||
from storage.redis import redis
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
|
||||
class TestRedisIntegration:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redis_connection(self):
|
||||
"""Тест подключения к Redis"""
|
||||
result = await redis.ping()
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_lifecycle(self):
|
||||
"""Тест полного жизненного цикла сессии"""
|
||||
sessions = SessionTokenManager()
|
||||
|
||||
# Создаем сессию
|
||||
token = await sessions.create_session(
|
||||
user_id="test_user",
|
||||
username="testuser"
|
||||
)
|
||||
|
||||
assert token is not None
|
||||
|
||||
# Проверяем сессию
|
||||
payload = await sessions.verify_session(token)
|
||||
assert payload is not None
|
||||
assert payload["user_id"] == "test_user"
|
||||
|
||||
# Получаем сессии пользователя
|
||||
user_sessions = await sessions.get_user_sessions("test_user")
|
||||
assert len(user_sessions) >= 1
|
||||
|
||||
# Отзываем сессию
|
||||
revoked = await sessions.revoke_session_token(token)
|
||||
assert revoked is True
|
||||
|
||||
# Проверяем, что сессия отозвана
|
||||
payload = await sessions.verify_session(token)
|
||||
assert payload is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_concurrent_sessions(self):
|
||||
"""Тест множественных сессий"""
|
||||
sessions = SessionTokenManager()
|
||||
|
||||
# Создаем несколько сессий одновременно
|
||||
tasks = []
|
||||
for i in range(5):
|
||||
task = sessions.create_session(
|
||||
user_id="concurrent_user",
|
||||
username=f"user_{i}"
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
tokens = await asyncio.gather(*tasks)
|
||||
|
||||
# Проверяем, что все токены созданы
|
||||
assert len(tokens) == 5
|
||||
assert all(token is not None for token in tokens)
|
||||
|
||||
# Проверяем, что все сессии валидны
|
||||
for token in tokens:
|
||||
payload = await sessions.verify_session(token)
|
||||
assert payload is not None
|
||||
|
||||
# Очищаем тестовые данные
|
||||
for token in tokens:
|
||||
await sessions.revoke_session_token(token)
|
||||
```
|
||||
|
||||
### OAuth Flow Integration Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from auth.oauth import oauth_login_http, oauth_callback_http
|
||||
|
||||
class TestOAuthIntegration:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_state_flow(self):
|
||||
"""Тест OAuth state flow"""
|
||||
from auth.oauth import store_oauth_state, get_oauth_state
|
||||
|
||||
# Сохраняем state
|
||||
state = "test_state_123"
|
||||
redirect_uri = "http://localhost:3000"
|
||||
|
||||
await store_oauth_state(state, redirect_uri)
|
||||
|
||||
# Получаем state
|
||||
stored_data = await get_oauth_state(state)
|
||||
|
||||
assert stored_data is not None
|
||||
assert stored_data["redirect_uri"] == redirect_uri
|
||||
|
||||
# Проверяем, что state удален после использования
|
||||
stored_data_again = await get_oauth_state(state)
|
||||
assert stored_data_again is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_login_redirect(self):
|
||||
"""Тест OAuth login redirect"""
|
||||
mock_request = AsyncMock()
|
||||
mock_request.query_params = {
|
||||
"provider": "google",
|
||||
"state": "test_state",
|
||||
"redirect_uri": "http://localhost:3000"
|
||||
}
|
||||
|
||||
with patch('auth.oauth.store_oauth_state') as mock_store:
|
||||
with patch('auth.oauth.generate_provider_url') as mock_generate:
|
||||
mock_generate.return_value = "https://accounts.google.com/oauth/authorize?..."
|
||||
|
||||
response = await oauth_login_http(mock_request)
|
||||
|
||||
assert response.status_code == 307 # Redirect
|
||||
mock_store.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_callback_success(self):
|
||||
"""Тест успешного OAuth callback"""
|
||||
mock_request = AsyncMock()
|
||||
mock_request.query_params = {
|
||||
"code": "auth_code_123",
|
||||
"state": "test_state"
|
||||
}
|
||||
|
||||
with patch('auth.oauth.get_oauth_state') as mock_get_state:
|
||||
mock_get_state.return_value = {
|
||||
"redirect_uri": "http://localhost:3000"
|
||||
}
|
||||
|
||||
with patch('auth.oauth.exchange_code_for_user_data') as mock_exchange:
|
||||
mock_exchange.return_value = {
|
||||
"id": "123",
|
||||
"email": "test@example.com",
|
||||
"name": "Test User"
|
||||
}
|
||||
|
||||
with patch('auth.oauth._create_or_update_user') as mock_create_user:
|
||||
mock_create_user.return_value = AsyncMock(id=123)
|
||||
|
||||
response = await oauth_callback_http(mock_request)
|
||||
|
||||
assert response.status_code == 307 # Redirect
|
||||
assert "access_token=" in response.headers["location"]
|
||||
```
|
||||
|
||||
## 🌐 E2E Tests
|
||||
|
||||
### Login Flow E2E Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from main import app
|
||||
|
||||
class TestLoginFlowE2E:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_login_flow(self):
|
||||
"""Тест полного flow входа в систему"""
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
|
||||
# 1. Регистрация пользователя
|
||||
register_response = await client.post("/auth/register", json={
|
||||
"email": "test@example.com",
|
||||
"password": "TestPassword123!",
|
||||
"name": "Test User"
|
||||
})
|
||||
|
||||
assert register_response.status_code == 200
|
||||
|
||||
# 2. Вход в систему
|
||||
login_response = await client.post("/auth/login", json={
|
||||
"email": "test@example.com",
|
||||
"password": "TestPassword123!"
|
||||
})
|
||||
|
||||
assert login_response.status_code == 200
|
||||
data = login_response.json()
|
||||
assert data["success"] is True
|
||||
assert "token" in data
|
||||
|
||||
# Проверяем установку cookie
|
||||
cookies = login_response.cookies
|
||||
assert "session_token" in cookies
|
||||
|
||||
# 3. Проверка защищенного endpoint с cookie
|
||||
session_response = await client.get("/auth/session", cookies={
|
||||
"session_token": cookies["session_token"]
|
||||
})
|
||||
|
||||
assert session_response.status_code == 200
|
||||
session_data = session_response.json()
|
||||
assert session_data["user"]["email"] == "test@example.com"
|
||||
|
||||
# 4. Выход из системы
|
||||
logout_response = await client.post("/auth/logout", cookies={
|
||||
"session_token": cookies["session_token"]
|
||||
})
|
||||
|
||||
assert logout_response.status_code == 200
|
||||
|
||||
# 5. Проверка, что сессия недоступна после выхода
|
||||
invalid_session_response = await client.get("/auth/session", cookies={
|
||||
"session_token": cookies["session_token"]
|
||||
})
|
||||
|
||||
assert invalid_session_response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bearer_token_auth(self):
|
||||
"""Тест аутентификации через Bearer token"""
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
|
||||
# Вход в систему
|
||||
login_response = await client.post("/auth/login", json={
|
||||
"email": "test@example.com",
|
||||
"password": "TestPassword123!"
|
||||
})
|
||||
|
||||
token = login_response.json()["token"]
|
||||
|
||||
# Использование Bearer token
|
||||
protected_response = await client.get("/auth/session", headers={
|
||||
"Authorization": f"Bearer {token}"
|
||||
})
|
||||
|
||||
assert protected_response.status_code == 200
|
||||
data = protected_response.json()
|
||||
assert data["user"]["email"] == "test@example.com"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_credentials(self):
|
||||
"""Тест входа с неверными данными"""
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
|
||||
response = await client.post("/auth/login", json={
|
||||
"email": "test@example.com",
|
||||
"password": "WrongPassword"
|
||||
})
|
||||
|
||||
assert response.status_code == 401
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert "error" in data
|
||||
```
|
||||
|
||||
### OAuth E2E Tests
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from httpx import AsyncClient
|
||||
from main import app
|
||||
|
||||
class TestOAuthFlowE2E:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oauth_google_flow(self):
|
||||
"""Тест OAuth flow с Google"""
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
|
||||
# 1. Инициация OAuth
|
||||
oauth_response = await client.get(
|
||||
"/auth/oauth/google",
|
||||
params={
|
||||
"state": "test_state_123",
|
||||
"redirect_uri": "http://localhost:3000"
|
||||
},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
assert oauth_response.status_code == 307
|
||||
assert "accounts.google.com" in oauth_response.headers["location"]
|
||||
|
||||
# 2. Мокаем OAuth callback
|
||||
with patch('auth.oauth.exchange_code_for_user_data') as mock_exchange:
|
||||
mock_exchange.return_value = {
|
||||
"id": "google_user_123",
|
||||
"email": "user@gmail.com",
|
||||
"name": "Google User"
|
||||
}
|
||||
|
||||
callback_response = await client.get(
|
||||
"/auth/oauth/google/callback",
|
||||
params={
|
||||
"code": "auth_code_123",
|
||||
"state": "test_state_123"
|
||||
},
|
||||
follow_redirects=False
|
||||
)
|
||||
|
||||
assert callback_response.status_code == 307
|
||||
location = callback_response.headers["location"]
|
||||
assert "access_token=" in location
|
||||
|
||||
# Извлекаем токен из redirect URL
|
||||
import urllib.parse
|
||||
parsed = urllib.parse.urlparse(location)
|
||||
query_params = urllib.parse.parse_qs(parsed.query)
|
||||
access_token = query_params["access_token"][0]
|
||||
|
||||
# 3. Проверяем, что токен работает
|
||||
session_response = await client.get("/auth/session", headers={
|
||||
"Authorization": f"Bearer {access_token}"
|
||||
})
|
||||
|
||||
assert session_response.status_code == 200
|
||||
data = session_response.json()
|
||||
assert data["user"]["email"] == "user@gmail.com"
|
||||
```
|
||||
|
||||
## 🧰 Test Fixtures
|
||||
|
||||
### Auth Fixtures
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import asyncio
|
||||
from auth.tokens.sessions import SessionTokenManager
|
||||
from auth.tokens.oauth import OAuthTokenManager
|
||||
|
||||
@pytest.fixture
|
||||
async def session_manager():
|
||||
"""Фикстура SessionTokenManager"""
|
||||
return SessionTokenManager()
|
||||
|
||||
@pytest.fixture
|
||||
async def oauth_manager():
|
||||
"""Фикстура OAuthTokenManager"""
|
||||
return OAuthTokenManager()
|
||||
|
||||
@pytest.fixture
|
||||
async def test_user_token(session_manager):
|
||||
"""Фикстура для создания тестового токена"""
|
||||
token = await session_manager.create_session(
|
||||
user_id="test_user_123",
|
||||
username="testuser"
|
||||
)
|
||||
|
||||
yield token
|
||||
|
||||
# Cleanup
|
||||
await session_manager.revoke_session_token(token)
|
||||
|
||||
@pytest.fixture
|
||||
async def authenticated_client():
|
||||
"""Фикстура для аутентифицированного клиента"""
|
||||
from httpx import AsyncClient
|
||||
from main import app
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
# Создаем пользователя и получаем токен
|
||||
login_response = await client.post("/auth/login", json={
|
||||
"email": "test@example.com",
|
||||
"password": "TestPassword123!"
|
||||
})
|
||||
|
||||
token = login_response.json()["token"]
|
||||
|
||||
# Настраиваем клиент с токеном
|
||||
client.headers.update({"Authorization": f"Bearer {token}"})
|
||||
|
||||
yield client
|
||||
|
||||
@pytest.fixture
|
||||
async def oauth_tokens(oauth_manager):
|
||||
"""Фикстура для OAuth токенов"""
|
||||
await oauth_manager.store_oauth_tokens(
|
||||
user_id="test_user_123",
|
||||
provider="google",
|
||||
access_token="test_access_token",
|
||||
refresh_token="test_refresh_token",
|
||||
expires_in=3600
|
||||
)
|
||||
|
||||
yield {
|
||||
"user_id": "test_user_123",
|
||||
"provider": "google",
|
||||
"access_token": "test_access_token",
|
||||
"refresh_token": "test_refresh_token"
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
await oauth_manager.revoke_oauth_tokens("test_user_123", "google")
|
||||
```
|
||||
|
||||
### Redis Fixtures
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from storage.redis import redis
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def redis_client():
|
||||
"""Фикстура Redis клиента"""
|
||||
yield redis
|
||||
|
||||
# Cleanup после всех тестов
|
||||
await redis.flushdb()
|
||||
|
||||
@pytest.fixture
|
||||
async def clean_redis():
|
||||
"""Фикстура для очистки Redis перед тестом"""
|
||||
# Очищаем тестовые ключи
|
||||
test_keys = await redis.keys("test:*")
|
||||
if test_keys:
|
||||
await redis.delete(*test_keys)
|
||||
|
||||
yield
|
||||
|
||||
# Очищаем после теста
|
||||
test_keys = await redis.keys("test:*")
|
||||
if test_keys:
|
||||
await redis.delete(*test_keys)
|
||||
```
|
||||
|
||||
## 📊 Test Configuration
|
||||
|
||||
### pytest.ini
|
||||
|
||||
```ini
|
||||
[tool:pytest]
|
||||
asyncio_mode = auto
|
||||
testpaths = tests
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
-v
|
||||
--tb=short
|
||||
--strict-markers
|
||||
--disable-warnings
|
||||
--cov=auth
|
||||
--cov-report=html
|
||||
--cov-report=term-missing
|
||||
--cov-fail-under=80
|
||||
|
||||
markers =
|
||||
unit: Unit tests
|
||||
integration: Integration tests
|
||||
e2e: End-to-end tests
|
||||
slow: Slow tests
|
||||
redis: Tests requiring Redis
|
||||
oauth: OAuth related tests
|
||||
```
|
||||
|
||||
### conftest.py
|
||||
|
||||
```python
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock
|
||||
from httpx import AsyncClient
|
||||
from main import app
|
||||
|
||||
# Настройка asyncio для тестов
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""Создает event loop для всей сессии тестов"""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
# Мок Redis для unit тестов
|
||||
@pytest.fixture
|
||||
def mock_redis():
|
||||
"""Мок Redis клиента"""
|
||||
mock = AsyncMock()
|
||||
mock.ping.return_value = True
|
||||
mock.get.return_value = None
|
||||
mock.set.return_value = True
|
||||
mock.delete.return_value = 1
|
||||
mock.exists.return_value = False
|
||||
mock.ttl.return_value = -1
|
||||
mock.hset.return_value = 1
|
||||
mock.hgetall.return_value = {}
|
||||
mock.sadd.return_value = 1
|
||||
mock.smembers.return_value = set()
|
||||
mock.srem.return_value = 1
|
||||
mock.expire.return_value = True
|
||||
mock.setex.return_value = True
|
||||
return mock
|
||||
|
||||
# Test client
|
||||
@pytest.fixture
|
||||
async def test_client():
|
||||
"""Тестовый HTTP клиент"""
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
yield client
|
||||
```
|
||||
|
||||
## 🚀 Running Tests
|
||||
|
||||
### Команды запуска
|
||||
|
||||
```bash
|
||||
# Все тесты
|
||||
pytest
|
||||
|
||||
# Unit тесты
|
||||
pytest tests/auth/unit/ -m unit
|
||||
|
||||
# Integration тесты
|
||||
pytest tests/auth/integration/ -m integration
|
||||
|
||||
# E2E тесты
|
||||
pytest tests/auth/e2e/ -m e2e
|
||||
|
||||
# Тесты с покрытием
|
||||
pytest --cov=auth --cov-report=html
|
||||
|
||||
# Параллельный запуск
|
||||
pytest -n auto
|
||||
|
||||
# Только быстрые тесты
|
||||
pytest -m "not slow"
|
||||
|
||||
# Конкретный тест
|
||||
pytest tests/auth/unit/test_session_manager.py::TestSessionTokenManager::test_create_session
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
```yaml
|
||||
# .github/workflows/tests.yml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:6.2
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "redis-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r requirements.dev.txt
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
pytest tests/auth/unit/ -m unit --cov=auth
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
pytest tests/auth/integration/ -m integration
|
||||
env:
|
||||
REDIS_URL: redis://localhost:6379/0
|
||||
|
||||
- name: Run E2E tests
|
||||
run: |
|
||||
pytest tests/auth/e2e/ -m e2e
|
||||
env:
|
||||
REDIS_URL: redis://localhost:6379/0
|
||||
JWT_SECRET: test_secret_key_for_ci
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
```
|
||||
|
||||
## 📈 Test Metrics
|
||||
|
||||
### Coverage Goals
|
||||
- **Unit Tests**: ≥ 90% coverage
|
||||
- **Integration Tests**: ≥ 80% coverage
|
||||
- **E2E Tests**: Critical paths covered
|
||||
- **Overall**: ≥ 85% coverage
|
||||
|
||||
### Performance Benchmarks
|
||||
- **Unit Tests**: < 100ms per test
|
||||
- **Integration Tests**: < 1s per test
|
||||
- **E2E Tests**: < 10s per test
|
||||
- **Total Test Suite**: < 5 minutes
|
||||
|
||||
### Quality Metrics
|
||||
- **Test Reliability**: ≥ 99% pass rate
|
||||
- **Flaky Tests**: < 1% of total tests
|
||||
- **Test Maintenance**: Regular updates with code changes
|
||||
Reference in New Issue
Block a user