# Минимальный OAuth Flow для testing.discours.io ## 🎯 Философия: Максимальная простота ### ✨ **Принцип: "Нет ошибки = успех"** Никаких лишних параметров, флагов или токенов в URL. Только самое необходимое. ## 🔧 Backend Implementation ### OAuth Callback Handler ```python @app.route('/oauth//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 (

Completing authentication...

) } ``` ### Session Provider (httpOnly only) ```typescript // context/SessionProvider.tsx export function SessionProvider(props: { children: any }) { const [user, setUser] = createSignal(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 **Элегантно. Просто. Безопасно.** ✨