[0.9.28] - OAuth/Auth with httpOnly cookie
All checks were successful
Deploy on push / deploy (push) Successful in 4m32s

This commit is contained in:
2025-09-28 12:22:37 +03:00
parent 6451ba7de5
commit fb98a1c6c8
27 changed files with 1449 additions and 2147 deletions

View File

@@ -2,17 +2,24 @@
## 📚 Обзор
Модульная система аутентификации с JWT токенами, Redis-сессиями, OAuth интеграцией и RBAC авторизацией. Поддерживает httpOnly cookies и Bearer токены для веб и API клиентов.
Модульная система аутентификации с **httpOnly cookies**, JWT токенами, Redis-сессиями, OAuth интеграцией и RBAC авторизацией.
### 🎯 **Единый подход с httpOnly cookies для ВСЕХ типов авторизации:**
-**OAuth** (Google/GitHub/Yandex/VK) → httpOnly cookie
-**Email/Password** → httpOnly cookie
-**GraphQL запросы**`credentials: 'include'`
-**Максимальная безопасность** → защита от XSS/CSRF
## 🚀 Быстрый старт
### Для микросервисов
### Для разработчиков
```python
from auth.tokens.sessions import SessionTokenManager
from auth.utils import extract_token_from_request
# Проверка токена
# Проверка токена (автоматически из cookie или Bearer заголовка)
sessions = SessionTokenManager()
token = await extract_token_from_request(request)
payload = await sessions.verify_session(token)
@@ -22,6 +29,18 @@ if payload:
print(f"Пользователь авторизован: {user_id}")
```
### Для фронтенда
```typescript
// Все запросы используют httpOnly cookies
const response = await fetch('/graphql', {
method: 'POST',
credentials: 'include', // ✅ КРИТИЧНО: отправляет httpOnly cookies
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables })
});
```
### Redis ключи для поиска
```bash
@@ -29,7 +48,7 @@ if payload:
session:{user_id}:{token} # Данные сессии (hash)
user_sessions:{user_id} # Список активных токенов (set)
# OAuth токены
# OAuth токены (для API интеграций)
oauth_access:{user_id}:{provider} # Access токен
oauth_refresh:{user_id}:{provider} # Refresh токен
```
@@ -43,7 +62,7 @@ oauth_refresh:{user_id}:{provider} # Refresh токен
### 🔑 Аутентификация
- **[Управление сессиями](sessions.md)** - JWT токены и Redis хранение
- **[OAuth интеграция](oauth.md)** - Социальные провайдеры
- **[OAuth интеграция](oauth.md)** - Социальные провайдеры с httpOnly cookies
- **[Микросервисы](microservices.md)** - 🎯 **Интеграция с другими сервисами**
### 🛠️ Разработка
@@ -56,6 +75,40 @@ oauth_refresh:{user_id}:{provider} # Refresh токен
- **[Security System](../security.md)** - Управление паролями и email
- **[Redis Schema](../redis-schema.md)** - Схема данных и кеширование
## 🔄 OAuth Flow (обновленный 2025)
### 1. 🚀 Инициация OAuth
```typescript
// Пользователь нажимает "Войти через Google"
const handleOAuthLogin = (provider: string) => {
window.location.href = `/oauth/${provider}/login`;
};
```
### 2. 🔄 OAuth Callback (бэкенд)
```python
# Google → /oauth/google/callback
# 1. Обменивает code на access_token
# 2. Получает профиль пользователя
# 3. Создает JWT сессию
# 4. Устанавливает httpOnly cookie
# 5. Редиректит на фронтенд БЕЗ токена в URL
```
### 3. 🌐 Фронтенд финализация
```typescript
// Проверяем URL на ошибки
const error = urlParams.get('error');
if (error) {
// Обработка ошибок OAuth
console.error('OAuth error:', error);
} else {
// Успех! httpOnly cookie уже установлен
await auth.checkSession(); // Загружает из cookie
navigate('/dashboard');
}
```
## 🔍 Для микросервисов
### Подключение к Redis
@@ -78,23 +131,22 @@ results = await batch.batch_validate_tokens(token_list)
### HTTP заголовки
```python
# Извлечение токена из запроса
from auth.utils import extract_token_from_request, get_safe_headers
# Извлечение токена из запроса (cookie или Bearer)
from auth.utils import extract_token_from_request
token = await extract_token_from_request(request)
# Или вручную
headers = get_safe_headers(request)
token = headers.get("authorization", "").replace("Bearer ", "")
# Автоматически проверяет:
# 1. Authorization: Bearer <token>
# 2. Cookie: session_token=<token>
```
## 🎯 Основные компоненты
- **SessionTokenManager** - JWT сессии с Redis хранением
- **OAuthTokenManager** - OAuth access/refresh токены
- **SessionTokenManager** - JWT сессии с Redis хранением + httpOnly cookies
- **OAuthTokenManager** - OAuth access/refresh токены для API интеграций
- **BatchTokenOperations** - Массовые операции с токенами
- **TokenMonitoring** - Мониторинг и статистика
- **AuthMiddleware** - HTTP middleware для автоматической обработки
- **AuthMiddleware** - HTTP middleware с поддержкой cookies
## ⚡ Производительность
@@ -103,3 +155,79 @@ token = headers.get("authorization", "").replace("Bearer ", "")
- **Pipeline использование** для атомарности
- **SCAN** вместо KEYS для безопасности
- **TTL** автоматическая очистка истекших токенов
- **httpOnly cookies** - автоматическая отправка браузером
## 🛡️ Безопасность (2025)
### Максимальная защита:
- **🚫 Защита от XSS**: httpOnly cookies недоступны JavaScript
- **🔒 Защита от CSRF**: SameSite=lax cookies
- **🛡️ Единообразие**: Все типы авторизации через cookies
- **📱 Автоматическая отправка**: Браузер сам включает cookies
### Миграция с Bearer токенов:
- ✅ OAuth теперь использует httpOnly cookies (вместо localStorage)
- ✅ Email/Password использует httpOnly cookies (вместо Bearer)
- ✅ Фронтенд: `credentials: 'include'` во всех запросах
- ✅ Middleware поддерживает оба подхода для совместимости
## 🔧 Настройка
### Environment Variables
```bash
# 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
# Cookie настройки
SESSION_COOKIE_SECURE=true
SESSION_COOKIE_HTTPONLY=true
SESSION_COOKIE_SAMESITE=lax
SESSION_COOKIE_MAX_AGE=2592000 # 30 дней
# JWT
JWT_SECRET=your_jwt_secret_key
JWT_EXPIRATION_HOURS=720 # 30 дней
# Redis
REDIS_URL=redis://localhost:6379/0
```
### Быстрая проверка
```bash
# Проверка OAuth провайдеров
curl https://your-domain.com/oauth/google
# Проверка сессии
curl -b "session_token=your_token" https://your-domain.com/graphql \
-d '{"query":"query { getSession { success author { id } } }"}'
```
## 📊 Мониторинг
```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'] / 1024 / 1024:.2f} MB")
# Health check
health = await monitoring.health_check()
if health["status"] == "healthy":
print("✅ Auth system is healthy")
```
## 🎯 Результат архитектуры 2025
После внедрения httpOnly cookies:
-**OAuth**: Google/GitHub → httpOnly cookie → GraphQL запросы
-**Email/Password**: Login form → httpOnly cookie → GraphQL запросы
-**Единая архитектура**: Все через cookies + `credentials: 'include'`
-**Максимальная безопасность**: Защита от XSS и CSRF для всех типов авторизации
- ✅ **Простота**: Браузер автоматически управляет токенами

View File

@@ -1,12 +1,19 @@
# Архитектура системы авторизации Discours Core
# 🏗️ Архитектура системы авторизации Discours Core
## 🎯 Обзор архитектуры
## 🎯 Обзор архитектуры 2025
Модульная система авторизации с разделением ответственности между компонентами.
Модульная система авторизации с **httpOnly cookies** для максимальной безопасности и единообразия.
**Ключевые принципы:**
- **🍪 httpOnly cookies** для ВСЕХ типов авторизации (OAuth + Email/Password)
- **🛡️ Максимальная безопасность** - защита от XSS и CSRF
- **🔄 Единообразие** - один механизм для всех провайдеров
- **📱 Автоматическое управление** - браузер сам отправляет cookies
**Хранение данных:**
- **Токены** → Redis (сессии, OAuth, verification)
- **Пользователи** → PostgreSQL (основные данные + OAuth в JSON поле)
- **Сессии** → Redis (JWT токены) + httpOnly cookies (передача)
- **OAuth токены** → Redis (для API интеграций)
- **Пользователи** → PostgreSQL (основные данные + OAuth связи)
## 📊 Схема потоков данных
@@ -121,67 +128,90 @@ graph TB
OTM --> RESP
```
## 🔐 OAuth Flow
## 🔐 OAuth Flow (httpOnly cookies)
```mermaid
sequenceDiagram
participant U as User
participant F as Frontend
participant A as Auth Service
participant B as Backend
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
F->>B: GET /oauth/{provider}/login
B->>R: Store OAuth state (TTL: 10 min)
B->>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
P->>B: GET /oauth/{provider}/callback?code={code}&state={state}
B->>R: Verify state
B->>P: Exchange code for token
P->>B: Return access token + user data
B->>B: Create/update user
B->>B: Generate JWT session token
B->>R: Store session in Redis
B->>F: Redirect + Set httpOnly cookie
Note over B,F: Cookie: session_token=JWT<br/>HttpOnly, Secure, SameSite=lax
F->>U: User logged in (cookie automatic)
Note over F,B: All subsequent requests
F->>B: GraphQL with credentials: 'include'
Note over F,B: Browser automatically sends cookie
```
## 🔄 Session Management
## 🔄 Session Management (httpOnly cookies)
```mermaid
stateDiagram-v2
[*] --> Anonymous
Anonymous --> Authenticating: Login attempt
Authenticating --> Authenticated: Valid JWT + Redis session
Anonymous --> Authenticating: Login attempt (OAuth/Email)
Authenticating --> Authenticated: Valid JWT + httpOnly cookie set
Authenticating --> Anonymous: Invalid credentials
Authenticated --> Refreshing: Token near expiry
Refreshing --> Authenticated: Successful refresh
Refreshing --> Authenticated: New httpOnly cookie set
Refreshing --> Anonymous: Refresh failed
Authenticated --> Anonymous: Logout/Revoke
Authenticated --> Anonymous: Token expired
Authenticated --> Anonymous: Logout (cookie deleted)
Authenticated --> Anonymous: Token expired (cookie invalid)
note right of Authenticated
All requests include
httpOnly cookie automatically
via credentials: 'include'
end note
```
## 🗄️ Redis структура данных
```bash
# JWT Sessions
# JWT Sessions (основные - передаются через httpOnly cookies)
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 Tokens (для API интеграций - НЕ для аутентификации)
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
# OAuth State (временные - для CSRF защиты)
oauth_state:{state} # JSON: {provider, redirect_uri, code_verifier} TTL: 10 мин
# Verification Tokens (email подтверждения и т.д.)
verification_token:{token} # JSON: {user_id, type, data, created_at}
```
### 🔄 Изменения в архитектуре 2025:
**Убрано:**
- ❌ Токены в URL параметрах (небезопасно)
- ❌ localStorage для основных токенов (уязвимо к XSS)
- ❌ Bearer заголовки для веб-приложений (сложнее управлять)
**Добавлено:**
- ✅ httpOnly cookies для всех типов авторизации
- ✅ Автоматическая отправка cookies браузером
- ✅ SameSite защита от CSRF
- ✅ Secure flag для HTTPS
### Примеры Redis команд
```bash

