Files
core/docs/oauth-frontend-integration.md
Untone 05c188df62
Some checks failed
Deploy on push / deploy (push) Failing after 39s
[0.9.29] - 2025-09-26
### 🚨 CRITICAL Security Fixes
- **🔒 Open Redirect Protection**: Добавлена строгая валидация redirect_uri против whitelist доменов
- **🔒 Rate Limiting**: Защита OAuth endpoints от брутфорса (10 попыток за 5 минут на IP)
- **🔒 Logout Endpoint**: Критически важный endpoint для безопасного отзыва httpOnly cookies
- **🔒 Provider Validation**: Усиленная валидация OAuth провайдеров с логированием атак
- **🚨 GlitchTip Alerts**: Автоматические алерты безопасности в GlitchTip при критических событиях

### 🛡️ Security Modules
- **auth/oauth_security.py**: Модуль безопасности OAuth с валидацией и rate limiting + GlitchTip алерты
- **auth/logout.py**: Безопасный logout с поддержкой JSON API и browser redirect
- **tests/test_oauth_security.py**: Комплексные тесты безопасности (11 тестов)
- **tests/test_oauth_glitchtip_alerts.py**: Тесты интеграции с GlitchTip (8 тестов)

### 🔧 OAuth Improvements
- **Minimal Flow**: Упрощен до минимума - только httpOnly cookie, нет JWT в URL
- **Simple Logic**: Нет error параметра = успех, максимальная простота
- **DRY Refactoring**: Устранено дублирование кода в logout и валидации

### 🎯 OAuth Endpoints
- **Старт**: `v3.dscrs.site/oauth/{provider}` - с rate limiting и валидацией
- **Callback**: `v3.dscrs.site/oauth/{provider}/callback` - безопасный redirect_uri
- **Logout**: `v3.dscrs.site/auth/logout` - отзыв httpOnly cookies
- **Финализация**: `testing.discours.io/oauth?redirect_url=...` - минимальная схема

### 📊 Security Test Coverage
-  Open redirect attack prevention
-  Rate limiting protection
-  Provider validation
-  Safe fallback mechanisms
-  Cookie security (httpOnly + Secure + SameSite)
-  GlitchTip integration (8 тестов алертов)

### 📝 Documentation
- Создан `docs/oauth-minimal-flow.md` - полное описание минимального flow
- Обновлена документация OAuth в `docs/auth/oauth.md`
- Добавлены security best practices
2025-09-26 21:03:45 +03:00

9.1 KiB
Raw Blame History

OAuth Frontend Integration для testing.discours.io

📋 Полный flow:

  1. OAuth success → бэкенд генерирует JWT
  2. Редирект: /oauth?access_token=JWT&redirect_url=... + httpOnly cookie
  3. Фронт роут: localStorage.setItem('auth_token', token)
  4. SessionProvider: loadSession() → использует localStorage токен
  5. GraphQL клиент: credentials: 'include' → использует httpOnly cookie

🔧 Frontend Implementation

1. OAuth Route Handler (/oauth)

// 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 accessToken = searchParams.access_token
    const redirectUrl = searchParams.redirect_url || '/'

    if (error) {
      // Обработка ошибок OAuth
      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('/')
      return
    }

    if (accessToken) {
      try {
        // 1. Сохраняем JWT в localStorage для быстрого доступа
        localStorage.setItem('auth_token', accessToken)
        
        // 2. SessionProvider загружает сессию (использует localStorage токен)
        await loadSession()
        
        // 3. Очищаем URL от токена (безопасность)
        window.history.replaceState({}, document.title, '/oauth-success')
        
        // 4. Редиректим на исходную страницу через 1 секунду
        setTimeout(() => {
          navigate(decodeURIComponent(redirectUrl))
        }, 1000)
        
      } catch (error) {
        console.error('Failed to load session:', error)
        localStorage.removeItem('auth_token')
        navigate('/')
      }
    } else {
      // Неожиданный случай
      navigate('/')
    }
  })

  return (
    <div class="oauth-callback">
      <div class="loading">
        <h2>Completing authentication...</h2>
        <div class="spinner"></div>
      </div>
    </div>
  )
}

2. OAuth Initiation

// utils/auth.ts
export const oauth = (provider: string) => {
  // Простой редирект - backend получит redirect_uri из Referer header
  window.location.href = `https://v3.dscrs.site/oauth/${provider}`
}

// Использование в компонентах
import { oauth } from '../utils/auth'

const LoginButton = () => (
  <button onClick={() => oauth('github')}>
    Login with GitHub
  </button>
)

3. Session Provider

// context/SessionProvider.tsx
import { createContext, useContext, createSignal, onMount } from 'solid-js'

interface SessionContextType {
  user: () => User | null
  isAuthenticated: () => boolean
  loadSession: () => Promise<void>
  logout: () => void
}

const SessionContext = createContext<SessionContextType>()

