📚 Documentation Updates
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:
2025-09-22 00:56:36 +03:00
parent 4dccb84b18
commit a4411e3c86
22 changed files with 4401 additions and 2454 deletions

View File

@@ -2,16 +2,23 @@
## [0.9.21] - 2025-09-21
### 📚 Documentation Updates
- **🔍 Comprehensive authentication documentation refactoring**: Полная переработка документации аутентификации
- Обновлена таблица содержания в README.md
- Исправлены архитектурные диаграммы - токены хранятся только в Redis
- Добавлены практические примеры кода для микросервисов
- Консолидирована OAuth документация
### 🔧 Redis Connection Pool Fix
- **🐛 Fixed "max number of clients reached" error**: Исправлена критическая ошибка превышения лимита соединений Redis
- Добавлен `aioredis.ConnectionPool` с ограничением `max_connections=20` для 5 микросервисов
- Добавлен `aioredis.ConnectionPool` с ограничением `max_connections=20`
- Реализовано переиспользование соединений вместо создания новых для каждого запроса
- Добавлено правильное закрытие connection pool при shutdown приложения
- Улучшена обработка ошибок соединения с автоматическим переподключением
- **📊 Health Monitoring**: Добавлен `/health` endpoint для мониторинга состояния Redis
- Отображает количество активных соединений, использование памяти, версию Redis
- Помогает диагностировать проблемы с соединениями в production
- **🔄 Connection Management**: Оптимизировано управление соединениями
- **🔄 Connection Management**: Оптимизировано управление соединениями в монолитном приложении
- Один connection pool для всех операций Redis
- Автоматическое переподключение при потере соединения
- Корректное закрытие всех соединений при остановке приложения

View File

@@ -8,16 +8,16 @@
```shell
# Подготовка окружения
python3.12 -m venv venv
source venv/bin/activate
pip install -r requirements.dev.txt
python3.12 -m venv .venv
source .venv/bin/activate
uv run pip install -r requirements.dev.txt
# Сертификаты для HTTPS
mkcert -install
mkcert localhost
# Запуск сервера
python -m granian main:app --interface asgi
uv run python -m granian main:app --interface asgi
```
### 📊 Статус проекта
@@ -35,12 +35,29 @@ python -m granian main:app --interface asgi
### 🔧 Основные компоненты
- **[API Documentation](api.md)** - GraphQL API и резолверы
- **[Authentication](auth.md)** - Система авторизации и OAuth
- **[Authentication System](auth/README.md)** - 🎯 **Основная документация по аутентификации**
- **[RBAC System](rbac-system.md)** - Роли и права доступа
- **[Caching System](redis-schema.md)** - Redis схема и кеширование
- **[Redis Schema](redis-schema.md)** - Схема данных Redis и кеширование
- **[Security System](security.md)** - Управление паролями и email
- **[Search System](search-system.md)** - 🔍 Семантический поиск с эмбедингами
- **[Admin Panel](admin-panel.md)** - Админ-панель управления
### 🔐 Система аутентификации
- **[Auth Overview](auth/README.md)** - 🎯 **Главная страница аутентификации**
- **[System Architecture](auth/system.md)** - Архитектура и компоненты
- **[Architecture Diagrams](auth/architecture.md)** - Диаграммы и потоки данных
- **[Session Management](auth/sessions.md)** - Управление сессиями и JWT
- **[OAuth Integration](auth/oauth.md)** - Социальные провайдеры
- **[Microservices Guide](auth/microservices.md)** - 🔍 **Интеграция с другими сервисами**
- **[Migration Guide](auth/migration.md)** - Обновление с предыдущих версий
### 🛡️ Безопасность и права доступа
- **[RBAC System](rbac-system.md)** - Система ролей и разрешений
- **[Security System](security.md)** - Управление паролями и email
- **[Redis Schema](redis-schema.md)** - Схема данных и кеширование
### 🛠️ Разработка
- **[Features](features.md)** - Обзор возможностей

View File

