Files
core/docs/auth/oauth.md
Untone fb98a1c6c8
All checks were successful
Deploy on push / deploy (push) Successful in 4m32s
[0.9.28] - OAuth/Auth with httpOnly cookie
2025-09-28 12:22:37 +03:00

13 KiB
Raw Blame History

🔐 OAuth Integration Guide

🎯 Обзор

Система OAuth интеграции с httpOnly cookies для максимальной безопасности. Поддержка популярных провайдеров с единым подходом к аутентификации.

🔄 Архитектура 2025: httpOnly cookies для всех

sequenceDiagram
    participant U as User
    participant F as Frontend
    participant B as Backend
    participant P as OAuth Provider

    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 (Фронтенд)

// Простой редирект - 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 (/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>
  );
}
// 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
  2. APIs & ServicesCredentialsOAuth 2.0 Client ID
  3. 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

  1. GitHub Developer Settings
  2. New OAuth App
  3. 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

  1. Yandex OAuth
  2. Создать новое приложение
  3. Callback URI: https://your-domain.com/oauth/yandex/callback
  4. Права: login:info, login:email, login:avatar
YANDEX_CLIENT_ID=your_yandex_client_id
YANDEX_CLIENT_SECRET=your_yandex_client_secret

VK OAuth

  1. VK Developers
  2. Создать приложениеВеб-сайт
  3. Redirect URI: https://your-domain.com/oauth/vk/callback
VK_CLIENT_ID=your_vk_app_id
VK_CLIENT_SECRET=your_vk_secure_key

🛡️ Безопасность

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