From a4411e3c86b1c7bf9ee6ff76fc6695b1c8ce65de Mon Sep 17 00:00:00 2001 From: Untone Date: Mon, 22 Sep 2025 00:56:36 +0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9A=20Documentation=20Updates=20-=20**?= =?UTF-8?q?=F0=9F=94=8D=20Comprehensive=20authentication=20documentation?= =?UTF-8?q?=20refactoring**:=20=D0=9F=D0=BE=D0=BB=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=B0=D1=83=D1=82=D0=B5=D0=BD=D1=82=D0=B8?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D0=B8=20=20=20-=20=D0=9E?= =?UTF-8?q?=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=82=D0=B0?= =?UTF-8?q?=D0=B1=D0=BB=D0=B8=D1=86=D0=B0=20=D1=81=D0=BE=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B2=20README.md=20=20=20-?= =?UTF-8?q?=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B?= =?UTF-8?q?=20=D0=B0=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20=D0=B4=D0=B8=D0=B0=D0=B3=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=BC=D1=8B=20-=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D1=8B=20?= =?UTF-8?q?=D1=85=D1=80=D0=B0=D0=BD=D1=8F=D1=82=D1=81=D1=8F=20=D1=82=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B2=20Redis=20=20=20-=20=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D1=80=D1=8B=20=D0=BA=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D0=BC=D0=B8=D0=BA=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=BE=D0=B2=20=20=20-=20?= =?UTF-8?q?=D0=9A=D0=BE=D0=BD=D1=81=D0=BE=D0=BB=D0=B8=D0=B4=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B0=20OAuth=20=D0=B4=D0=BE=D0=BA?= =?UTF-8?q?=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +- docs/README.md | 29 +- docs/auth-architecture.md | 253 ------ docs/auth-system.md | 371 -------- docs/auth.md | 769 ---------------- docs/auth/README.md | 105 +++ docs/auth/api.md | 657 ++++++++++++++ docs/auth/architecture.md | 276 ++++++ docs/auth/microservices.md | 546 +++++++++++ docs/{auth-migration.md => auth/migration.md} | 4 +- docs/auth/oauth.md | 466 ++++++++++ docs/auth/security.md | 579 ++++++++++++ docs/auth/sessions.md | 502 +++++++++++ docs/auth/system.md | 373 ++++++++ docs/auth/testing.md | 845 ++++++++++++++++++ docs/oauth-deployment.md | 199 ----- docs/oauth-implementation.md | 430 --------- docs/oauth-setup.md | 93 -- docs/oauth.md | 329 ------- docs/rbac-system.md | 6 + docs/redis-schema.md | 6 + docs/security.md | 6 + 22 files changed, 4401 insertions(+), 2454 deletions(-) delete mode 100644 docs/auth-architecture.md delete mode 100644 docs/auth-system.md delete mode 100644 docs/auth.md create mode 100644 docs/auth/README.md create mode 100644 docs/auth/api.md create mode 100644 docs/auth/architecture.md create mode 100644 docs/auth/microservices.md rename docs/{auth-migration.md => auth/migration.md} (98%) create mode 100644 docs/auth/oauth.md create mode 100644 docs/auth/security.md create mode 100644 docs/auth/sessions.md create mode 100644 docs/auth/system.md create mode 100644 docs/auth/testing.md delete mode 100644 docs/oauth-deployment.md delete mode 100644 docs/oauth-implementation.md delete mode 100644 docs/oauth-setup.md delete mode 100644 docs/oauth.md diff --git a/CHANGELOG.md b/CHANGELOG.md index df055ebd..23342f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - Автоматическое переподключение при потере соединения - Корректное закрытие всех соединений при остановке приложения diff --git a/docs/README.md b/docs/README.md index 7bd7c63d..9422c23d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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)** - Обзор возможностей diff --git a/docs/auth-architecture.md b/docs/auth-architecture.md deleted file mode 100644 index 9c2557be..00000000 --- a/docs/auth-architecture.md +++ /dev/null @@ -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 diff --git a/docs/auth-system.md b/docs/auth-system.md deleted file mode 100644 index aee5fffe..00000000 --- a/docs/auth-system.md +++ /dev/null @@ -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() -``` diff --git a/docs/auth.md b/docs/auth.md deleted file mode 100644 index 0b409e15..00000000 --- a/docs/auth.md +++ /dev/null @@ -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 интеграция работает как прежде diff --git a/docs/auth/README.md b/docs/auth/README.md new file mode 100644 index 00000000..10144d23 --- /dev/null +++ b/docs/auth/README.md @@ -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** автоматическая очистка истекших токенов diff --git a/docs/auth/api.md b/docs/auth/api.md new file mode 100644 index 00000000..82542e33 --- /dev/null +++ b/docs/auth/api.md @@ -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 +``` diff --git a/docs/auth/architecture.md b/docs/auth/architecture.md new file mode 100644 index 00000000..c916038b --- /dev/null +++ b/docs/auth/architecture.md @@ -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} +``` diff --git a/docs/auth/microservices.md b/docs/auth/microservices.md new file mode 100644 index 00000000..73d60f00 --- /dev/null +++ b/docs/auth/microservices.md @@ -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 тесты для проверки производительности diff --git a/docs/auth-migration.md b/docs/auth/migration.md similarity index 98% rename from docs/auth-migration.md rename to docs/auth/migration.md index 9ef29a11..a265338c 100644 --- a/docs/auth-migration.md +++ b/docs/auth/migration.md @@ -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` diff --git a/docs/auth/oauth.md b/docs/auth/oauth.md new file mode 100644 index 00000000..428631e3 --- /dev/null +++ b/docs/auth/oauth.md @@ -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 +``` diff --git a/docs/auth/security.md b/docs/auth/security.md new file mode 100644 index 00000000..9ba5f477 --- /dev/null +++ b/docs/auth/security.md @@ -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**: Документирование процедур diff --git a/docs/auth/sessions.md b/docs/auth/sessions.md new file mode 100644 index 00000000..3f9a5bad --- /dev/null +++ b/docs/auth/sessions.md @@ -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 ` +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); + } +}; +``` diff --git a/docs/auth/system.md b/docs/auth/system.md new file mode 100644 index 00000000..02d7090b --- /dev/null +++ b/docs/auth/system.md @@ -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 diff --git a/docs/auth/testing.md b/docs/auth/testing.md new file mode 100644 index 00000000..f73bb7d0 --- /dev/null +++ b/docs/auth/testing.md @@ -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 diff --git a/docs/oauth-deployment.md b/docs/oauth-deployment.md deleted file mode 100644 index d9f000f3..00000000 --- a/docs/oauth-deployment.md +++ /dev/null @@ -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]" -``` diff --git a/docs/oauth-implementation.md b/docs/oauth-implementation.md deleted file mode 100644 index 78a91ff5..00000000 --- a/docs/oauth-implementation.md +++ /dev/null @@ -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 событий diff --git a/docs/oauth-setup.md b/docs/oauth-setup.md deleted file mode 100644 index 79155f6a..00000000 --- a/docs/oauth-setup.md +++ /dev/null @@ -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 diff --git a/docs/oauth.md b/docs/oauth.md deleted file mode 100644 index 46492362..00000000 --- a/docs/oauth.md +++ /dev/null @@ -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") -``` diff --git a/docs/rbac-system.md b/docs/rbac-system.md index 2f88f408..9eb39043 100644 --- a/docs/rbac-system.md +++ b/docs/rbac-system.md @@ -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)** - Схема данных и кеширование + ## Новые возможности системы ### Рекурсивное наследование разрешений diff --git a/docs/redis-schema.md b/docs/redis-schema.md index 3d5a3a24..fae5f914 100644 --- a/docs/redis-schema.md +++ b/docs/redis-schema.md @@ -4,6 +4,12 @@ Redis используется как основное хранилище для кэширования, сессий, токенов и временных данных. Все ключи следуют структурированным паттернам для обеспечения консистентности и производительности. +## 🔗 Связанные системы + +- **[Authentication System](auth/README.md)** - Система аутентификации (использует Redis для сессий) +- **[RBAC System](rbac-system.md)** - Система ролей (кеширование разрешений) +- **[Security System](security.md)** - Управление паролями (токены в Redis) + ## Принципы именования ключей ### Общие правила diff --git a/docs/security.md b/docs/security.md index 254678c6..4e6d084c 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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 ### Мутации