@@ -1,253 +0,0 @@
# Архитектура системы авторизации
## Схема потоков данных
```mermaid
graph TB
subgraph "Frontend"
FE[Web Frontend]
MOB[Mobile App]
end
subgraph "Auth Layer"
MW[AuthMiddleware]
DEC[GraphQL Decorators]
HANDLER[Auth Handlers]
end
subgraph "Core Auth"
IDENTITY[Identity]
JWT[JWT Codec]
OAUTH[OAuth Manager]
PERM[Permissions]
end
subgraph "Token System"
TS[TokenStorage]
STM[SessionTokenManager]
VTM[VerificationTokenManager]
OTM[OAuthTokenManager]
BTM[BatchTokenOperations]
MON[TokenMonitoring]
end
subgraph "Storage"
REDIS[(Redis)]
DB[(PostgreSQL)]
end
subgraph "External"
GOOGLE[Google OAuth]
GITHUB[GitHub OAuth]
FACEBOOK[Facebook]
OTHER[Other Providers]
end
FE --> MW
MOB --> MW
MW --> IDENTITY
MW --> JWT
DEC --> PERM
HANDLER --> OAUTH
IDENTITY --> STM
OAUTH --> OTM
TS --> STM
TS --> VTM
TS --> OTM
STM --> REDIS
VTM --> REDIS
OTM --> REDIS
BTM --> REDIS
MON --> REDIS
IDENTITY --> DB
OAUTH --> DB
PERM --> DB
OAUTH --> GOOGLE
OAUTH --> GITHUB
OAUTH --> FACEBOOK
OAUTH --> OTHER
```
## Диаграмма компонентов
```mermaid
graph LR
subgraph "HTTP Layer"
REQ[HTTP Request]
RESP[HTTP Response]
end
subgraph "Middleware"
AUTH_MW[Auth Middleware]
CORS_MW[CORS Middleware]
end
subgraph "GraphQL"
RESOLVER[GraphQL Resolvers]
DECORATOR[Auth Decorators]
end
subgraph "Auth Core"
VALIDATION[Validation]
IDENTIFICATION[Identity Check]
AUTHORIZATION[Permission Check]
end
subgraph "Token Management"
CREATE[Token Creation]
VERIFY[Token Verification]
REVOKE[Token Revocation]
REFRESH[Token Refresh]
end
REQ --> CORS_MW
CORS_MW --> AUTH_MW
AUTH_MW --> RESOLVER
RESOLVER --> DECORATOR
DECORATOR --> VALIDATION
VALIDATION --> IDENTIFICATION
IDENTIFICATION --> AUTHORIZATION
AUTHORIZATION --> CREATE
AUTHORIZATION --> VERIFY
AUTHORIZATION --> REVOKE
AUTHORIZATION --> REFRESH
CREATE --> RESP
VERIFY --> RESP
REVOKE --> RESP
REFRESH --> RESP
```
## Схема OAuth потока
```mermaid
sequenceDiagram
participant U as User
participant F as Frontend
participant A as Auth Service
participant R as Redis
participant P as OAuth Provider
participant D as Database
U->>F: Click "Login with Provider"
F->>A: GET /oauth/{provider}?state={csrf}
A->>R: Store OAuth state
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->>D: Find/create user
A->>A: Generate JWT session token
A->>R: Store session in Redis
A->>F: Redirect with JWT token
F->>U: User logged in
```
## Схема сессионного управления
```mermaid
stateDiagram-v2
[*] --> Anonymous
Anonymous --> Authenticating: Login attempt
Authenticating --> Authenticated: Valid credentials
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 структура данных
```
├── Sessions
│ ├── session:{user_id}:{token} → Hash {user_id, username, device_info, last_activity}
│ ├── user_sessions:{user_id} → Set {token1, token2, ...}
│ └── {user_id}-{username}-{token} → Hash (legacy format)
├── Verification
│ └── verification_token:{token} → JSON {user_id, type, data, created_at}
├── OAuth
│ ├── 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}
└── Monitoring
└── token_stats → Hash {session_count, oauth_count, memory_usage}
```
## Компоненты безопасности
```mermaid
graph TD
subgraph "Input Validation"
EMAIL[Email Format]
PASS[Password Strength]
TOKEN[Token Format]
end
subgraph "Authentication"
BCRYPT[bcrypt + SHA256]
JWT_SIGN[JWT Signing]
OAUTH_VERIFY[OAuth Verification]
end
subgraph "Authorization"
ROLE[Role-based Access]
PERM[Permission Checks]
RESOURCE[Resource Access]
end
subgraph "Session Security"
TTL[Token TTL]
REVOKE[Token Revocation]
REFRESH[Secure Refresh]
end
EMAIL --> BCRYPT
PASS --> BCRYPT
TOKEN --> JWT_SIGN
BCRYPT --> ROLE
JWT_SIGN --> ROLE
OAUTH_VERIFY --> ROLE
ROLE --> PERM
PERM --> RESOURCE
RESOURCE --> TTL
RESOURCE --> REVOKE
RESOURCE --> REFRESH
```
## Масштабирование и производительность
### Горизонтальное масштабирование
- **Stateless JWT** токены
- **Redis Cluster** для высокой доступности
- **Load Balancer** aware session management
### Оптимизации
- **Connection pooling** для Redis
- **Batch operations** для массовых операций
- **Pipeline использование** для атомарности
- **LRU кэширование** для часто используемых данных
### Мониторинг производительности
- **Response time** auth операций
- **Redis memory usage** и hit rate
- **Token creation/validation** rate
- **OAuth provider** response times

View File

@@ -1,371 +0,0 @@
# Система авторизации Discours.io
## Обзор архитектуры
Система авторизации построена на модульной архитектуре с разделением на независимые компоненты:
```
auth/
├── tokens/ # Система управления токенами
├── middleware.py # HTTP middleware для аутентификации
├── decorators.py # GraphQL декораторы авторизации
├── oauth.py # OAuth провайдеры
├── orm.py # ORM модели пользователей
├── permissions.py # Система разрешений
├── identity.py # Методы идентификации
├── jwtcodec.py # JWT кодек
├── validations.py # Валидация данных
├── credentials.py # Работа с креденшалами
├── exceptions.py # Исключения авторизации
└── handler.py # HTTP обработчики
```
## Система токенов
### Система сессий
Система использует стандартный `SessionTokenManager` для управления сессиями в Redis:
**Принцип работы:**
1. При успешной аутентификации токен сохраняется в Redis через `SessionTokenManager`
2. Сессии автоматически проверяются при каждом запросе через `verify_session`
3. TTL сессий: 30 дней (настраивается)
4. Автоматическое обновление `last_activity` при активности
**Redis структура сессий:**
```
session:{user_id}:{token} # hash с данными сессии
user_sessions:{user_id} # set с активными токенами
```
**Логика получения токена (приоритет):**
1. `scope["auth_token"]` - токен из текущего запроса
2. Заголовок `Authorization`
3. Заголовок `SESSION_TOKEN_HEADER`
4. Cookie `SESSION_COOKIE_NAME`
### Типы токенов
| Тип | TTL | Назначение |
|-----|-----|------------|
| `session` | 30 дней | Токены пользовательских сессий |
| `verification` | 1 час | Токены подтверждения (email, телефон) |
| `oauth_access` | 1 час | OAuth access токены |
| `oauth_refresh` | 30 дней | OAuth refresh токены |
### Компоненты системы токенов
#### `SessionTokenManager`
Управление сессиями пользователей:
- JWT-токены с payload `{user_id, username, iat, exp}`
- Redis хранение для отзыва и управления
- Поддержка multiple sessions per user
- Автоматическое продление при активности
**Основные методы:**
```python
async def create_session(user_id: str, auth_data=None, username=None, device_info=None) -> str
async def verify_session(token: str) -> Optional[Any]
async def refresh_session(user_id: int, old_token: str, device_info=None) -> Optional[str]
async def revoke_session_token(token: str) -> bool
async def revoke_user_sessions(user_id: str) -> int
```
**Redis структура:**
```
session:{user_id}:{token} # hash с данными сессии
user_sessions:{user_id} # set с активными токенами
{user_id}-{username}-{token} # legacy ключи для совместимости
```
#### `VerificationTokenManager`
Управление токенами подтверждения:
- Email verification
- Phone verification
- Password reset
- Одноразовые токены
**Основные методы:**
```python
async def create_verification_token(user_id: str, verification_type: str, data: TokenData, ttl=None) -> str
async def validate_verification_token(token: str) -> tuple[bool, Optional[TokenData]]
async def confirm_verification_token(token: str) -> Optional[TokenData] # одноразовое использование
```
#### `OAuthTokenManager`
Управление OAuth токенами:
- Google, GitHub, Facebook, X, Telegram, VK, Yandex
- Access/refresh token pairs
- Provider-specific storage
**Redis структура:**
```
oauth_access:{user_id}:{provider} # access токен
oauth_refresh:{user_id}:{provider} # refresh токен
```
#### `BatchTokenOperations`
Пакетные операции для производительности:
- Массовая валидация токенов
- Пакетный отзыв
- Очистка истекших токенов
#### `TokenMonitoring`
Мониторинг и статистика:
- Подсчет активных токенов по типам
- Статистика использования памяти
- Health check системы токенов
- Оптимизация производительности
### 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)
# Deprecated методы (для миграции)
await TokenStorage.create_onetime(user) # -> VerificationTokenManager
```
## OAuth система
### Поддерживаемые провайдеры
- **Google** - OpenID Connect
- **GitHub** - OAuth 2.0
- **Facebook** - Facebook Login
- **X (Twitter)** - OAuth 2.0 (без email)
- **Telegram** - Telegram Login Widget (без email)
- **VK** - VK OAuth (требует разрешений для email)
- **Yandex** - Yandex OAuth
### Процесс OAuth авторизации
1. **Инициация**: `GET /oauth/{provider}?state={csrf_token}&redirect_uri={url}`
2. **Callback**: `GET /oauth/{provider}/callback?code={code}&state={state}`
3. **Обработка**: Получение user profile, создание/обновление пользователя
4. **Результат**: JWT токен в cookie + redirect на фронтенд
### Безопасность OAuth
- **PKCE** (Proof Key for Code Exchange) для дополнительной безопасности
- **State параметры** хранятся в Redis с TTL 10 минут
- **Одноразовые сессии** - после использования удаляются
- **Генерация временных email** для провайдеров без email (X, Telegram)
## Middleware и декораторы
### AuthMiddleware
HTTP middleware для автоматической аутентификации:
- Извлечение токенов из cookies/headers
- Валидация JWT токенов
- Добавление user context в request
- Обработка истекших токенов
### GraphQL декораторы
```python
@auth_required # Требует авторизации
@permission_required # Требует конкретных разрешений
@admin_required # Требует admin права
```
## 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
created_at: int
updated_at: int
last_seen: int
# OAuth связи
oauth_accounts: List[OAuthAccount]
```
### OAuthAccount
```python
class OAuthAccount:
id: int
author_id: int
provider: str # google, github, etc.
provider_id: str # ID пользователя у провайдера
provider_email: Optional[str]
provider_data: dict # Дополнительные данные от провайдера
```
## Система разрешений
### Роли
- **user** - Обычный пользователь
- **moderator** - Модератор контента
- **admin** - Администратор системы
### Разрешения
- **read** - Чтение контента
- **write** - Создание контента
- **moderate** - Модерация контента
- **admin** - Административные действия
### Проверка разрешений
```python
from auth.permissions import check_permission
@permission_required("moderate")
async def moderate_content(info, content_id: str):
# Только пользователи с правами модерации
pass
```
## Безопасность
### Хеширование паролей
- **bcrypt** с rounds=10
- **SHA256** препроцессинг для длинных паролей
- **Salt** автоматически генерируется bcrypt
### JWT токены
- **Алгоритм**: HS256
- **Secret**: Из переменной окружения JWT_SECRET
- **Payload**: `{user_id, username, iat, exp}`
- **Expiration**: 30 дней (настраивается)
### Redis security
- **TTL** для всех токенов
- **Атомарные операции** через pipelines
- **SCAN** вместо KEYS для производительности
- **Транзакции** для критических операций
## Конфигурация
### Переменные окружения
```bash
# JWT
JWT_SECRET=your_super_secret_key
JWT_EXPIRATION_HOURS=720 # 30 дней
# Redis
REDIS_URL=redis://localhost:6379/0
# OAuth провайдеры
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
FACEBOOK_APP_ID=...
FACEBOOK_APP_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
```
## API Endpoints
### Аутентификация
```
POST /auth/login # Email/password вход
POST /auth/logout # Выход (отзыв токена)
POST /auth/refresh # Обновление токена
POST /auth/register # Регистрация
```
### OAuth
```
GET /oauth/{provider} # Инициация OAuth
GET /oauth/{provider}/callback # OAuth callback
```
### Профиль
```
GET /auth/profile # Текущий пользователь
PUT /auth/profile # Обновление профиля
POST /auth/change-password # Смена пароля
```
## Мониторинг и логирование
### Метрики
- Количество активных сессий по типам
- Использование памяти Redis
- Статистика OAuth провайдеров
- Health check всех компонентов
### Логирование
- **INFO**: Успешные операции (создание сессий, OAuth)
- **WARNING**: Подозрительная активность (неверные пароли)
- **ERROR**: Ошибки системы (Redis недоступен, JWT invalid)
## Производительность
### Оптимизации Redis
- **Pipeline операции** для атомарности
- **Batch обработка** токенов (100-1000 за раз)
- **SCAN** вместо KEYS для безопасности
- **TTL** автоматическая очистка
### Кэширование
- **@lru_cache** для часто используемых ключей
- **Connection pooling** для Redis
- **JWT decode caching** в middleware
## Миграция и совместимость
### Legacy поддержка
- Старые ключи Redis: `{user_id}-{username}-{token}`
- Автоматическая миграция при обращении
- Deprecated методы с предупреждениями
### Планы развития
- [ ] Удаление legacy ключей
- [ ] Переход на RS256 для JWT
- [ ] WebAuthn/FIDO2 поддержка
- [ ] Rate limiting для auth endpoints
- [ ] Audit log для всех auth операций
## Тестирование
### Unit тесты
```bash
pytest tests/auth/ # Все auth тесты
pytest tests/auth/test_oauth.py # OAuth тесты
pytest tests/auth/test_tokens.py # Token тесты
```
### Integration тесты
- OAuth flow с моками провайдеров
- Redis операции
- JWT lifecycle
- Permission checks
## Troubleshooting
### Частые проблемы
1. **Redis connection failed** - Проверить REDIS_URL и доступность
2. **JWT invalid** - Проверить JWT_SECRET и время сервера
3. **OAuth failed** - Проверить client_id/secret провайдеров
4. **Session not found** - Возможно токен истек или отозван
### Диагностика
```python
# Проверка health системы токенов
from auth.tokens.monitoring import TokenMonitoring
health = await TokenMonitoring().health_check()
# Статистика токенов
stats = await TokenMonitoring().get_token_statistics()
```

View File

@@ -1,769 +0,0 @@
# Модуль аутентификации и авторизации
## Общее описание
Модуль реализует полноценную систему аутентификации с использованием локальной БД, Redis и httpOnly cookies для безопасного хранения токенов сессий.
## Архитектура системы
### Основные компоненты
#### 1. **AuthMiddleware** (`auth/middleware.py`)
- Единый middleware для обработки авторизации в GraphQL запросах
- Извлечение Bearer токена из заголовка Authorization или httpOnly cookie
- Проверка сессии через TokenStorage
- Создание `request.user` и `request.auth`
- Предоставление методов для установки/удаления cookies
#### 2. **EnhancedGraphQLHTTPHandler** (`auth/handler.py`)
- Расширенный GraphQL HTTP обработчик с поддержкой cookie и авторизации
- Создание расширенного контекста запроса с авторизационными данными
- Корректная обработка ответов с cookie и headers
- Интеграция с AuthMiddleware
#### 3. **TokenStorage** (`auth/tokens/storage.py`)
- Централизованное управление токенами сессий
- Хранение в Redis с TTL
- Верификация и валидация токенов
- Управление жизненным циклом сессий
#### 4. **AuthCredentials** (`auth/credentials.py`)
- Модель данных для хранения информации об авторизации
- Содержит `author_id`, `scopes`, `logged_in`, `error_message`, `email`, `token`
### Модели данных
#### Author (`orm/author.py`)
- Основная модель пользователя с расширенным функционалом аутентификации
- Поддерживает:
- Локальную аутентификацию по email/телефону
- Систему ролей и разрешений (RBAC)
- Блокировку аккаунта при множественных неудачных попытках входа
- Верификацию email/телефона
## Система 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
)
```
## Аутентификация
### Извлечение токенов
Система проверяет токены в следующем порядке приоритета:
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
# 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
```
## OAuth интеграция
### Поддерживаемые провайдеры
- **Google** - OAuth 2.0 с PKCE
- **Facebook** - OAuth 2.0
- **GitHub** - OAuth 2.0
### Реализация
```python
# auth/oauth.py
class OAuthProvider:
"""Базовый класс для OAuth провайдеров"""
def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
async def get_authorization_url(self, state: str = None) -> str:
"""Генерирует URL для авторизации"""
pass
async def exchange_code_for_token(self, code: str) -> dict:
"""Обменивает код авторизации на токен доступа"""
pass
async def get_user_info(self, access_token: str) -> dict:
"""Получает информацию о пользователе"""
pass
```
## Валидация
### Модели валидации
```python
# auth/validations.py
from pydantic import BaseModel, EmailStr
class LoginRequest(BaseModel):
email: EmailStr
password: str
class RegisterRequest(BaseModel):
email: EmailStr
password: str
name: str
phone: str | None = None
class PasswordResetRequest(BaseModel):
email: EmailStr
class EmailConfirmationRequest(BaseModel):
token: str
```
## API Endpoints
### GraphQL мутации
```graphql
# Мутации аутентификации
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
success
token
user {
id
email
name
}
error
}
}
mutation Register($input: RegisterInput!) {
registerUser(input: $input) {
success
user {
id
email
name
}
error
}
}
mutation Logout {
logout {
success
message
}
}
# Получение текущей сессии
query GetSession {
getSession {
success
token
user {
id
email
name
roles
}
error
}
}
```
### REST API endpoints
```python
# Основные endpoints
POST /auth/login # Вход в систему
POST /auth/register # Регистрация
POST /auth/logout # Выход из системы
GET /auth/session # Получение текущей сессии
POST /auth/refresh # Обновление токена
# OAuth endpoints
GET /auth/oauth/{provider} # Инициация OAuth
GET /auth/oauth/{provider}/callback # OAuth callback
```
## Безопасность
### Хеширование паролей
```python
# auth/identity.py
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
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
# auth/core.py
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
```
### CSRF защита
```python
# auth/middleware.py
def generate_csrf_token() -> str:
"""Генерирует CSRF токен"""
return secrets.token_urlsafe(32)
def verify_csrf_token(token: str, stored_token: str) -> bool:
"""Проверяет CSRF токен"""
return secrets.compare_digest(token, stored_token)
```
## Декораторы
### Основные декораторы
```python
# auth/decorators.py
from functools import wraps
from graphql import GraphQLError
def login_required(func):
"""Декоратор для проверки авторизации"""
@wraps(func)
async def wrapper(*args, **kwargs):
info = args[-1] if args else None
if not info or not hasattr(info, 'context'):
raise GraphQLError("Context not available")
user = info.context.get('user')
if not user or not user.is_authenticated:
raise GraphQLError("Authentication required")
return await func(*args, **kwargs)
return wrapper
def require_permission(permission: str):
"""Декоратор для проверки разрешений"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
info = args[-1] if args else None
if not info or not hasattr(info, 'context'):
raise GraphQLError("Context not available")
user = info.context.get('user')
if not user or not user.is_authenticated:
raise GraphQLError("Authentication required")
# Проверяем разрешение через RBAC
has_perm = await check_user_permission(
user.id, permission, info.context.get('community_id', 1)
)
if not has_perm:
raise GraphQLError("Insufficient permissions")
return await func(*args, **kwargs)
return wrapper
return decorator
```
## Интеграция с RBAC
### Проверка разрешений
```python
# auth/decorators.py
async def check_user_permission(author_id: int, permission: str, community_id: int) -> bool:
"""Проверяет разрешение пользователя через RBAC систему"""
try:
from rbac.api import user_has_permission
return await user_has_permission(author_id, permission, community_id)
except Exception as e:
logger.error(f"Ошибка проверки разрешений: {e}")
return False
```
### Получение ролей пользователя
```python
# auth/middleware.py
async def get_user_roles(author_id: int, community_id: int = 1) -> list[str]:
"""Получает роли пользователя в сообществе"""
try:
from rbac.api import get_user_roles_in_community
return get_user_roles_in_community(author_id, community_id)
except Exception as e:
logger.error(f"Ошибка получения ролей: {e}")
return []
```
## Мониторинг и логирование
### Логирование событий
```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'])
```
## Конфигурация
### Основные настройки
```python
# settings.py
# Настройки сессий
SESSION_TOKEN_LIFE_SPAN = 30 * 24 * 60 * 60 # 30 дней
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
# JWT настройки
JWT_SECRET_KEY = "your-secret-key"
JWT_ALGORITHM = "HS256"
JWT_EXPIRATION_DELTA = 30 * 24 * 60 * 60
# OAuth настройки
GOOGLE_CLIENT_ID = "your-google-client-id"
GOOGLE_CLIENT_SECRET = "your-google-client-secret"
FACEBOOK_CLIENT_ID = "your-facebook-client-id"
FACEBOOK_CLIENT_SECRET = "your-facebook-client-secret"
# Безопасность
MAX_LOGIN_ATTEMPTS = 5
ACCOUNT_LOCKOUT_DURATION = 1800 # 30 минут
PASSWORD_MIN_LENGTH = 8
```
## Примеры использования
### 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. OAuth авторизация
```typescript
// Frontend - OAuth кнопка
const handleGoogleLogin = () => {
// Перенаправляем на OAuth endpoint
window.location.href = '/auth/oauth/google';
};
// Обработка OAuth callback
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
if (code && state) {
// Обмениваем код на токен
exchangeOAuthCode(code, state);
}
}, []);
```
### 5. Выход из системы
```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);
}
};
```
## Тестирование
### Тесты аутентификации
```python
# tests/test_auth.py
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"
```
### Тесты OAuth
```python
# tests/test_oauth.py
@pytest.mark.asyncio
async def test_google_oauth_flow(client: AsyncClient, mock_google):
"""Тест OAuth flow для Google"""
# Мокаем ответ от Google
mock_google.return_value = {
"id": "12345",
"email": "test@gmail.com",
"name": "Test User"
}
# Инициация OAuth
response = await client.get("/auth/oauth/google")
assert response.status_code == 302
# Проверяем редирект
assert "accounts.google.com" in response.headers["location"]
```
## Безопасность
### Лучшие практики
1. **httpOnly Cookies**: Токены сессий хранятся только в httpOnly cookies
2. **HTTPS**: Все endpoints должны работать через HTTPS в продакшене
3. **SameSite**: Используется `SameSite=lax` для защиты от CSRF
4. **Rate Limiting**: Ограничение количества попыток входа
5. **Логирование**: Детальное логирование всех событий авторизации
6. **Валидация**: Строгая валидация всех входных данных
### Защита от атак
- **XSS**: httpOnly cookies недоступны для JavaScript
- **CSRF**: SameSite cookies и CSRF токены
- **Session Hijacking**: Secure cookies и регулярная ротация токенов
- **Brute Force**: Ограничение попыток входа и блокировка аккаунтов
- **SQL Injection**: Использование ORM и параметризованных запросов
## Миграция
### Обновление существующего кода
Если в вашем коде используются старые методы аутентификации:
```python
# Старый код
token = request.headers.get("Authorization")
# Новый код
from auth.utils import extract_token_from_request
token = await extract_token_from_request(request)
```
### Совместимость
Новая система полностью совместима с существующим кодом:
- Поддерживаются как cookies, так и заголовки Authorization
- Все существующие декораторы работают без изменений
- API endpoints сохранили свои сигнатуры
- RBAC интеграция работает как прежде

105
docs/auth/README.md Normal file
View 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
View 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
View 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
View 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 тесты для проверки производительности

View File

@@ -318,5 +318,5 @@ async def check_performance():
### Контакты
- **Issues**: GitHub Issues
- **Документация**: `/docs/auth-system.md`
- **Архитектура**: `/docs/auth-architecture.md`
- **Документация**: `/docs/auth/system.md`
- **Архитектура**: `/docs/auth/architecture.md`

466
docs/auth/oauth.md Normal file
View 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
View 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
View 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
View 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
View 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

View File

@@ -1,199 +0,0 @@
# OAuth Deployment Checklist
## 🚀 Quick Setup Guide
### 1. Backend Implementation
```bash
# Добавьте в requirements.txt или poetry
redis>=4.0.0
httpx>=0.24.0
pydantic>=2.0.0
```
### 2. Environment Variables
```bash
# .env file
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
FACEBOOK_APP_ID=your_facebook_app_id
FACEBOOK_APP_SECRET=your_facebook_app_secret
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_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
REDIS_URL=redis://localhost:6379/0
JWT_SECRET=your_super_secret_jwt_key
JWT_EXPIRATION_HOURS=24
```
### 3. Database Migration
```sql
-- Create oauth_links table
CREATE TABLE oauth_links (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES authors(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
provider_data JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(provider, provider_id)
);
CREATE INDEX idx_oauth_links_user_id ON oauth_links(user_id);
CREATE INDEX idx_oauth_links_provider ON oauth_links(provider, provider_id);
```
### 4. OAuth Provider Setup
#### 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` (для разработки)
#### 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`
#### 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`
### 5. Backend Endpoints (FastAPI example)
```python
# auth/oauth.py
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import RedirectResponse
router = APIRouter(prefix="/auth/oauth")
@router.get("/{provider}")
async def oauth_redirect(provider: str, state: str, redirect_uri: str):
# Валидация провайдера
if provider not in ["google", "facebook", "github", "vk", "yandex"]:
raise HTTPException(400, "Unsupported 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)
@router.get("/{provider}/callback")
async def oauth_callback(provider: str, code: str, state: str):
# Проверка state
stored_data = await get_oauth_state(state)
if not stored_data:
raise HTTPException(400, "Invalid state")
# Обмен code на user_data
user_data = await exchange_code_for_user_data(provider, code)
# Создание/поиск пользователя
user = await get_or_create_user_from_oauth(provider, user_data)
# Генерация JWT
access_token = generate_jwt_token(user.id)
# Редирект с токеном
return RedirectResponse(
url=f"{stored_data['redirect_uri']}?state={state}&access_token={access_token}"
)
```
### 6. Testing
```bash
# Запуск E2E тестов
npm run test:e2e -- oauth.spec.ts
# Проверка OAuth endpoints
curl -X GET "http://localhost:8000/auth/oauth/google?state=test&redirect_uri=http://localhost:3000"
```
### 7. Production Deployment
#### Frontend
- [ ] Проверить корректность `coreApiUrl` в production
- [ ] Добавить обработку ошибок OAuth в UI
- [ ] Настроить CSP headers для OAuth редиректов
#### Backend
- [ ] Настроить HTTPS для всех OAuth endpoints
- [ ] Добавить rate limiting для OAuth endpoints
- [ ] Настроить CORS для фронтенд доменов
- [ ] Добавить мониторинг OAuth ошибок
- [ ] Настроить логирование OAuth событий
#### Infrastructure
- [ ] Настроить Redis для production
- [ ] Добавить health checks для OAuth endpoints
- [ ] Настроить backup для oauth_links таблицы
### 8. Security Checklist
- [ ] Все OAuth секреты в environment variables
- [ ] State validation с TTL (10 минут)
- [ ] CSRF protection включен
- [ ] Redirect URI validation
- [ ] Rate limiting на OAuth endpoints
- [ ] Логирование всех OAuth событий
- [ ] HTTPS обязателен в production
### 9. Monitoring
```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
```
## 🔧 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]"
```

View File

@@ -1,430 +0,0 @@
# OAuth Implementation Guide
## Фронтенд (Текущая реализация)
### Контекст сессии
```typescript
// src/context/session.tsx
const oauth = (provider: string) => {
console.info('[oauth] Starting OAuth flow for provider:', provider)
if (isServer) {
console.warn('[oauth] OAuth not available during SSR')
return
}
// Генерируем state для OAuth
const state = crypto.randomUUID()
localStorage.setItem('oauth_state', state)
// Формируем URL для OAuth
const oauthUrl = `${coreApiUrl}/auth/oauth/${provider}?state=${state}&redirect_uri=${encodeURIComponent(window.location.origin)}`
// Перенаправляем на OAuth провайдера
window.location.href = oauthUrl
}
```
### Обработка OAuth callback
```typescript
// Обработка OAuth параметров в SessionProvider
createEffect(
on([() => searchParams?.state, () => searchParams?.access_token, () => searchParams?.token],
([state, access_token, token]) => {
// OAuth обработка
if (state && access_token) {
console.info('[SessionProvider] Processing OAuth callback')
const storedState = !isServer ? localStorage.getItem('oauth_state') : null
if (storedState === state) {
console.info('[SessionProvider] OAuth state verified')
batch(() => {
changeSearchParams({ mode: 'confirm-email', m: 'auth', access_token }, { replace: true })
if (!isServer) localStorage.removeItem('oauth_state')
})
} else {
console.warn('[SessionProvider] OAuth state mismatch')
setAuthError('OAuth state mismatch')
}
return
}
// Обработка токена сброса пароля
if (token) {
console.info('[SessionProvider] Processing password reset token')
changeSearchParams({ mode: 'change-password', m: 'auth', token }, { replace: true })
}
},
{ defer: true }
)
)
```
## Бекенд Requirements
### 1. OAuth Endpoints
#### GET `/auth/oauth/{provider}`
```python
@router.get("/auth/oauth/{provider}")
async def oauth_redirect(
provider: str,
state: str,
redirect_uri: str,
request: Request
):
"""
Инициация OAuth flow с внешним провайдером
Args:
provider: Провайдер OAuth (google, facebook, github)
state: CSRF токен от клиента
redirect_uri: URL для редиректа после авторизации
Returns:
RedirectResponse: Редирект на провайдера OAuth
"""
# Валидация провайдера
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
):
"""
Обработка callback от OAuth провайдера
Args:
provider: Провайдер OAuth
code: Authorization code от провайдера
state: CSRF токен для проверки
Returns:
RedirectResponse: Редирект обратно на фронтенд с токеном
"""
# Проверка 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)
```
### 2. Provider Configuration
#### 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"
}
```
#### 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"
}
```
#### 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"
}
```
### 3. User Management
#### OAuth User Model
```python
class OAuthUser(BaseModel):
provider: str
provider_id: str
email: str
name: str
avatar_url: Optional[str] = None
raw_data: dict
```
#### User Creation/Linking
```python
async def get_or_create_user_from_oauth(
provider: str,
oauth_data: OAuthUser
) -> User:
"""
Поиск существующего пользователя или создание нового
Args:
provider: OAuth провайдер
oauth_data: Данные пользователя от провайдера
Returns:
User: Пользователь в системе
"""
# Поиск по OAuth связке
oauth_link = await OAuthLink.get_by_provider_and_id(
provider=provider,
provider_id=oauth_data.provider_id
)
if oauth_link:
return await User.get(oauth_link.user_id)
# Поиск по email
existing_user = await User.get_by_email(oauth_data.email)
if existing_user:
# Привязка OAuth к существующему пользователю
await OAuthLink.create(
user_id=existing_user.id,
provider=provider,
provider_id=oauth_data.provider_id,
provider_data=oauth_data.raw_data
)
return existing_user
# Создание нового пользователя
new_user = await User.create(
email=oauth_data.email,
name=oauth_data.name,
pic=oauth_data.avatar_url,
is_verified=True, # OAuth email считается верифицированным
registration_method='oauth',
registration_provider=provider
)
# Создание OAuth связки
await OAuthLink.create(
user_id=new_user.id,
provider=provider,
provider_id=oauth_data.provider_id,
provider_data=oauth_data.raw_data
)
return new_user
```
### 4. Security
#### 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
```
#### 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)
```
### 5. Database Schema
#### OAuth Links Table
```sql
CREATE TABLE oauth_links (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
provider_data JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(provider, provider_id),
INDEX(user_id),
INDEX(provider, provider_id)
);
```
### 6. Environment Variables
#### Required Config
```bash
# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
# Facebook OAuth
FACEBOOK_APP_ID=your_facebook_app_id
FACEBOOK_APP_SECRET=your_facebook_app_secret
# GitHub OAuth
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
# Redis для state management
REDIS_URL=redis://localhost:6379/0
# JWT
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRATION_HOURS=24
```
### 7. Error Handling
#### OAuth Exceptions
```python
class OAuthException(Exception):
pass
class InvalidProviderException(OAuthException):
pass
class StateValidationException(OAuthException):
pass
class ProviderAPIException(OAuthException):
pass
# Error responses
@app.exception_handler(OAuthException)
async def oauth_exception_handler(request: Request, exc: OAuthException):
logger.error(f"OAuth error: {exc}")
return RedirectResponse(
url=f"{request.base_url}?error=oauth_failed&message={str(exc)}"
)
```
### 8. Testing
#### 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"]
```
## Frontend Testing
### 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()
})
```
## Deployment Checklist
- [ ] Зарегистрировать OAuth приложения у провайдеров
- [ ] Настроить redirect URLs в консолях провайдеров
- [ ] Добавить environment variables
- [ ] Настроить Redis для state management
- [ ] Создать таблицу oauth_links
- [ ] Добавить rate limiting для OAuth endpoints
- [ ] Настроить мониторинг OAuth ошибок
- [ ] Протестировать все провайдеры в staging
- [ ] Добавить логирование OAuth событий

View File

@@ -1,93 +0,0 @@
# OAuth Providers Setup Guide
This guide explains how to set up OAuth authentication for various social platforms.
## Supported Providers
The platform supports the following OAuth providers:
- Google
- GitHub
- Facebook
- X (Twitter)
- Telegram
- VK (VKontakte)
- Yandex
## Environment Variables
Add the needed environment variables to your `.env` file
## Provider Setup Instructions
### Google
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select existing
3. Enable Google+ API and OAuth 2.0
4. Create OAuth 2.0 Client ID credentials
5. Add your callback URLs: `https://yourdomain.com/oauth/google/callback`
### GitHub
1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
2. Create a new OAuth App
3. Set Authorization callback URL: `https://yourdomain.com/oauth/github/callback`
### Facebook
1. Go to [Facebook Developers](https://developers.facebook.com/)
2. Create a new app
3. Add Facebook Login product
4. Configure Valid OAuth redirect URIs: `https://yourdomain.com/oauth/facebook/callback`
### X (Twitter)
1. Go to [Twitter Developer Portal](https://developer.twitter.com/)
2. Create a new app
3. Enable OAuth 2.0 authentication
4. Set Callback URLs: `https://yourdomain.com/oauth/x/callback`
5. **Note**: X doesn't provide email addresses through their API
### Telegram
1. Create a bot with [@BotFather](https://t.me/botfather)
2. Use `/newbot` command and follow instructions
3. Get your bot token
4. Configure domain settings with `/setdomain` command
5. **Note**: Telegram doesn't provide email addresses
### VK (VKontakte)
1. Go to [VK for Developers](https://vk.com/dev)
2. Create a new application
3. Set Authorized redirect URI: `https://yourdomain.com/oauth/vk/callback`
4. **Note**: Email access requires special permissions from VK
### Yandex
1. Go to [Yandex OAuth](https://oauth.yandex.com/)
2. Create a new application
3. Set Callback URI: `https://yourdomain.com/oauth/yandex/callback`
4. Select required permissions: `login:email login:info`
## Email Handling
Some providers (X, Telegram) don't provide email addresses. In these cases:
- A temporary email is generated: `{provider}_{user_id}@oauth.local`
- Users can update their email in profile settings later
- `email_verified` is set to `false` for generated emails
## Usage in Frontend
OAuth URLs:
```
/oauth/google
/oauth/github
/oauth/facebook
/oauth/x
/oauth/telegram
/oauth/vk
/oauth/yandex
```
Each provider accepts a `state` parameter for CSRF protection and a `redirect_uri` for post-authentication redirects.
## Security Notes
- All OAuth flows use PKCE (Proof Key for Code Exchange) for additional security
- State parameters are stored in Redis with 10-minute TTL
- OAuth sessions are one-time use only
- Failed authentications are logged for monitoring

View File

@@ -1,329 +0,0 @@
# OAuth Token Management
## Overview
Система управления OAuth токенами с использованием Redis для безопасного и производительного хранения токенов доступа и обновления от различных провайдеров.
## Архитектура
### Redis Storage
OAuth токены хранятся в Redis с автоматическим истечением (TTL):
- `oauth_access:{user_id}:{provider}` - access tokens
- `oauth_refresh:{user_id}:{provider}` - refresh tokens
### Поддерживаемые провайдеры
- Google OAuth 2.0
- Facebook Login
- GitHub OAuth
## API Documentation
### OAuthTokenStorage Class
#### store_access_token()
Сохраняет access token в Redis с автоматическим TTL.
```python
await OAuthTokenStorage.store_access_token(
user_id=123,
provider="google",
access_token="ya29.a0AfH6SM...",
expires_in=3600,
additional_data={"scope": "profile email"}
)
```
#### store_refresh_token()
Сохраняет refresh token с длительным TTL (30 дней по умолчанию).
```python
await OAuthTokenStorage.store_refresh_token(
user_id=123,
provider="google",
refresh_token="1//04...",
ttl=2592000 # 30 дней
)
```
#### get_access_token()
Получает действующий access token из Redis.
```python
token_data = await OAuthTokenStorage.get_access_token(123, "google")
if token_data:
access_token = token_data["token"]
expires_in = token_data["expires_in"]
```
#### refresh_access_token()
Обновляет access token (и опционально refresh token).
```python
success = await OAuthTokenStorage.refresh_access_token(
user_id=123,
provider="google",
new_access_token="ya29.new_token...",
expires_in=3600,
new_refresh_token="1//04new..." # опционально
)
```
#### delete_tokens()
Удаляет все токены пользователя для провайдера.
```python
await OAuthTokenStorage.delete_tokens(123, "google")
```
#### get_user_providers()
Получает список OAuth провайдеров для пользователя.
```python
providers = await OAuthTokenStorage.get_user_providers(123)
# ["google", "github"]
```
#### extend_token_ttl()
Продлевает срок действия токена.
```python
# Продлить access token на 30 минут
success = await OAuthTokenStorage.extend_token_ttl(123, "google", "access", 1800)
# Продлить refresh token на 7 дней
success = await OAuthTokenStorage.extend_token_ttl(123, "google", "refresh", 604800)
```
#### get_token_info()
Получает подробную информацию о токенах включая TTL.
```python
info = await OAuthTokenStorage.get_token_info(123, "google")
# {
# "user_id": 123,
# "provider": "google",
# "access_token": {"exists": True, "ttl": 3245},
# "refresh_token": {"exists": True, "ttl": 2589600}
# }
```
## Data Structures
### Access Token Structure
```json
{
"token": "ya29.a0AfH6SM...",
"provider": "google",
"user_id": 123,
"created_at": 1640995200,
"expires_in": 3600,
"scope": "profile email",
"token_type": "Bearer"
}
```
### Refresh Token Structure
```json
{
"token": "1//04...",
"provider": "google",
"user_id": 123,
"created_at": 1640995200
}
```
## Security Considerations
### Token Expiration
- **Access tokens**: TTL основан на `expires_in` от провайдера (обычно 1 час)
- **Refresh tokens**: TTL 30 дней по умолчанию
- **Автоматическая очистка**: Redis автоматически удаляет истекшие токены
- **Внутренняя система истечения**: Использует SET + EXPIRE для точного контроля TTL
### Redis Expiration Benefits
- **Гибкость**: Можно изменять TTL существующих токенов через EXPIRE
- **Мониторинг**: Команда TTL показывает оставшееся время жизни токена
- **Расширение**: Возможность продления срока действия токенов без перезаписи
- **Атомарность**: Separate SET/EXPIRE operations для лучшего контроля
### Access Control
- Токены доступны только владельцу аккаунта
- Нет доступа к токенам через GraphQL API
- Токены не хранятся в основной базе данных
### Provider Isolation
- Токены разных провайдеров хранятся отдельно
- Удаление токенов одного провайдера не влияет на другие
- Поддержка множественных OAuth подключений
## Integration Examples
### OAuth Login Flow
```python
# После успешной авторизации через OAuth провайдера
async def handle_oauth_callback(user_id: int, provider: str, tokens: dict):
# Сохраняем токены в Redis
await OAuthTokenStorage.store_access_token(
user_id=user_id,
provider=provider,
access_token=tokens["access_token"],
expires_in=tokens.get("expires_in", 3600)
)
if "refresh_token" in tokens:
await OAuthTokenStorage.store_refresh_token(
user_id=user_id,
provider=provider,
refresh_token=tokens["refresh_token"]
)
```
### Token Refresh
```python
async def refresh_oauth_token(user_id: int, provider: str):
# Получаем refresh token
refresh_data = await OAuthTokenStorage.get_refresh_token(user_id, provider)
if not refresh_data:
return False
# Обмениваем refresh token на новый access token
new_tokens = await exchange_refresh_token(
provider, refresh_data["token"]
)
# Сохраняем новые токены
return await OAuthTokenStorage.refresh_access_token(
user_id=user_id,
provider=provider,
new_access_token=new_tokens["access_token"],
expires_in=new_tokens.get("expires_in"),
new_refresh_token=new_tokens.get("refresh_token")
)
```
### API Integration
```python
async def make_oauth_request(user_id: int, provider: str, endpoint: str):
# Получаем действующий access token
token_data = await OAuthTokenStorage.get_access_token(user_id, provider)
if not token_data:
# Токен отсутствует, требуется повторная авторизация
raise OAuthTokenMissing()
# Делаем запрос к API провайдера
headers = {"Authorization": f"Bearer {token_data['token']}"}
response = await httpx.get(endpoint, headers=headers)
if response.status_code == 401:
# Токен истек, пытаемся обновить
if await refresh_oauth_token(user_id, provider):
# Повторяем запрос с новым токеном
token_data = await OAuthTokenStorage.get_access_token(user_id, provider)
headers = {"Authorization": f"Bearer {token_data['token']}"}
response = await httpx.get(endpoint, headers=headers)
return response.json()
```
### TTL Monitoring and Management
```python
async def monitor_token_expiration(user_id: int, provider: str):
"""Мониторинг и управление сроком действия токенов"""
# Получаем информацию о токенах
info = await OAuthTokenStorage.get_token_info(user_id, provider)
# Проверяем access token
if info["access_token"]["exists"]:
ttl = info["access_token"]["ttl"]
if ttl < 300: # Меньше 5 минут
logger.warning(f"Access token expires soon: {ttl}s")
# Автоматически обновляем токен
await refresh_oauth_token(user_id, provider)
# Проверяем refresh token
if info["refresh_token"]["exists"]:
ttl = info["refresh_token"]["ttl"]
if ttl < 86400: # Меньше 1 дня
logger.warning(f"Refresh token expires soon: {ttl}s")
# Уведомляем пользователя о необходимости повторной авторизации
async def extend_session_if_active(user_id: int, provider: str):
"""Продлевает сессию для активных пользователей"""
# Проверяем активность пользователя
if await is_user_active(user_id):
# Продлеваем access token на 1 час
success = await OAuthTokenStorage.extend_token_ttl(
user_id, provider, "access", 3600
)
if success:
logger.info(f"Extended access token for active user {user_id}")
```
## Migration from Database
Если у вас уже есть OAuth токены в базе данных, используйте этот скрипт для миграции:
```python
async def migrate_oauth_tokens():
"""Миграция OAuth токенов из БД в Redis"""
with local_session() as session:
# Предполагая, что токены хранились в таблице authors
authors = session.query(Author).where(
or_(
Author.provider_access_token.is_not(None),
Author.provider_refresh_token.is_not(None)
)
).all()
for author in authors:
# Получаем провайдер из oauth вместо старого поля oauth
if author.oauth:
for provider in author.oauth.keys():
if author.provider_access_token:
await OAuthTokenStorage.store_access_token(
user_id=author.id,
provider=provider,
access_token=author.provider_access_token
)
if author.provider_refresh_token:
await OAuthTokenStorage.store_refresh_token(
user_id=author.id,
provider=provider,
refresh_token=author.provider_refresh_token
)
print(f"Migrated OAuth tokens for {len(authors)} authors")
```
## Performance Benefits
### Redis Advantages
- **Скорость**: Доступ к токенам за микросекунды
- **Масштабируемость**: Не нагружает основную БД
- **Автоматическая очистка**: TTL убирает истекшие токены
- **Память**: Эффективное использование памяти Redis
### Reduced Database Load
- OAuth токены больше не записываются в основную БД
- Уменьшено количество записей в таблице authors
- Faster user queries без JOIN к токенам
## Monitoring and Maintenance
### Redis Memory Usage
```bash
# Проверка использования памяти OAuth токенами
redis-cli --scan --pattern "oauth_*" | wc -l
redis-cli memory usage oauth_access:123:google
```
### Cleanup Statistics
```python
# Периодическая очистка и логирование (опционально)
async def oauth_cleanup_job():
cleaned = await OAuthTokenStorage.cleanup_expired_tokens()
logger.info(f"OAuth cleanup completed, {cleaned} tokens processed")
```

View File

@@ -478,6 +478,12 @@ permission_checks_total = Counter('rbac_permission_checks_total')
role_assignments_total = Counter('rbac_role_assignments_total')
```
## 🔗 Связанные системы
- **[Authentication System](auth/README.md)** - Система аутентификации
- **[Security System](security.md)** - Управление паролями и email
- **[Redis Schema](redis-schema.md)** - Схема данных и кеширование
## Новые возможности системы
### Рекурсивное наследование разрешений

View File

@@ -4,6 +4,12 @@
Redis используется как основное хранилище для кэширования, сессий, токенов и временных данных. Все ключи следуют структурированным паттернам для обеспечения консистентности и производительности.
## 🔗 Связанные системы
- **[Authentication System](auth/README.md)** - Система аутентификации (использует Redis для сессий)
- **[RBAC System](rbac-system.md)** - Система ролей (кеширование разрешений)
- **[Security System](security.md)** - Управление паролями (токены в Redis)
## Принципы именования ключей
### Общие правила

View File

@@ -3,6 +3,12 @@
## Overview
Система безопасности обеспечивает управление паролями и email адресами пользователей через специализированные GraphQL мутации с использованием Redis для хранения токенов.
## 🔗 Связанные системы
- **[Authentication System](auth/README.md)** - Основная система аутентификации
- **[RBAC System](rbac-system.md)** - Система ролей и разрешений
- **[Redis Schema](redis-schema.md)** - Схема данных Redis
## GraphQL API
### Мутации