All checks were successful
Deploy on push / deploy (push) Successful in 2m46s
### 🍪 CRITICAL Cross-Origin Auth - **🔧 SESSION_COOKIE_DOMAIN**: Добавлена поддержка поддоменов `.discours.io` для cross-origin cookies - **🌐 Cross-Origin SSE**: Исправлена работа Server-Sent Events с httpOnly cookies между поддоменами - **🔐 Unified Auth**: Унифицированы настройки cookies для OAuth, login, refresh, logout операций - **📝 MyPy Compliance**: Исправлена типизация `SESSION_COOKIE_SAMESITE` с использованием `cast()` ### 🛠️ Technical Changes - **settings.py**: Добавлен `SESSION_COOKIE_DOMAIN` с типобезопасной настройкой SameSite - **auth/oauth.py**: Обновлены все `set_cookie` вызовы с `domain` параметром - **auth/middleware.py**: Добавлена поддержка `SESSION_COOKIE_DOMAIN` в logout операциях - **resolvers/auth.py**: Унифицированы cookie настройки в login/refresh/logout resolvers - **auth/__init__.py**: Обновлены cookie операции с domain поддержкой ### 📚 Documentation - **docs/auth/sse-httponly-integration.md**: Новая документация по SSE + httpOnly cookies интеграции - **docs/auth/architecture.md**: Обновлены диаграммы для unified httpOnly cookie архитектуры ### 🎯 Impact - ✅ **GraphQL API** (`v3.discours.io`) теперь работает с httpOnly cookies cross-origin - ✅ **SSE сервер** (`connect.discours.io`) работает с теми же cookies - ✅ **Безопасность**: httpOnly cookies защищают от XSS атак - ✅ **UX**: Автоматическая аутентификация без управления токенами в JavaScript
12 KiB
12 KiB
🔐 OAuth Integration Guide
🎯 Обзор
Система OAuth интеграции с Bearer токенами для основного сайта. Поддержка популярных провайдеров с cross-origin совместимостью.
Важно: OAuth доступен только для основного сайта. Админка использует только email/password аутентификацию.
🔄 Архитектура: стандартный подход
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant P as OAuth Provider
U->>F: Click "Login with Provider"
F->>B: GET /oauth/{provider}/login
B->>P: Redirect to Provider
P->>U: Show authorization page
U->>P: Grant permission
P->>B: GET /oauth/{provider}/callback?code={code}
B->>P: Exchange code for token
P->>B: Return access token + user data
B->>B: Create/update user + JWT session
B->>F: Redirect with token in URL
Note over B,F: URL: /?access_token=JWT_TOKEN
F->>F: Save token to localStorage
F->>F: Clear token from URL
F->>U: User logged in
Note over F,B: All subsequent requests
F->>B: GraphQL with Authorization: Bearer
🚀 Поддерживаемые провайдеры
| Провайдер | Статус | Особенности |
|---|---|---|
| ✅ | 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 Login API v18.0+ | |
| X (Twitter) | ✅ | OAuth 2.0 API v2 |
🔧 OAuth Flow
1. 🚀 Инициация OAuth (Фронтенд)
// Простой редирект - 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
# /oauth/github/login
# 1. Сохраняет redirect_uri из Referer header в Redis state
# 2. Генерирует PKCE challenge для безопасности
# 3. Редиректит на провайдера с параметрами авторизации
GET /oauth/{provider}/callback - Callback
# 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. 🌐 Фронтенд финализация
// OAuth callback route
export default function OAuthCallback() {
const navigate = useNavigate();
const auth = useAuth();
onMount(async () => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('access_token');
const error = urlParams.get('error');
if (error) {
// ❌ Ошибка OAuth
console.error('OAuth error:', error);
navigate('/login?error=' + error);
} else if (token) {
// ✅ Успех! Сохраняем токен в localStorage
localStorage.setItem('access_token', token);
// Очищаем URL от токена
window.history.replaceState({}, '', window.location.pathname);
// Возвращаемся на сохраненную страницу
const returnUrl = localStorage.getItem('oauth_return_url') || '/';
localStorage.removeItem('oauth_return_url');
navigate(returnUrl);
} else {
navigate('/login?error=no_token');
}
});
return (
<div class="oauth-callback">
<h2>Завершение авторизации...</h2>
<p>Пожалуйста, подождите...</p>
</div>
);
}
4. 🔑 Использование Bearer токенов
// GraphQL клиент использует Bearer токены из localStorage
const graphqlRequest = async (query: string, variables?: any) => {
const token = localStorage.getItem('access_token');
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}` // ✅ Bearer токен из localStorage
},
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
- Google Cloud Console
- APIs & Services → Credentials → OAuth 2.0 Client ID
- Authorized redirect URIs:
https://your-domain.com/oauth/google/callback
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GitHub OAuth
- GitHub Developer Settings
- New OAuth App
- Authorization callback URL:
https://your-domain.com/oauth/github/callback
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
Yandex OAuth
- Yandex OAuth
- Создать новое приложение
- Callback URI:
https://your-domain.com/oauth/yandex/callback - Права:
login:info,login:email,login:avatar
YANDEX_CLIENT_ID=your_yandex_client_id
YANDEX_CLIENT_SECRET=your_yandex_client_secret
VK OAuth
- VK Developers
- Создать приложение → Веб-сайт
- Redirect URI:
https://your-domain.com/oauth/vk/callback
VK_CLIENT_ID=your_vk_app_id
VK_CLIENT_SECRET=your_vk_secure_key
🛡️ Безопасность
httpOnly Cookie настройки
# 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 токенов
from auth.tokens.oauth import OAuthTokenManager
oauth = OAuthTokenManager()
# Сохранение OAuth токенов (для API интеграций)
await oauth.store_oauth_tokens(
user_id="123",
provider="google",
access_token="ya29.a0AfH6SM...",
refresh_token="1//04...",
expires_in=3600
)
# Получение токена для API вызовов
token_data = await oauth.get_token("123", "google", "oauth_access")
if token_data:
# Используем токен для вызовов Google API
headers = {"Authorization": f"Bearer {token_data['token']}"}
Redis структура
# OAuth токены для API интеграций
oauth_access:{user_id}:{provider} # Access токен
oauth_refresh:{user_id}:{provider} # Refresh токен
# OAuth state (временный)
oauth_state:{state} # Данные авторизации (TTL: 10 мин)
# Сессии пользователей (основные)
session:{user_id}:{token} # JWT сессия (TTL: 30 дней)
🧪 Тестирование
E2E Test
test('OAuth flow with httpOnly cookies', async ({ page }) => {
// 1. Инициация OAuth
await page.goto('/login');
await page.click('[data-testid="google-login"]');
// 2. Проверяем редирект на Google
await expect(page).toHaveURL(/accounts\.google\.com/);
// 3. Симулируем успешный callback (в тестовой среде)
await page.goto('/oauth/callback');
// 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);
// 5. Проверяем что пользователь авторизован
await expect(page.locator('[data-testid="user-menu"]')).toBeVisible();
});
Отладка
# Проверка OAuth провайдеров
curl -v "https://your-domain.com/oauth/google/login"
# Проверка 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 } } }"}'
📊 Мониторинг
from auth.tokens.monitoring import TokenMonitoring
monitoring = TokenMonitoring()
# Статистика 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 интеграция! 🔐✨