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