export function SessionProvider(props: { children: any }) {
  const [user, setUser] = createSignal<User | null>(null)
  
  const isAuthenticated = () => !!user()
  
  const loadSession = async () => {
    try {
      // Проверяем localStorage токен
      const token = localStorage.getItem('auth_token')
      if (!token) {
        setUser(null)
        return
      }
      
      // Загружаем профиль пользователя через GraphQL (использует httpOnly cookie)
      const response = await client.query({
        query: GET_CURRENT_USER,
        fetchPolicy: 'network-only' // Всегда свежие данные
      })
      
      if (response.data?.currentUser) {
        setUser(response.data.currentUser)
      } else {
        // Токен невалидный, очищаем
        localStorage.removeItem('auth_token')
        setUser(null)
      }
    } catch (error) {
      console.error('Failed to load session:', error)
      localStorage.removeItem('auth_token')
      setUser(null)
    }
  }
  
  const logout = () => {
    localStorage.removeItem('auth_token')
    setUser(null)
    
    // Опционально: вызов logout endpoint для очистки httpOnly cookie
    fetch('https://v3.dscrs.site/auth/logout', {
      method: 'POST',
      credentials: 'include'
    })
  }
  
  // Загружаем сессию при инициализации
  onMount(() => {
    loadSession()
  })
  
  return (
    <SessionContext.Provider value={{
      user,
      isAuthenticated,
      loadSession,
      logout
    }}>
      {props.children}
    </SessionContext.Provider>
  )
}

export const useSession = () => {
  const context = useContext(SessionContext)
  if (!context) {
    throw new Error('useSession must be used within SessionProvider')
  }
  return context
}

4. GraphQL Client Setup

// graphql/client.ts
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client'

const httpLink = createHttpLink({
  uri: 'https://v3.dscrs.site/graphql',
  credentials: 'include', // ✅ КРИТИЧНО: отправляет httpOnly cookie
})

export const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      errorPolicy: 'all'
    },
    query: {
      errorPolicy: 'all'
    }
  }
})

5. API Client для прямых вызовов

// utils/api.ts
class ApiClient {
  private baseUrl = 'https://v3.dscrs.site'
  
  private getAuthHeaders() {
    const token = localStorage.getItem('auth_token')
    return token ? { 'Authorization': `Bearer ${token}` } : {}
  }
  
  async request(endpoint: string, options: RequestInit = {}) {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...this.getAuthHeaders(),
        ...options.headers
      },
      credentials: 'include' // Для httpOnly cookie
    })
    
    if (!response.ok) {
      if (response.status === 401) {
        // Токен истек, очищаем localStorage
        localStorage.removeItem('auth_token')
        window.location.href = '/login'
      }
      throw new Error(`API Error: ${response.status}`)
    }
    
    return response.json()
  }
  
  // Методы для различных API calls
  async uploadFile(file: File) {
    const formData = new FormData()
    formData.append('file', file)
    
    return this.request('/upload', {
      method: 'POST',
      body: formData,
      headers: this.getAuthHeaders() // Только Authorization header, без Content-Type
    })
  }
}

export const apiClient = new ApiClient()

🔒 Безопасность

Преимущества двойной схемы:

  1. httpOnly Cookie - защита от XSS для GraphQL
  2. localStorage JWT - быстрый доступ для API calls
  3. Automatic cleanup - токен удаляется при ошибках 401
  4. URL cleanup - токен не остается в истории браузера

Обработка ошибок:

// utils/errorHandler.ts
export const handleAuthError = (error: any) => {
  if (error.networkError?.statusCode === 401) {
    // Токен истек
    localStorage.removeItem('auth_token')
    window.location.href = '/login'
  }
}

// В Apollo Client
import { onError } from '@apollo/client/link/error'

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (networkError?.statusCode === 401) {
    handleAuthError(networkError)
  }
})

🧪 Testing

E2E Test

// tests/oauth.spec.ts
import { test, expect } from '@playwright/test'

test('OAuth flow works correctly', async ({ page }) => {
  // 1. Инициация OAuth
  await page.goto('https://testing.discours.io')
  await page.click('[data-testid="github-login"]')
  
  // 2. Проверяем редирект на GitHub
  await expect(page).toHaveURL(/github\.com\/login\/oauth\/authorize/)
  
  // 3. Симулируем успешный callback
  await page.goto('https://testing.discours.io/oauth?access_token=test_jwt&redirect_url=%2Fdashboard')
  
  // 4. Проверяем что токен сохранился
  const token = await page.evaluate(() => localStorage.getItem('auth_token'))
  expect(token).toBe('test_jwt')
  
  // 5. Проверяем редирект на dashboard
  await expect(page).toHaveURL('https://testing.discours.io/dashboard')
})

📊 Monitoring

Метрики для отслеживания:

  • OAuth success rate
  • Token validation errors
  • Session load time
  • Cookie/localStorage sync issues