[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

@@ -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 интеграция!** 🔐✨