Files
core/docs/auth/oauth.md
2025-09-29 17:57:45 +03:00

381 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔐 OAuth Integration Guide
## 🎯 Обзор
Система OAuth интеграции с **Bearer токенами** для основного сайта. Поддержка популярных провайдеров с cross-origin совместимостью.
**Важно:** OAuth доступен только для основного сайта. Админка использует только email/password аутентификацию.
### 🔄 **Архитектура: стандартный подход**
```mermaid
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
```
## 🚀 Поддерживаемые провайдеры
| Провайдер | Статус | Особенности |
|-----------|--------|-------------|
| **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
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 токенов
```typescript
// 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
1. [Google Cloud Console](https://console.cloud.google.com/)
2. **APIs & Services****Credentials****OAuth 2.0 Client ID**
3. **Authorized redirect URIs**: `https://v3.discours.io/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://v3.discours.io/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://v3.discours.io/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://v3.discours.io/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",
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 структура
```bash
# 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
```typescript
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();
});
```
### Отладка
```bash
# Проверка OAuth провайдеров
curl -v "https://v3.discours.io/oauth/google/login"
# Проверка callback
curl -v "https://v3.discours.io/oauth/google/callback?code=test&state=test"
# Проверка сессии с cookie
curl -b "session_token=your_token" "https://v3.discours.io/graphql" \
-d '{"query":"query { getSession { success author { id } } }"}'
```
## 📊 Мониторинг
```python
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 интеграция!** 🔐✨