View File

@@ -1,34 +1,285 @@
# OAuth Integration Guide
# 🔐 OAuth Integration Guide
## 🎯 Обзор
Система OAuth интеграции с поддержкой популярных провайдеров. Токены хранятся в Redis с автоматическим TTL и поддержкой refresh.
Система OAuth интеграции с **httpOnly cookies** для максимальной безопасности. Поддержка популярных провайдеров с единым подходом к аутентификации.
## 🚀 Быстрый старт
### 🔄 **Архитектура 2025: httpOnly cookies для всех**
### Поддерживаемые провайдеры
- **Google** ✅ - OpenID Connect (актуальные endpoints)
- **GitHub** ✅ - OAuth 2.0 (scope: read:user user:email)
- **Facebook** ✅ - Facebook Login API v18.0+ (scope: email public_profile)
- **VK** ✅ - VK OAuth API v5.199+ (scope: email)
- **X (Twitter)** ✅ - OAuth 2.0 API v2 (scope: tweet.read users.read)
- **Yandex** ✅ - Yandex OAuth (scope: login:email login:info login:avatar)
- **Telegram** ⚠️ - Telegram Login (специфическая реализация)
```mermaid
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant P as OAuth Provider
### Redis структура
```bash
oauth_access:{user_id}:{provider} # Access токены
oauth_refresh:{user_id}:{provider} # Refresh токены
oauth_state:{state} # OAuth state с TTL 10 минут
U->>F: Click "Login with Google"
F->>B: GET /oauth/google/login
B->>P: Redirect to Provider
P->>U: Show authorization page
U->>P: Grant permission
P->>B: GET /oauth/google/callback?code=xxx
B->>P: Exchange code for token
P->>B: Return access token + user data
B->>B: Create JWT session
B->>F: Redirect + Set httpOnly cookie
F->>U: User logged in (cookie automatic)
```
### Основные операции
## 🚀 Поддерживаемые провайдеры
| Провайдер | Статус | Особенности |
|-----------|--------|-------------|
| **Google** | ✅ | OpenID Connect, актуальные endpoints |
| **GitHub** | ✅ | OAuth 2.0, scope: `read:user user:email` |
| **Yandex** | ✅ | OAuth, scope: `login:email login:info` |
| **VK** | ✅ | OAuth API v5.199+, scope: `email` |
| **Facebook** | ✅ | Facebook Login API v18.0+ |
| **X (Twitter)** | ✅ | OAuth 2.0 API v2 |
## 🔧 OAuth Flow
### 1. 🚀 Инициация OAuth (Фронтенд)
```typescript
// Простой редирект - backend получит redirect_uri из Referer header
const handleOAuthLogin = (provider: string) => {
// Сохраняем текущую страницу для возврата
localStorage.setItem('oauth_return_url', window.location.pathname);
// Редиректим на OAuth endpoint
window.location.href = `/oauth/${provider}/login`;
};
// Использование
<button onClick={() => handleOAuthLogin('google')}>
🔐 Войти через Google
</button>
```
### 2. 🔄 Backend Endpoints
#### GET `/oauth/{provider}/login` - Старт OAuth
```python
# /oauth/github/login
# 1. Сохраняет redirect_uri из Referer header в Redis state
# 2. Генерирует PKCE challenge для безопасности
# 3. Редиректит на провайдера с параметрами авторизации
```
#### GET `/oauth/{provider}/callback` - Callback
```python
# GitHub → /oauth/github/callback?code=xxx&state=yyy
# 1. Валидирует state (CSRF защита)
# 2. Обменивает code на access_token
# 3. Получает профиль пользователя
# 4. Создает/обновляет пользователя в БД
# 5. Создает JWT сессию
# 6. Устанавливает httpOnly cookie
# 7. Редиректит на фронтенд БЕЗ токена в URL
```
### 3. 🌐 Фронтенд финализация
```typescript
// OAuth callback route (/oauth/callback или аналогичный)
export default function OAuthCallback() {
const navigate = useNavigate();
const auth = useAuth();
onMount(async () => {
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
if (error) {
// ❌ Ошибка OAuth
console.error('OAuth error:', error);
switch (error) {
case 'access_denied':
alert('Доступ отклонен провайдером');
break;
case 'oauth_state_expired':
alert('Сессия OAuth истекла. Попробуйте еще раз.');
break;
default:
alert('Ошибка авторизации. Попробуйте еще раз.');
}
navigate('/login');
} else {
// ✅ Успех! httpOnly cookie уже установлен
try {
// Проверяем сессию (cookie отправится автоматически)
await auth.checkSession();
if (auth.isAuthenticated()) {
// Возвращаемся на сохраненную страницу
const returnUrl = localStorage.getItem('oauth_return_url') || '/';
localStorage.removeItem('oauth_return_url');
navigate(returnUrl);
} else {
throw new Error('Session validation failed');
}
} catch (error) {
console.error('Failed to validate session:', error);
navigate('/login?error=session_failed');
}
}
});
return (
<div class="oauth-callback">
<h2>Завершение авторизации...</h2>
<p>Пожалуйста, подождите...</p>
</div>
);
}
```
### 4. 🍪 Единая аутентификация через httpOnly cookie
```typescript
// GraphQL клиент использует httpOnly cookie
const graphqlRequest = async (query: string, variables?: any) => {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // ✅ КРИТИЧНО: отправляет httpOnly cookie
body: JSON.stringify({ query, variables })
});
return response.json();
};
// Auth Context
export const AuthProvider = (props: { children: JSX.Element }) => {
const [user, setUser] = createSignal<User | null>(null);
const checkSession = async () => {
try {
const response = await graphqlRequest(`
query GetSession {
getSession {
success
author { id slug email name }
}
}
`);
if (response.data?.getSession?.success) {
setUser(response.data.getSession.author);
} else {
setUser(null);
}
} catch (error) {
console.error('Session check failed:', error);
setUser(null);
}
};
const logout = async () => {
try {
// Удаляем httpOnly cookie на бэкенде
await graphqlRequest(`mutation { logout { success } }`);
} catch (error) {
console.error('Logout error:', error);
}
setUser(null);
window.location.href = '/';
};
// Проверяем сессию при загрузке
onMount(() => checkSession());
return (
<AuthContext.Provider value={{
user,
isAuthenticated: () => !!user(),
checkSession,
logout,
}}>
{props.children}
</AuthContext.Provider>
);
};
```
## 🔐 Настройка провайдеров
### Google OAuth
1. [Google Cloud Console](https://console.cloud.google.com/)
2. **APIs & Services****Credentials****OAuth 2.0 Client ID**
3. **Authorized redirect URIs**: `https://your-domain.com/oauth/google/callback`
```bash
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
```
### GitHub OAuth
1. [GitHub Developer Settings](https://github.com/settings/developers)
2. **New OAuth App**
3. **Authorization callback URL**: `https://your-domain.com/oauth/github/callback`
```bash
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
```
### Yandex OAuth
1. [Yandex OAuth](https://oauth.yandex.ru/)
2. **Создать новое приложение**
3. **Callback URI**: `https://your-domain.com/oauth/yandex/callback`
4. **Права**: `login:info`, `login:email`, `login:avatar`
```bash
YANDEX_CLIENT_ID=your_yandex_client_id
YANDEX_CLIENT_SECRET=your_yandex_client_secret
```
### VK OAuth
1. [VK Developers](https://dev.vk.com/apps)
2. **Создать приложение****Веб-сайт**
3. **Redirect URI**: `https://your-domain.com/oauth/vk/callback`
```bash
VK_CLIENT_ID=your_vk_app_id
VK_CLIENT_SECRET=your_vk_secure_key
```
## 🛡️ Безопасность
### httpOnly Cookie настройки
```python
# settings.py
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 # 30 дней
```
### CSRF Protection
- **State parameter**: Криптографически стойкий state для каждого запроса
- **PKCE**: Code challenge для дополнительной защиты
- **Redirect URI validation**: Проверка разрешенных доменов
### TTL и истечение
- **OAuth state**: 10 минут (одноразовое использование)
- **Session tokens**: 30 дней (настраивается)
- **Автоматическая очистка**: Redis удаляет истекшие токены
## 🔧 API для разработчиков
### Проверка OAuth токенов
```python
from auth.tokens.oauth import OAuthTokenManager
oauth = OAuthTokenManager()
# Сохранение токенов
# Сохранение OAuth токенов (для API интеграций)
await oauth.store_oauth_tokens(
user_id="123",
provider="google",
@@ -37,584 +288,98 @@ await oauth.store_oauth_tokens(
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 (Фронтенд)
```javascript
// Простой вызов без параметров - backend получит redirect_uri из Referer header
const oauth = (provider: string) => {
window.location.href = `https://v3.dscrs.site/oauth/${provider}`
}
```
### 2. Backend Endpoints
#### GET `/oauth/{provider}` - Старт OAuth
```python
# v3.dscrs.site/oauth/github
# 1. Сохраняет redirect_uri из Referer header в Redis state
# 2. Редиректит на провайдера с PKCE challenge
```
#### GET `/oauth/{provider}/callback` - Callback
```python
# GitHub → v3.dscrs.site/oauth/github/callback?code=xxx&state=yyy
# 1. Обменивает code на access_token
# 2. Получает профиль пользователя
# 3. Создает/обновляет пользователя
# 4. Создает JWT сессию
# 5. Устанавливает httpOnly cookie (для GraphQL)
# 6. Редиректит на https://testing.discours.io/oauth?redirect_url=... (JWT в httpOnly cookie)
```
### 3. Фронтенд финализация
```javascript
// https://testing.discours.io/oauth роут
const urlParams = new URLSearchParams(window.location.search)
const error = urlParams.get('error')
const redirectUrl = urlParams.get('redirect_url') || '/'
if (error) {
// Обработка ошибок OAuth
console.error('OAuth error:', error)
alert('Authentication failed. Please try again.')
window.location.href = '/'
} else {
// Нет ошибки = успех! JWT уже в httpOnly cookie
// SessionProvider загружает сессию из cookie
await sessionProvider.loadSession()
// Редиректим на исходную страницу
window.location.href = redirectUrl
}
```
### 4. Единая аутентификация через httpOnly cookie
```javascript
// GraphQL клиент использует httpOnly cookie
const client = new ApolloClient({
uri: 'https://v3.dscrs.site/graphql',
credentials: 'include', // ✅ Отправляет httpOnly cookie
})
// Все API вызовы также используют httpOnly cookie
fetch('/api/endpoint', {
credentials: 'include' // ✅ Отправляет httpOnly cookie
})
```
### 4. Настройки провайдеров (админки)
- **GitHub**: `https://v3.dscrs.site/oauth/github/callback`
- **Google**: `https://v3.dscrs.site/oauth/google/callback`
- **Twitter**: `https://v3.dscrs.site/oauth/twitter/callback`
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 `/oauth/{provider}/callback`
```python
@router.get("/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"),
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
"scope": "openid email profile"
}
```
**✅ Преимущества OpenID Connect:**
- Автоматическое обнаружение endpoints через `.well-known/openid-configuration`
- Поддержка актуальных стандартов безопасности
- Автоматические обновления при изменениях Google API
### 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"
}
```
**⚠️ Важные требования GitHub:**
- Scope `user:email` **обязателен** для получения email адреса
- Проверяйте rate limits (5000 запросов/час для авторизованных пользователей)
- Используйте `User-Agent` header во всех запросах к API
### 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",
"token_endpoint_auth_method": "client_secret_post" # Требование Facebook
}
```
**⚠️ Важные требования Facebook:**
- Используйте **минимум API v18.0**
- Обязательно настройте **точные Redirect URIs** в Facebook App
- Приложение должно быть в режиме **"Live"** для работы с реальными пользователями
- **HTTPS обязателен** для production окружения
### 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",
"api_version": "5.199" # Актуальная версия API
}
```
**⚠️ Важные требования VK:**
- Используйте **API версию 5.199+** (5.131 устарела)
- Scope `email` необходим для получения email адреса
- Redirect URI должен **точно совпадать** с настройками в приложении VK
- Поддерживаются только HTTPS redirect URI в production
### X (Twitter) OAuth
```python
X_OAUTH_CONFIG = {
"client_id": os.getenv("X_CLIENT_ID"),
"client_secret": os.getenv("X_CLIENT_SECRET"),
"auth_url": "https://twitter.com/i/oauth2/authorize",
"token_url": "https://api.twitter.com/2/oauth2/token",
"user_info_url": "https://api.twitter.com/2/users/me",
"scope": "tweet.read users.read"
}
```
**⚠️ Важные требования X:**
- Используйте **API v2** endpoints
- Scope `users.read` обязателен для получения профиля
- Email недоступен через публичное API
- Требуется верификация приложения для production
### 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 login:avatar"
}
```
**⚠️ Важные требования Yandex:**
- Scope `login:email` для получения email
- Scope `login:info` для базовой информации профиля
- Scope `login:avatar` для получения аватара
- Поддержка только HTTPS redirect URI
### Telegram OAuth
```python
TELEGRAM_OAUTH_CONFIG = {
"client_id": os.getenv("TELEGRAM_CLIENT_ID"),
"client_secret": os.getenv("TELEGRAM_CLIENT_SECRET"),
"auth_url": "https://oauth.telegram.org/auth",
"token_url": "https://oauth.telegram.org/auth/request",
"scope": "read"
}
```
**⚠️ Важные требования Telegram:**
- Специальная настройка через @BotFather
- Email недоступен - используется временный email
- Получение номера телефона требует дополнительных разрешений
## 🔒 Безопасность
### 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()
# Делаем запрос
# Получение токена для API вызовов
token_data = await oauth.get_token("123", "google", "oauth_access")
if token_data:
# Используем токен для вызовов Google API
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
### Redis структура
```bash
# Google OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
# OAuth токены для API интеграций
oauth_access:{user_id}:{provider} # Access токен
oauth_refresh:{user_id}:{provider} # Refresh токен
# GitHub OAuth
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
# OAuth state (временный)
oauth_state:{state} # Данные авторизации (TTL: 10 мин)
# 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
# X (Twitter) OAuth
X_CLIENT_ID=your_x_client_id
X_CLIENT_SECRET=your_x_client_secret
# Yandex OAuth
YANDEX_CLIENT_ID=your_yandex_client_id
YANDEX_CLIENT_SECRET=your_yandex_client_secret
# Telegram OAuth
TELEGRAM_CLIENT_ID=your_telegram_client_id
TELEGRAM_CLIENT_SECRET=your_telegram_client_secret
# HTTPS настройки
HTTPS_ENABLED=true # false для разработки
# 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/oauth/facebook/callback`
5. Переключить приложение в режим "Live"
#### X (Twitter) OAuth
1. Перейти в [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard)
2. Создать новое приложение
3. Настроить OAuth 2.0 settings
4. Добавить Callback URLs:
- `https://your-domain.com/oauth/x/callback`
5. Получить Client ID и Client Secret
#### VK OAuth
1. Перейти в [VK Developers](https://vk.com/dev)
2. Создать новое приложение типа "Веб-сайт"
3. Настроить "Доверенный redirect URI":
- `https://your-domain.com/oauth/vk/callback`
4. Получить ID приложения и Защищённый ключ
#### Yandex OAuth
1. Перейти в [Yandex OAuth](https://oauth.yandex.ru/)
2. Создать новое приложение
3. Настроить Callback URL:
- `https://your-domain.com/oauth/yandex/callback`
4. Выбрать необходимые права доступа
5. Получить ID и пароль приложения
#### Telegram OAuth
1. Создать бота через @BotFather
2. Получить Bot Token
3. Настроить OAuth через Telegram API
4. **Внимание**: Telegram OAuth имеет специфическую реализацию
### 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:*"
# Сессии пользователей (основные)
session:{user_id}:{token} # JWT сессия (TTL: 30 дней)
```
## 🧪 Тестирование
### 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
### E2E Test
```typescript
// tests/oauth.spec.ts
test('OAuth flow with Google', async ({ page }) => {
await page.goto('/login')
test('OAuth flow with httpOnly cookies', async ({ page }) => {
// 1. Инициация OAuth
await page.goto('/login');
await page.click('[data-testid="google-login"]');
// Click Google OAuth button
await page.click('[data-testid="oauth-google"]')
// 2. Проверяем редирект на Google
await expect(page).toHaveURL(/accounts\.google\.com/);
// Should redirect to Google
await page.waitForURL(/accounts\.google\.com/)
// 3. Симулируем успешный callback (в тестовой среде)
await page.goto('/oauth/callback');
// Mock successful OAuth (in test environment)
await page.goto('/?state=test&access_token=mock_token')
// 4. Проверяем что cookie установлен
const cookies = await page.context().cookies();
const authCookie = cookies.find(c => c.name === 'session_token');
expect(authCookie).toBeTruthy();
expect(authCookie?.httpOnly).toBe(true);
// Should be logged in
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible()
})
// 5. Проверяем что пользователь авторизован
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"
# Проверка OAuth провайдеров
curl -v "https://your-domain.com/oauth/google/login"
# Frontend логи (browser console)
# Фильтр: "[oauth]" или "[SessionProvider]"
# Проверка callback
curl -v "https://your-domain.com/oauth/google/callback?code=test&state=test"
# Проверка сессии с cookie
curl -b "session_token=your_token" "https://your-domain.com/graphql" \
-d '{"query":"query { getSession { success author { id } } }"}'
```
## 📊 Мониторинг
```python
# Добавить метрики для мониторинга
from prometheus_client import Counter, Histogram
from auth.tokens.monitoring import TokenMonitoring
oauth_requests = Counter('oauth_requests_total', 'OAuth requests', ['provider', 'status'])
oauth_duration = Histogram('oauth_duration_seconds', 'OAuth request duration')
monitoring = TokenMonitoring()
@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
# Статистика OAuth
stats = await monitoring.get_token_statistics()
oauth_tokens = stats.get("oauth_access_tokens", 0) + stats.get("oauth_refresh_tokens", 0)
print(f"OAuth tokens: {oauth_tokens}")
# Health check
health = await monitoring.health_check()
if health["status"] == "healthy":
print("✅ OAuth system is healthy")
```
## 🎯 Преимущества новой архитектуры
### 🛡️ Максимальная безопасность:
- **🚫 Защита от XSS**: Токены недоступны JavaScript
- **🔒 Защита от CSRF**: SameSite cookies
- **🛡️ Единообразие**: Все провайдеры используют один механизм
### 🚀 Простота использования:
- **📱 Автоматическая отправка**: Браузер сам включает cookies
- **🧹 Чистый код**: Нет управления токенами в JavaScript
- **🔄 Единый API**: Один GraphQL клиент для всех случаев
### ⚡ Производительность:
- **🚀 Быстрее**: Нет localStorage операций
- **📦 Меньше кода**: Упрощенная логика фронтенда
- **🔄 Автоматическое управление**: Браузер оптимизирует отправку cookies
**Результат: Самая безопасная и простая OAuth интеграция!** 🔐✨

267
docs/auth/setup.md Normal file
View File

@@ -0,0 +1,267 @@
# 🔧 Настройка системы аутентификации
## 🎯 Быстрая настройка
### 1. Environment Variables
```bash
# JWT настройки
JWT_SECRET=your_super_secret_key_minimum_256_bits
JWT_ALGORITHM=HS256
JWT_EXPIRATION_HOURS=720 # 30 дней
# Cookie настройки (httpOnly для безопасности)
SESSION_COOKIE_NAME=session_token
SESSION_COOKIE_HTTPONLY=true
SESSION_COOKIE_SECURE=true # Только HTTPS в продакшене
SESSION_COOKIE_SAMESITE=lax # CSRF защита
SESSION_COOKIE_MAX_AGE=2592000 # 30 дней
# Redis
REDIS_URL=redis://localhost:6379/0
REDIS_SOCKET_KEEPALIVE=true
REDIS_HEALTH_CHECK_INTERVAL=30
# 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
YANDEX_CLIENT_ID=your_yandex_client_id
YANDEX_CLIENT_SECRET=your_yandex_client_secret
VK_CLIENT_ID=your_vk_app_id
VK_CLIENT_SECRET=your_vk_secure_key
# Безопасность
RATE_LIMIT_ENABLED=true
MAX_LOGIN_ATTEMPTS=5
LOCKOUT_DURATION=1800 # 30 минут
```
### 2. OAuth Провайдеры
#### Google OAuth
1. [Google Cloud Console](https://console.cloud.google.com/)
2. **APIs & Services****Credentials****Create OAuth 2.0 Client ID**
3. **Authorized redirect URIs**:
- `https://your-domain.com/oauth/google/callback` (продакшн)
- `http://localhost:8000/oauth/google/callback` (разработка)
#### GitHub OAuth
1. [GitHub Developer Settings](https://github.com/settings/developers)
2. **New OAuth App**
3. **Authorization callback URL**: `https://your-domain.com/oauth/github/callback`
#### Yandex OAuth
1. [Yandex OAuth](https://oauth.yandex.ru/)
2. **Создать новое приложение**
3. **Callback URI**: `https://your-domain.com/oauth/yandex/callback`
4. **Права**: `login:info`, `login:email`, `login:avatar`
#### VK OAuth
1. [VK Developers](https://dev.vk.com/apps)
2. **Создать приложение****Веб-сайт**
3. **Redirect URI**: `https://your-domain.com/oauth/vk/callback`
### 3. Проверка настройки
```bash
# Проверка переменных окружения
python -c "
import os
required = ['JWT_SECRET', 'REDIS_URL', 'GOOGLE_CLIENT_ID']
for var in required:
print(f'{var}: {\"✅\" if os.getenv(var) else \"❌\"}')"
# Проверка Redis подключения
python -c "
import asyncio
from storage.redis import redis
async def test():
result = await redis.ping()
print(f'Redis: {\"✅\" if result else \"❌\"}')
asyncio.run(test())"
# Проверка OAuth провайдеров
curl -v "https://your-domain.com/oauth/google/login"
```
## 🔒 Безопасность в продакшене
### SSL/HTTPS настройки
```bash
# Принудительное HTTPS
FORCE_HTTPS=true
HSTS_MAX_AGE=31536000
# Secure cookies только для HTTPS
SESSION_COOKIE_SECURE=true
```
### Rate Limiting
```bash
RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=3600 # 1 час
```
### Account Lockout
```bash
MAX_LOGIN_ATTEMPTS=5
LOCKOUT_DURATION=1800 # 30 минут
```
## 🐛 Диагностика проблем
### Частые ошибки
#### "Provider not configured"
```bash
# Проверить переменные окружения
echo $GOOGLE_CLIENT_ID
echo $GOOGLE_CLIENT_SECRET
# Перезапустить приложение после установки переменных
```
#### "redirect_uri_mismatch"
- Проверить точное соответствие URL в настройках провайдера
- Убедиться что протокол (http/https) совпадает
- Callback URL должен указывать на backend, НЕ на frontend
#### "Cookies не работают"
```bash
# Проверить настройки cookie
curl -v -b "session_token=test" "https://your-domain.com/graphql"
# Проверить что фронтенд отправляет credentials
# В коде должно быть: credentials: 'include'
```
#### "CORS ошибки"
```python
# В настройках CORS должно быть:
allow_credentials=True
allow_origins=["https://your-frontend-domain.com"]
```
### Логи для отладки
```bash
# Поиск ошибок аутентификации
grep -i "auth\|oauth\|cookie" /var/log/app/app.log
# Мониторинг Redis операций
redis-cli monitor | grep "session\|oauth"
```
## 📊 Мониторинг
### Health Check
```python
from auth.tokens.monitoring import TokenMonitoring
async def auth_health():
monitoring = TokenMonitoring()
health = await monitoring.health_check()
stats = await monitoring.get_token_statistics()
return {
"status": health["status"],
"redis_connected": health["redis_connected"],
"active_sessions": stats["session_tokens"],
"memory_usage_mb": stats["memory_usage"] / 1024 / 1024
}
```
### Метрики для мониторинга
- Количество активных сессий
- Успешность OAuth авторизаций
- Rate limit нарушения
- Заблокированные аккаунты
- Использование памяти Redis
## 🧪 Тестирование
### Unit тесты
```bash
# Запуск auth тестов
pytest tests/auth/ -v
# Проверка типов
mypy auth/
```
### E2E тесты
```bash
# Тестирование OAuth flow
playwright test tests/oauth.spec.ts
# Тестирование cookie аутентификации
playwright test tests/auth-cookies.spec.ts
```
### Нагрузочное тестирование
```bash
# Тестирование login endpoint
ab -n 1000 -c 10 -p login.json -T application/json http://localhost:8000/graphql
# Содержимое login.json:
# {"query":"mutation{login(email:\"test@example.com\",password:\"password\"){success}}"}
```
## 🚀 Развертывание
### Docker
```dockerfile
# Dockerfile
ENV JWT_SECRET=your_secret_here
ENV REDIS_URL=redis://redis:6379/0
ENV SESSION_COOKIE_SECURE=true
```
### Dokku/Heroku
```bash
# Установка переменных окружения
dokku config:set myapp JWT_SECRET=xxx REDIS_URL=yyy
heroku config:set JWT_SECRET=xxx REDIS_URL=yyy
```
### Nginx настройки
```nginx
# Поддержка cookies
proxy_set_header Cookie $http_cookie;
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=lax";
# CORS для credentials
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Origin https://your-frontend.com;
```
## ✅ Checklist для продакшена
### Безопасность
- [ ] JWT secret минимум 256 бит
- [ ] HTTPS принудительно включен
- [ ] httpOnly cookies настроены
- [ ] SameSite cookies включены
- [ ] Rate limiting активен
- [ ] Account lockout настроен
### OAuth
- [ ] Все провайдеры настроены
- [ ] Redirect URIs правильные
- [ ] Client secrets безопасно хранятся
- [ ] PKCE включен для поддерживающих провайдеров
### Мониторинг
- [ ] Health checks настроены
- [ ] Логирование работает
- [ ] Метрики собираются
- [ ] Алерты настроены
### Производительность
- [ ] Redis connection pooling
- [ ] TTL для всех ключей
- [ ] Batch операции для массовых действий
- [ ] Memory optimization включена
**Готово к продакшену!** 🚀✅

View File

@@ -0,0 +1,414 @@
# 📡 SSE + httpOnly Cookies Integration
## 🎯 Обзор
Server-Sent Events (SSE) **отлично работают** с httpOnly cookies! Браузер автоматически отправляет cookies при установке SSE соединения.
## 🔄 Как это работает
### 1. 🚀 Установка SSE соединения
```typescript
// Фронтенд - SSE с cross-origin поддоменом
const eventSource = new EventSource('https://connect.discours.io/notifications', {
withCredentials: true // ✅ КРИТИЧНО: отправляет httpOnly cookies cross-origin
});
// Для продакшена
const SSE_URL = process.env.NODE_ENV === 'production'
? 'https://connect.discours.io/'
: 'https://connect.discours.io/';
const eventSource = new EventSource(SSE_URL, {
withCredentials: true // ✅ Обязательно для cross-origin cookies
});
```
### 2. 🔧 Backend SSE endpoint с аутентификацией
```python
# main.py - добавляем SSE endpoint
from starlette.responses import StreamingResponse
from auth.middleware import auth_middleware
@app.route("/sse/notifications")
async def sse_notifications(request: Request):
"""SSE endpoint для real-time уведомлений"""
# ✅ Аутентификация через httpOnly cookie
user_data = await auth_middleware.authenticate_user(request)
if not user_data:
return Response("Unauthorized", status_code=401)
user_id = user_data.get("user_id")
async def event_stream():
"""Генератор SSE событий"""
try:
# Подписываемся на Redis каналы пользователя
channels = [
f"notifications:{user_id}",
f"follower:{user_id}",
f"shout:{user_id}"
]
pubsub = redis.pubsub()
await pubsub.subscribe(*channels)
# Отправляем initial heartbeat
yield f"data: {json.dumps({'type': 'connected', 'user_id': user_id})}\n\n"
async for message in pubsub.listen():
if message['type'] == 'message':
# Форматируем SSE событие
data = message['data'].decode('utf-8')
yield f"data: {data}\n\n"
except asyncio.CancelledError:
await pubsub.unsubscribe()
await pubsub.close()
except Exception as e:
logger.error(f"SSE error for user {user_id}: {e}")
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Credentials": "true", # Для CORS
}
)
```
### 3. 🌐 Фронтенд SSE клиент
```typescript
// SSE клиент с автоматической аутентификацией через cookies
class SSEClient {
private eventSource: EventSource | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
connect() {
try {
// ✅ Cross-origin SSE с cookies
const SSE_URL = process.env.NODE_ENV === 'production'
? 'https://connect.discours.io/sse/notifications'
: 'https://connect.discours.io/sse/notifications';
this.eventSource = new EventSource(SSE_URL, {
withCredentials: true // ✅ КРИТИЧНО для cross-origin cookies
});
this.eventSource.onopen = () => {
console.log('✅ SSE connected');
this.reconnectAttempts = 0;
};
this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleNotification(data);
} catch (error) {
console.error('SSE message parse error:', error);
}
};
this.eventSource.onerror = (error) => {
console.error('SSE error:', error);
// Если получили 401 - cookie недействителен
if (this.eventSource?.readyState === EventSource.CLOSED) {
this.handleAuthError();
} else {
this.handleReconnect();
}
};
} catch (error) {
console.error('SSE connection error:', error);
this.handleReconnect();
}
}
private handleNotification(data: any) {
switch (data.type) {
case 'connected':
console.log(`SSE connected for user: ${data.user_id}`);
break;
case 'follower':
this.handleFollowerNotification(data);
break;
case 'shout':
this.handleShoutNotification(data);
break;
case 'error':
console.error('SSE server error:', data.message);
break;
}
}
private handleAuthError() {
console.warn('SSE authentication failed - redirecting to login');
// Cookie недействителен - редиректим на login
window.location.href = '/login?error=session_expired';
}
private handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.pow(2, this.reconnectAttempts) * 1000; // Exponential backoff
console.log(`Reconnecting SSE in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
this.disconnect();
this.connect();
}, delay);
} else {
console.error('Max SSE reconnect attempts reached');
}
}
disconnect() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
private handleFollowerNotification(data: any) {
// Обновляем UI при новом подписчике
if (data.action === 'create') {
showNotification(`${data.payload.follower_name} подписался на вас!`);
updateFollowersCount(+1);
}
}
private handleShoutNotification(data: any) {
// Обновляем UI при новых публикациях
if (data.action === 'create') {
showNotification(`Новая публикация: ${data.payload.title}`);
refreshFeed();
}
}
}
// Использование в приложении
const sseClient = new SSEClient();
// Подключаемся после успешной аутентификации
const auth = useAuth();
if (auth.isAuthenticated()) {
sseClient.connect();
}
// Отключаемся при logout
auth.onLogout(() => {
sseClient.disconnect();
});
```
## 🔧 Интеграция с существующей системой
### SSE сервер на connect.discours.io
```python
# connect.discours.io / connect.discours.io - отдельный SSE сервер
from starlette.applications import Starlette
from starlette.middleware.cors import CORSMiddleware
from starlette.routing import Route
# SSE приложение
sse_app = Starlette(
routes=[
# ✅ Единственный endpoint - SSE notifications
Route("/sse/notifications", sse_notifications, methods=["GET"]),
Route("/health", health_check, methods=["GET"]),
],
middleware=[
# ✅ CORS для cross-origin cookies
Middleware(
CORSMiddleware,
allow_origins=[
"https://testing.discours.io",
"https://discours.io",
"https://new.discours.io",
"http://localhost:3000", # dev
],
allow_credentials=True, # ✅ Разрешаем cookies
allow_methods=["GET", "OPTIONS"],
allow_headers=["*"],
),
],
)
# Основной сервер остается без изменений
# main.py - БЕЗ SSE routes
app = Starlette(
routes=[
Route("/graphql", graphql_handler, methods=["GET", "POST", "OPTIONS"]),
Route("/oauth/{provider}/callback", oauth_callback_http, methods=["GET"]),
Route("/oauth/{provider}", oauth_login_http, methods=["GET"]),
# SSE НЕ здесь - он на отдельном поддомене!
],
middleware=middleware,
lifespan=lifespan,
)
```
### Используем существующую notify систему
```python
# services/notify.py - уже готова!
# Ваша система уже отправляет уведомления в Redis каналы:
async def notify_follower(follower, author_id, action="follow"):
channel_name = f"follower:{author_id}"
data = {
"type": "follower",
"action": "create" if action == "follow" else "delete",
"entity": "follower",
"payload": {
"follower_id": follower["id"],
"follower_name": follower["name"],
"following_id": author_id,
}
}
# ✅ Отправляем в Redis - SSE endpoint получит автоматически
await redis.publish(channel_name, orjson.dumps(data))
```
## 🛡️ Безопасность SSE + httpOnly cookies
### Преимущества:
- **🚫 Защита от XSS**: Токены недоступны JavaScript
- **🔒 Автоматическая аутентификация**: Браузер сам отправляет cookies
- **🛡️ CSRF защита**: SameSite cookies
- **📱 Простота**: Нет управления токенами в JavaScript
### CORS настройки для cross-origin SSE:
```python
# connect.discours.io / connect.discours.io - CORS для SSE
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://testing.discours.io",
"https://discours.io",
"https://new.discours.io",
# Для разработки
"http://localhost:3000",
"http://localhost:3001",
],
allow_credentials=True, # ✅ КРИТИЧНО: разрешает отправку cookies cross-origin
allow_methods=["GET", "OPTIONS"], # SSE использует GET + preflight OPTIONS
allow_headers=["*"],
)
```
### Cookie Domain настройки:
```python
# settings.py - Cookie должен работать для всех поддоменов
SESSION_COOKIE_DOMAIN = ".discours.io" # ✅ Работает для всех поддоменов
SESSION_COOKIE_SECURE = True # ✅ Только HTTPS
SESSION_COOKIE_SAMESITE = "none" # ✅ Для cross-origin (но secure!)
# Для продакшена
if PRODUCTION:
SESSION_COOKIE_DOMAIN = ".discours.io"
```
## 🧪 Тестирование SSE + cookies
```typescript
// Тест SSE соединения
test('SSE connects with httpOnly cookies', async ({ page }) => {
// 1. Авторизуемся (cookie устанавливается)
await page.goto('/login');
await loginWithEmail(page, 'test@example.com', 'password');
// 2. Проверяем что cookie установлен
const cookies = await page.context().cookies();
const authCookie = cookies.find(c => c.name === 'session_token');
expect(authCookie).toBeTruthy();
// 3. Тестируем cross-origin SSE соединение
const sseConnected = await page.evaluate(() => {
return new Promise((resolve) => {
const eventSource = new EventSource('https://connect.discours.io/', {
withCredentials: true // ✅ Отправляем cookies cross-origin
});
eventSource.onopen = () => {
resolve(true);
eventSource.close();
};
eventSource.onerror = () => {
resolve(false);
eventSource.close();
};
// Timeout после 5 секунд
setTimeout(() => {
resolve(false);
eventSource.close();
}, 5000);
});
});
expect(sseConnected).toBe(true);
});
```
## 📊 Мониторинг SSE соединений
```python
# Добавляем метрики SSE
from collections import defaultdict
sse_connections = defaultdict(int)
async def sse_notifications(request: Request):
user_data = await auth_middleware.authenticate_user(request)
if not user_data:
return Response("Unauthorized", status_code=401)
user_id = user_data.get("user_id")
# Увеличиваем счетчик соединений
sse_connections[user_id] += 1
logger.info(f"SSE connected: user_id={user_id}, total_connections={sse_connections[user_id]}")
try:
async def event_stream():
# ... SSE логика ...
pass
return StreamingResponse(event_stream(), media_type="text/event-stream")
finally:
# Уменьшаем счетчик при отключении
sse_connections[user_id] -= 1
logger.info(f"SSE disconnected: user_id={user_id}, remaining_connections={sse_connections[user_id]}")
```
## 🎯 Результат
**SSE + httpOnly cookies = Идеальное сочетание для real-time уведомлений:**
-**Безопасность**: Максимальная защита от XSS/CSRF
-**Простота**: Автоматическая аутентификация
-**Производительность**: Нет дополнительных HTTP запросов для аутентификации
-**Надежность**: Браузер сам управляет отправкой cookies
-**Совместимость**: Работает со всеми современными браузерами
**Ваша существующая notify система готова к работе с SSE!** 📡🍪✨

View File

@@ -1,72 +0,0 @@
# 🔍 OAuth Debug Checklist
## 🚨 Проблема: Google callback получает параметры, но фронтенд получает auth_failed
### ✅ Диагностика выполнена:
1. **✅ OAuth Endpoint существует**: `/oauth/google/callback` - роут настроен в `main.py`
2. **⚠️ OAuth провайдеры**: Настроены на продакшн сервере (Dokku), локально отсутствуют
3. **✅ Логирование добавлено**: Детальные логи на каждом этапе OAuth flow
### 🎯 Правильный подход к диагностике продакшн проблем:
### 🔧 Исправления внесены:
1. **Улучшена обработка ошибок**: Правильный redirect на testing.discours.io с error параметрами
2. **Добавлена диагностика**: Проверка конфигурации OAuth провайдеров
3. **Создан скрипт проверки**: `scripts/check_oauth_config.py`
### 🎯 Диагностика продакшн OAuth проблем:
#### 1. Анализ продакшн логов (приоритет):
```bash
# Смотрим логи OAuth callback на продакшн сервере
dokku logs discours --tail 100 | grep -E "(OAuth|callback|google)"
# Ищем конкретные ошибки в логах
dokku logs discours --tail 500 | grep -E "(❌|ERROR|Exception)"
```
#### 2. Проверка переменных окружения на сервере:
```bash
# Проверяем настройки OAuth на продакшн
dokku config:show discours | grep -E "(GOOGLE|GITHUB|OAUTH)"
```
#### 3. Проверка redirect URI в Google Console:
- Должен быть: `https://v3.dscrs.site/oauth/google/callback`
- Проверить точное совпадение URL
- Убедиться что HTTPS включен
#### 4. Тестирование с детальными логами:
- Логи уже добавлены в код
- Смотреть продакшн логи во время OAuth попытки
- Анализировать каждый этап: token exchange, profile fetch, user creation
### 🔍 Логи для мониторинга:
После настройки OAuth провайдеров, логи должны показывать:
```
✅ Got access token for google: True
✅ Got user profile for google: id=..., email=..., name=...
✅ User created/updated for google: user_id=..., email=...
✅ Session token created for google: token_length=...
🔗 OAuth redirect URL: https://testing.discours.io/oauth?redirect_url=...
OAuth успешно завершен для google, user_id=...
```
### 🚨 Критические проверки:
1. **Redirect URI в Google Console** должен точно совпадать с `https://v3.dscrs.site/oauth/google/callback`
2. **HTTPS обязателен** для продакшена
3. **Переменные окружения** должны быть установлены на сервере
4. **Перезапуск сервера** после установки переменных окружения
### 📊 Ожидаемый результат:
После настройки OAuth:
- ✅ Google callback обрабатывается успешно
- ✅ Пользователь создается/обновляется
- ✅ Session token устанавливается в httpOnly cookie
- ✅ Редирект на `https://testing.discours.io/oauth?redirect_url=...`
- ✅ Фронтенд получает успешную аутентификацию

View File

@@ -1,325 +0,0 @@
# OAuth Frontend Integration для testing.discours.io
## 🎯 Схема: JWT в URL + httpOnly Cookie
### 📋 Полный flow:
1. **OAuth success** → бэкенд генерирует JWT
2. **Редирект**: `/oauth?access_token=JWT&redirect_url=...` + httpOnly cookie
3. **Фронт роут**: `localStorage.setItem('auth_token', token)`
4. **SessionProvider**: `loadSession()` → использует localStorage токен
5. **GraphQL клиент**: `credentials: 'include'` → использует httpOnly cookie
## 🔧 Frontend Implementation
### 1. OAuth Route Handler (`/oauth`)
```typescript
// routes/oauth.tsx
import { useEffect } from 'solid-js'
import { useNavigate, useSearchParams } from '@solidjs/router'
import { useSession } from '../context/SessionProvider'
export default function OAuthCallback() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const { loadSession } = useSession()
useEffect(async () => {
const error = searchParams.error
const accessToken = searchParams.access_token
const redirectUrl = searchParams.redirect_url || '/'
if (error) {
// Обработка ошибок OAuth
console.error('OAuth error:', error)
// Показываем пользователю ошибку
if (error === 'oauth_state_expired') {
alert('OAuth session expired. Please try logging in again.')
} else if (error === 'access_denied') {
alert('Access denied by provider.')
} else {
alert('Authentication failed. Please try again.')
}
navigate('/')
return
}
if (accessToken) {
try {
// 1. Сохраняем JWT в localStorage для быстрого доступа
localStorage.setItem('auth_token', accessToken)
// 2. SessionProvider загружает сессию (использует localStorage токен)
await loadSession()
// 3. Очищаем URL от токена (безопасность)
window.history.replaceState({}, document.title, '/oauth-success')
// 4. Редиректим на исходную страницу через 1 секунду
setTimeout(() => {
navigate(decodeURIComponent(redirectUrl))
}, 1000)
} catch (error) {
console.error('Failed to load session:', error)
localStorage.removeItem('auth_token')
navigate('/')
}
} else {
// Неожиданный случай
navigate('/')
}
})
return (
<div class="oauth-callback">
<div class="loading">
<h2>Completing authentication...</h2>
<div class="spinner"></div>
</div>
</div>
)
}
```
### 2. OAuth Initiation
```typescript
// utils/auth.ts
export const oauth = (provider: string) => {
// Простой редирект - backend получит redirect_uri из Referer header
window.location.href = `https://v3.dscrs.site/oauth/${provider}`
}
// Использование в компонентах
import { oauth } from '../utils/auth'
const LoginButton = () => (
<button onClick={() => oauth('github')}>
Login with GitHub
</button>
)
```
### 3. Session Provider
```typescript
// context/SessionProvider.tsx
import { createContext, useContext, createSignal, onMount } from 'solid-js'
interface SessionContextType {
user: () => User | null
isAuthenticated: () => boolean
loadSession: () => Promise<void>
logout: () => void
}
const SessionContext = createContext<SessionContextType>()
export function SessionProvider(props: { children: any }) {
const [user, setUser] = createSignal<User | null>(null)
const isAuthenticated = () => !!user()
const loadSession = async () => {
try {
// Проверяем localStorage токен
const token = localStorage.getItem('auth_token')
if (!token) {
setUser(null)
return
}
// Загружаем профиль пользователя через GraphQL (использует httpOnly cookie)
const response = await client.query({
query: GET_CURRENT_USER,
fetchPolicy: 'network-only' // Всегда свежие данные
})
if (response.data?.currentUser) {
setUser(response.data.currentUser)
} else {
// Токен невалидный, очищаем
localStorage.removeItem('auth_token')
setUser(null)
}
} catch (error) {
console.error('Failed to load session:', error)
localStorage.removeItem('auth_token')
setUser(null)
}
}
const logout = () => {
localStorage.removeItem('auth_token')
setUser(null)
// Опционально: вызов logout endpoint для очистки httpOnly cookie
fetch('https://v3.dscrs.site/auth/logout', {
method: 'POST',
credentials: 'include'
})
}
// Загружаем сессию при инициализации
onMount(() => {
loadSession()
})
return (
<SessionContext.Provider value={{
user,
isAuthenticated,
loadSession,
logout
}}>
{props.children}
</SessionContext.Provider>
)
}
export const useSession = () => {
const context = useContext(SessionContext)
if (!context) {
throw new Error('useSession must be used within SessionProvider')
}
return context
}
```
### 4. GraphQL Client Setup
```typescript
// graphql/client.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'
const httpLink = createHttpLink({
uri: 'https://v3.dscrs.site/graphql',
credentials: 'include', // ✅ КРИТИЧНО: отправляет httpOnly cookie
})
export const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
errorPolicy: 'all'
},
query: {
errorPolicy: 'all'
}
}
})
```
### 5. API Client для прямых вызовов
```typescript
// utils/api.ts
class ApiClient {
private baseUrl = 'https://v3.dscrs.site'
private getAuthHeaders() {
const token = localStorage.getItem('auth_token')
return token ? { 'Authorization': `Bearer ${token}` } : {}
}
async request(endpoint: string, options: RequestInit = {}) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...this.getAuthHeaders(),
...options.headers
},
credentials: 'include' // Для httpOnly cookie
})
if (!response.ok) {
if (response.status === 401) {
// Токен истек, очищаем localStorage
localStorage.removeItem('auth_token')
window.location.href = '/login'
}
throw new Error(`API Error: ${response.status}`)
}
return response.json()
}
// Методы для различных API calls
async uploadFile(file: File) {
const formData = new FormData()
formData.append('file', file)
return this.request('/upload', {
method: 'POST',
body: formData,
headers: this.getAuthHeaders() // Только Authorization header, без Content-Type
})
}
}
export const apiClient = new ApiClient()
```
## 🔒 Безопасность
### Преимущества двойной схемы:
1. **httpOnly Cookie** - защита от XSS для GraphQL
2. **localStorage JWT** - быстрый доступ для API calls
3. **Automatic cleanup** - токен удаляется при ошибках 401
4. **URL cleanup** - токен не остается в истории браузера
### Обработка ошибок:
```typescript
// utils/errorHandler.ts
export const handleAuthError = (error: any) => {
if (error.networkError?.statusCode === 401) {
// Токен истек
localStorage.removeItem('auth_token')
window.location.href = '/login'
}
}
// В Apollo Client
import { onError } from '@apollo/client/link/error'
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (networkError?.statusCode === 401) {
handleAuthError(networkError)
}
})
```
## 🧪 Testing
### E2E Test
```typescript
// tests/oauth.spec.ts
import { test, expect } from '@playwright/test'
test('OAuth flow works correctly', async ({ page }) => {
// 1. Инициация OAuth
await page.goto('https://testing.discours.io')
await page.click('[data-testid="github-login"]')
// 2. Проверяем редирект на GitHub
await expect(page).toHaveURL(/github\.com\/login\/oauth\/authorize/)
// 3. Симулируем успешный callback
await page.goto('https://testing.discours.io/oauth?access_token=test_jwt&redirect_url=%2Fdashboard')
// 4. Проверяем что токен сохранился
const token = await page.evaluate(() => localStorage.getItem('auth_token'))
expect(token).toBe('test_jwt')
// 5. Проверяем редирект на dashboard
await expect(page).toHaveURL('https://testing.discours.io/dashboard')
})
```
## 📊 Monitoring
### Метрики для отслеживания:
- OAuth success rate
- Token validation errors
- Session load time
- Cookie/localStorage sync issues

View File

@@ -1,172 +0,0 @@
# GlitchTip Security Alerts Integration
## 🚨 Автоматические алерты безопасности OAuth
Система OAuth теперь автоматически отправляет алерты в GlitchTip при обнаружении подозрительной активности.
## 🎯 Типы алертов
### 🔴 Критические события (ERROR level)
- **`open_redirect_attempt`** - Попытка open redirect атаки
- **`rate_limit_exceeded`** - Превышение лимита запросов (брутфорс)
- **`invalid_provider`** - Попытка использования несуществующего провайдера
- **`suspicious_redirect_uri`** - Подозрительный redirect URI
- **`brute_force_detected`** - Обнаружена брутфорс атака
### 🟡 Обычные события (WARNING level)
- **`oauth_login_attempt`** - Обычная попытка входа
- **`provider_validation`** - Валидация провайдера
- **`redirect_uri_validation`** - Валидация redirect URI
## 🏷️ Теги для фильтрации в GlitchTip
Каждый алерт содержит теги для удобной фильтрации:
```python
{
"security_event": "rate_limit_exceeded",
"component": "oauth",
"client_ip": "192.168.1.100",
"oauth_provider": "github",
"has_redirect_uri": "true"
}
```
## 📊 Контекст события
Детальная информация в контексте `security_details`:
```python
{
"ip": "192.168.1.100",
"provider": "github",
"attempts": 15,
"limit": 10,
"window_seconds": 300,
"severity": "high",
"malicious_uri": "https://evil.com/steal",
"attack_type": "open_redirect"
}
```
## 🔧 Интеграция в коде
### Автоматические алерты
```python
# При превышении rate limit
if len(requests) >= OAUTH_RATE_LIMIT:
send_rate_limit_alert(client_ip, len(requests))
return False
# При попытке open redirect
if not is_allowed:
send_open_redirect_alert(redirect_uri)
return False
```
### Ручные алерты
```python
from auth.oauth_security import log_oauth_security_event
# Отправка кастомного алерта
log_oauth_security_event("suspicious_activity", {
"ip": client_ip,
"details": "Custom security event",
"severity": "medium"
})
```
## 🛡️ Обработка ошибок
Система устойчива к сбоям GlitchTip:
```python
try:
# Отправка алерта в GlitchTip
sentry_sdk.capture_message(message, level=level)
except Exception as e:
# Не ломаем основную логику
logger.error(f"Failed to send alert to GlitchTip: {e}")
```
## 📈 Мониторинг в GlitchTip
### Фильтры для критических событий:
```
tag:security_event AND level:error
```
### Фильтры по компонентам:
```
tag:component:oauth
```
### Фильтры по IP адресам:
```
tag:client_ip:192.168.1.100
```
## 🚨 Алерты по типам атак
### Open Redirect атаки:
```
tag:security_event:open_redirect_attempt
```
### Брутфорс атаки:
```
tag:security_event:rate_limit_exceeded
```
### Невалидные провайдеры:
```
tag:security_event:invalid_provider
```
## 📊 Статистика безопасности
GlitchTip позволяет отслеживать:
- Количество атак по времени
- Топ атакующих IP адресов
- Самые частые типы атак
- Географическое распределение атак
## 🔄 Настройка алертов
В GlitchTip можно настроить:
- Email уведомления при критических событиях
- Slack/Discord интеграции
- Webhook для автоматической блокировки IP
- Дашборды для мониторинга безопасности
## ✅ Тестирование
Система покрыта тестами:
```bash
# Запуск тестов GlitchTip интеграции
uv run python -m pytest tests/test_oauth_glitchtip_alerts.py -v
# Результат: 8/8 тестов прошли
✅ Critical events sent as ERROR
✅ Normal events sent as WARNING
✅ Open redirect alert integration
✅ Rate limit alert integration
✅ Failure handling (graceful degradation)
✅ Security context tags
✅ Event logging integration
✅ Critical events list validation
```
## 🎯 Преимущества
1. **Реальное время** - мгновенные алерты при атаках
2. **Контекст** - полная информация о событии
3. **Фильтрация** - удобные теги для поиска
4. **Устойчивость** - не ломает основную логику при сбоях
5. **Тестируемость** - полное покрытие тестами
6. **Масштабируемость** - готово для высоких нагрузок
**Система безопасности OAuth теперь имеет полноценный мониторинг!** 🔒✨

View File

@@ -1,287 +0,0 @@
# Минимальный OAuth Flow для testing.discours.io
## 🎯 Философия: Максимальная простота
### ✨ **Принцип: "Нет ошибки = успех"**
Никаких лишних параметров, флагов или токенов в URL. Только самое необходимое.
## 🔧 Backend Implementation
### OAuth Callback Handler
```python
@app.route('/oauth/<provider>/callback')
def oauth_callback(provider):
try:
# 1. Валидация state (CSRF защита)
state = request.args.get('state')
oauth_data = get_oauth_state(state)
if not oauth_data:
raise ValueError('Invalid or expired state')
# 2. Обмен code на access_token
code = request.args.get('code')
access_token = exchange_code_for_token(provider, code)
# 3. Получение профиля пользователя
user_data = get_user_profile(provider, access_token)
# 4. Создание/обновление пользователя
user = create_or_update_user(user_data, provider)
# 5. Генерация JWT
jwt_token = create_jwt_token(user.id)
# 6. Простой редирект без лишних параметров
redirect_url = oauth_data.get('redirect_uri', '/')
response = make_response(redirect(
f'https://testing.discours.io/oauth?redirect_url={quote(redirect_url)}'
))
# 7. JWT только в httpOnly cookie
response.set_cookie(
'auth_token',
jwt_token,
httponly=True, # ✅ Защита от XSS
secure=True, # ✅ Только HTTPS
samesite='Lax', # ✅ CSRF защита
max_age=7*24*60*60, # 7 дней
domain='.discours.io' # ✅ Поддомены
)
return response
except Exception as e:
# При ошибке - добавляем error параметр
logger.error(f'OAuth error: {e}')
redirect_url = oauth_data.get('redirect_uri', '/') if 'oauth_data' in locals() else '/'
return redirect(
f'https://testing.discours.io/oauth?error=auth_failed&redirect_url={quote(redirect_url)}'
)
```
## 🌐 Frontend Implementation
### OAuth Route Handler
```typescript
// routes/oauth.tsx
import { useEffect } from 'solid-js'
import { useNavigate, useSearchParams } from '@solidjs/router'
import { useSession } from '../context/SessionProvider'
export default function OAuthCallback() {
const navigate = useNavigate()
const [searchParams] = useSearchParams()
const { loadSession } = useSession()
useEffect(async () => {
const error = searchParams.error
const redirectUrl = searchParams.redirect_url || '/'
if (error) {
// Есть ошибка = неудача
console.error('OAuth error:', error)
if (error === 'oauth_state_expired') {
alert('OAuth session expired. Please try logging in again.')
} else if (error === 'access_denied') {
alert('Access denied by provider.')
} else {
alert('Authentication failed. Please try again.')
}
navigate('/')
} else {
// Нет ошибки = успех! JWT уже в httpOnly cookie
try {
await loadSession() // Загружает из httpOnly cookie
navigate(decodeURIComponent(redirectUrl))
} catch (error) {
console.error('Failed to load session:', error)
navigate('/')
}
}
})
return (
<div class="oauth-callback">
<div class="loading">
<h2>Completing authentication...</h2>
<div class="spinner"></div>
</div>
</div>
)
}
```
### Session Provider (httpOnly only)
```typescript
// context/SessionProvider.tsx
export function SessionProvider(props: { children: any }) {
const [user, setUser] = createSignal<User | null>(null)
const loadSession = async () => {
try {
// Загружаем профиль через GraphQL (httpOnly cookie автоматически)
const response = await client.query({
query: GET_CURRENT_USER,
fetchPolicy: 'network-only'
})
if (response.data?.currentUser) {
setUser(response.data.currentUser)
} else {
setUser(null)
}
} catch (error) {
console.error('Failed to load session:', error)
setUser(null)
}
}
const logout = async () => {
setUser(null)
// Очистка httpOnly cookie через logout endpoint
await fetch('https://v3.dscrs.site/auth/logout', {
method: 'POST',
credentials: 'include'
})
}
// ... остальная логика
}
```
## 🔒 Unified Authentication
### Все запросы используют httpOnly cookie
```typescript
// GraphQL Client
const client = new ApolloClient({
uri: 'https://v3.dscrs.site/graphql',
credentials: 'include', // ✅ httpOnly cookie
})
// REST API calls
const apiCall = async (endpoint: string, options: RequestInit = {}) => {
return fetch(`https://v3.dscrs.site${endpoint}`, {
...options,
credentials: 'include', // ✅ httpOnly cookie
headers: {
'Content-Type': 'application/json',
...options.headers
}
})
}
// File uploads
const uploadFile = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
return fetch('https://v3.dscrs.site/upload', {
method: 'POST',
body: formData,
credentials: 'include' // ✅ httpOnly cookie
})
}
```
## 🎯 URL Examples
### ✅ Успешная авторизация
```
https://testing.discours.io/oauth?redirect_url=%2Fdashboard
```
- Нет `error` параметра = успех
- JWT в httpOnly cookie
- Редирект на `/dashboard`
### ❌ Ошибка авторизации
```
https://testing.discours.io/oauth?error=auth_failed&redirect_url=%2F
```
- Есть `error` параметр = неудача
- Показать ошибку пользователю
- Редирект на главную
### 🔒 Истекший state
```
https://testing.discours.io/oauth?error=oauth_state_expired&redirect_url=%2F
```
- CSRF защита сработала
- Предложить повторить авторизацию
## 🚀 Преимущества минимального подхода
### 🔒 Максимальная безопасность
- **Никаких JWT в URL** - нет токенов в истории браузера
- **httpOnly cookie** - защита от XSS атак
- **SameSite=Lax** - защита от CSRF
- **Secure flag** - только HTTPS
### 🧹 Чистота и простота
- **Минимум параметров** - только необходимые
- **Логичная схема** - отсутствие ошибки = успех
- **Единый источник истины** - httpOnly cookie для всего
- **Простой код** - меньше условий и проверок
### ⚡ Производительность
- **Меньше парсинга** - меньше URL параметров
- **Автоматические cookie** - браузер сам отправляет
- **Меньше localStorage операций** - нет дублирования
- **Простая логика** - быстрее выполнение
## 🧪 Testing
### E2E Test
```typescript
test('Minimal OAuth flow', async ({ page }) => {
// 1. Инициация
await page.goto('https://testing.discours.io')
await page.click('[data-testid="github-login"]')
// 2. Симуляция успешного callback
await page.goto('https://testing.discours.io/oauth?redirect_url=%2Fdashboard')
// 3. Проверяем что попали на dashboard (успех)
await expect(page).toHaveURL('https://testing.discours.io/dashboard')
// 4. Проверяем что cookie установлен
const cookies = await page.context().cookies()
const authCookie = cookies.find(c => c.name === 'auth_token')
expect(authCookie).toBeTruthy()
expect(authCookie?.httpOnly).toBe(true)
})
test('OAuth error handling', async ({ page }) => {
// Симуляция ошибки
await page.goto('https://testing.discours.io/oauth?error=auth_failed&redirect_url=%2F')
// Проверяем что показалась ошибка и редирект на главную
await expect(page).toHaveURL('https://testing.discours.io/')
})
```
## 📊 Comparison
| Параметр | Старый подход | Новый подход |
|----------|---------------|--------------|
| URL параметры | `success=true&access_token=JWT&redirect_url=...` | `redirect_url=...` |
| Токен в URL | ✅ Да | ❌ Нет |
| localStorage | ✅ Используется | ❌ Не нужен |
| httpOnly cookie | ✅ Да | ✅ Да |
| Логика успеха | Проверка `success=true` | Отсутствие `error` |
| Безопасность | Средняя | Максимальная |
| Простота | Средняя | Максимальная |
## 🎉 Результат
**Самый простой и безопасный OAuth flow:**
1. Нет ошибки = успех
2. Один источник аутентификации = httpOnly cookie
3. Минимум параметров = максимум простоты
4. Максимальная безопасность = никаких токенов в URL
**Элегантно. Просто. Безопасно.**

View File

@@ -1,144 +0,0 @@
# 🔐 Настройка OAuth Провайдеров
## 🎯 Архитектура OAuth
**Важно понимать разделение:**
- **Frontend**: `testing.discours.io` - где пользователь нажимает кнопку входа
- **Backend**: `v3.dscrs.site` - где обрабатывается OAuth логика
- **Callback URL**: Всегда должен указывать на **backend** (`v3.dscrs.site`)
## 🔄 OAuth Flow (пошагово):
1. Пользователь на `testing.discours.io` нажимает "Войти через GitHub"
2. Фронтенд редиректит на `v3.dscrs.site/oauth/github`
3. Backend редиректит на `github.com` с callback_uri=`v3.dscrs.site/oauth/github/callback`
4. GitHub после авторизации редиректит на `v3.dscrs.site/oauth/github/callback`
5. Backend обрабатывает callback и редиректит обратно на `testing.discours.io`
## 🎯 Быстрая настройка
### 1. Google OAuth
1. Перейти в [Google Cloud Console](https://console.cloud.google.com/)
2. Создать проект или выбрать существующий
3. **APIs & Services****Credentials****Create Credentials****OAuth 2.0 Client ID**
4. **Application type**: Web application
5. **Authorized redirect URIs**:
- `https://v3.dscrs.site/oauth/google/callback` ⚠️ **ОБЯЗАТЕЛЬНО HTTPS**
- `http://localhost:8000/oauth/google/callback` (для разработки)
6. Скопировать **Client ID** и **Client Secret**
**Переменные окружения:**
```bash
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
```
### 2. GitHub OAuth
1. Перейти в [GitHub Developer Settings](https://github.com/settings/developers)
2. **New OAuth App**
3. **Authorization callback URL**: `https://v3.dscrs.site/oauth/github/callback` ⚠️ **ОБЯЗАТЕЛЬНО HTTPS**
4. Скопировать **Client ID** и **Client Secret**
**Переменные окружения:**
```bash
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
```
### 3. VK OAuth
1. Перейти в [VK Developers](https://dev.vk.com/apps)
2. **Создать приложение****Веб-сайт**
3. **Настройки****Redirect URI**: `https://v3.dscrs.site/oauth/vk/callback` ⚠️ **ОБЯЗАТЕЛЬНО HTTPS**
4. Скопировать **ID приложения** и **Защищённый ключ**
**Переменные окружения:**
```bash
VK_CLIENT_ID=your_vk_app_id
VK_CLIENT_SECRET=your_vk_secure_key
```
### 4. Facebook OAuth
1. Перейти в [Facebook Developers](https://developers.facebook.com/)
2. **My Apps****Create App****Consumer**
3. **Facebook Login****Settings**
4. **Valid OAuth Redirect URIs**: `https://v3.dscrs.site/oauth/facebook/callback` ⚠️ **ОБЯЗАТЕЛЬНО HTTPS**
5. Скопировать **App ID** и **App Secret**
**Переменные окружения:**
```bash
FACEBOOK_CLIENT_ID=your_facebook_app_id
FACEBOOK_CLIENT_SECRET=your_facebook_app_secret
```
### 5. Yandex OAuth
1. Перейти в [Yandex OAuth](https://oauth.yandex.ru/)
2. **Создать новое приложение**
3. **Callback URI**: `https://v3.dscrs.site/oauth/yandex/callback` ⚠️ **ОБЯЗАТЕЛЬНО HTTPS**
4. **Права**: `login:info`, `login:email`, `login:avatar`
5. Скопировать **ID** и **Пароль**
**Переменные окружения:**
```bash
YANDEX_CLIENT_ID=your_yandex_client_id
YANDEX_CLIENT_SECRET=your_yandex_client_secret
```
## 🚀 Развертывание
### Локальная разработка
```bash
# .env файл
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
# и т.д.
```
### Продакшн (Dokku/Heroku)
```bash
# Установка переменных окружения
dokku config:set myapp GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy
# или
heroku config:set GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy
```
## ✅ Проверка настройки
1. Перезапустить приложение
2. Проверить логи: `OAuth provider google: id=SET, key=SET`
3. Тестовый вход: `https://v3.dscrs.site/oauth/google`
## 🔍 Диагностика проблем
**Ошибка "Provider not configured":**
- Проверить переменные окружения
- Убедиться что значения не пустые
- Перезапустить приложение
**Ошибка redirect_uri_mismatch:**
- Проверить точное соответствие URL в настройках провайдера
- Убедиться что протокол (http/https) совпадает
- **ВАЖНО**: Callback URL должен указывать на backend (`v3.dscrs.site`), НЕ на frontend (`testing.discours.io`)
**Ошибка "redirect_uri is not associated with this application":**
- Callback URL в настройках провайдера должен быть `https://v3.dscrs.site/oauth/{provider}/callback`
- **ОБЯЗАТЕЛЬНО HTTPS** (не HTTP) для продакшна
- НЕ указывать frontend URL в настройках провайдера
- Проверить что URL в настройках GitHub **точно совпадает** с тем что отправляет код
**VK ошибка "Code challenge method is unsupported":**
- Это нормально, VK не поддерживает PKCE
- Система автоматически обрабатывает это
## 📞 Поддержка
При проблемах проверить:
1. Логи приложения при запуске
2. Настройки redirect URI у провайдера
3. Корректность переменных окружения

View File

@@ -1,174 +0,0 @@
# OAuth Test Scenarios для testing.discours.io
## 🧪 Тестовые сценарии для проверки OAuth flow
### 1. ✅ Успешная авторизация GitHub
```bash
# Шаг 1: Инициация OAuth
curl -v "https://v3.dscrs.site/oauth/github" \
-H "Referer: https://testing.discours.io/some-page" \
-H "User-Agent: Mozilla/5.0"
# Ожидаемый результат:
# - Редирект 302 на GitHub с правильными параметрами
# - state сохранен в Redis с TTL 10 минут
# - redirect_uri взят из Referer header
# Шаг 2: Callback от GitHub (симуляция)
curl -v "https://v3.dscrs.site/oauth/github/callback?code=test_code&state=valid_state" \
-H "User-Agent: Mozilla/5.0"
# Ожидаемый результат:
# - Обмен code на access_token
# - Получение профиля пользователя
# - Создание JWT токена
# - Установка httpOnly cookie с domain=".discours.io"
# - Редирект на https://testing.discours.io/oauth?success=true
```
### 2. 🚨 Обработка ошибок провайдера
```bash
# GitHub отклонил доступ
curl -v "https://v3.dscrs.site/oauth/github/callback?error=access_denied&state=valid_state"
# Ожидаемый результат:
# - Редирект на https://testing.discours.io/oauth?error=access_denied
```
### 3. 🛡️ CSRF защита (state validation)
```bash
# Неправильный state
curl -v "https://v3.dscrs.site/oauth/github/callback?code=test_code&state=invalid_state"
# Ожидаемый результат:
# - Редирект на https://testing.discours.io/oauth?error=oauth_state_expired
```
### 4. 🔍 Валидация провайдера
```bash
# Несуществующий провайдер
curl -v "https://v3.dscrs.site/oauth/invalid_provider"
# Ожидаемый результат:
# - JSON ответ с ошибкой {"error": "Invalid provider"}
```
### 5. 🍪 Проверка cookie установки
```bash
# Проверка что cookie устанавливается правильно
curl -v "https://v3.dscrs.site/oauth/github/callback?code=valid_code&state=valid_state" \
-c cookies.txt
# Проверить в cookies.txt:
# - session_token cookie
# - HttpOnly=true
# - Secure=true
# - SameSite=Lax
# - Domain=.discours.io
```
### 6. 🌐 CORS проверка
```bash
# Preflight запрос
curl -v "https://v3.dscrs.site/oauth/github" \
-X OPTIONS \
-H "Origin: https://testing.discours.io" \
-H "Access-Control-Request-Method: GET"
# Ожидаемый результат:
# - Access-Control-Allow-Origin: https://testing.discours.io
# - Access-Control-Allow-Credentials: true
```
### 7. 🔄 Полный E2E тест
```bash
#!/bin/bash
# Полный тест OAuth flow
echo "🔄 Тестируем полный OAuth flow..."
# 1. Инициация
INIT_RESPONSE=$(curl -s -D headers1.txt "https://v3.dscrs.site/oauth/github" \
-H "Referer: https://testing.discours.io/test-page")
# Извлекаем Location header для получения state
GITHUB_URL=$(grep -i "location:" headers1.txt | cut -d' ' -f2 | tr -d '\r')
STATE=$(echo "$GITHUB_URL" | grep -o 'state=[^&]*' | cut -d'=' -f2)
echo "✅ State получен: $STATE"
# 2. Симуляция callback
CALLBACK_RESPONSE=$(curl -s -D headers2.txt \
"https://v3.dscrs.site/oauth/github/callback?code=test_code&state=$STATE")
# Проверяем редирект
REDIRECT_URL=$(grep -i "location:" headers2.txt | cut -d' ' -f2 | tr -d '\r')
echo "✅ Redirect URL: $REDIRECT_URL"
# Проверяем cookie
COOKIE=$(grep -i "set-cookie:" headers2.txt | grep "session_token")
echo "✅ Cookie установлен: $COOKIE"
if [[ "$REDIRECT_URL" == *"testing.discours.io/oauth?success=true"* ]]; then
echo "🎉 OAuth flow работает корректно!"
else
echo "❌ OAuth flow не работает"
exit 1
fi
```
## 🔧 Настройки провайдеров для тестирования
### GitHub OAuth App
```
Application name: Discours Testing
Homepage URL: https://testing.discours.io
Authorization callback URL: https://v3.dscrs.site/oauth/github/callback
```
### Google OAuth Client
```
Authorized JavaScript origins: https://testing.discours.io
Authorized redirect URIs: https://v3.dscrs.site/oauth/google/callback
```
### Environment Variables
```bash
# Для тестирования нужны эти переменные:
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
# Redis для state storage
REDIS_URL=redis://localhost:6379
# Frontend URL
FRONTEND_URL=https://testing.discours.io
```
## 🐛 Возможные проблемы и решения
### 1. Cookie не устанавливается
**Проблема**: Domain mismatch между v3.dscrs.site и testing.discours.io
**Решение**: Используется domain=".discours.io" для поддержки поддоменов
### 2. CORS ошибки
**Проблема**: Браузер блокирует запросы между доменами
**Решение**: allow_credentials=True в CORS настройках
### 3. State expired
**Проблема**: Redis state истекает через 10 минут
**Решение**: Увеличить TTL или оптимизировать flow
### 4. Provider not configured
**Проблема**: Отсутствуют CLIENT_ID/CLIENT_SECRET
**Решение**: Проверить environment variables
## 📊 Метрики успешности
- ✅ Успешная авторизация: > 95%
- ✅ CSRF защита: 100% блокировка invalid state
- ✅ Cookie безопасность: HttpOnly + Secure + SameSite
- ✅ Error handling: Все ошибки редиректят на фронт
- ✅ Performance: < 2 секунд на полный flow