Files
voidea/docs/adr/003-oauth-schema.md
T

7.2 KiB
Raw Blame History

ADR-003: OAuth схема авторизации

Статус: принято Дата: 2026-05-10


Контекст

Проект VoIdea поддерживает несколько способов авторизации:

  • Email + пароль
  • OAuth провайдеры (Яндекс, Google, Apple)

Необходимо определить правила привязки провайдеров к аккаунтам.


Решение

Один пользователь = один провайдер

Нельзя привязать Google к аккаунту, зарегистрированному через Яндекс.


Правила

Основные

  1. При регистрации через OAuth — аккаунт привязан к этому провайдеру навсегда
  2. При регистрации через email — можно использовать только email + пароль
  3. Нельзя добавить второй провайдер — даже если email совпадает

Примеры

Действие Результат
Регистрация через Яндекс → Вход через Google Ошибка: создай новый аккаунт
Регистрация через Google → Вход через Яндекс Ошибка: создай новый аккаунт
Регистрация через email → Вход через Яндекс Ошибка: это разные аккаунты
Регистрация через Яндекс → Повторный вход через Яндекс Работает

Обоснование

Почему один провайдер

  1. Безопасность

    • Меньше точек входа для атак
    • Сложнее украсть аккаунт
    • Чёткая атрибуция действий
  2. Простота реализации

    • Не нужно merge аккаунтов
    • Не нужно решать конфликты данных
    • Понятная модель данных
  3. Privacy

    • Данные не смешиваются между провайдерами
    • Пользователь понимает что использует
  4. Яндекс vs Google

    • Разные экосистемы
    • Разные данные пользователя
    • Разная политика безопасности

Структура данных

Users table

CREATE TABLE users (
    id UUID PRIMARY KEY,
    
    -- Идентификация
    email VARCHAR(255) UNIQUE,          -- NULL если OAuth без email
    password_hash VARCHAR(255),          -- NULL если OAuth-only
    
    -- OAuth (только один провайдер)
    oauth_provider VARCHAR(20),          -- yandex|google|apple|null
    oauth_id VARCHAR(255),               -- ID в системе провайдера
    
    -- Метаданные
    email_verified BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    
    -- Constraints
    CONSTRAINT users_oauth_xor_email CHECK (
        -- OAuth с email
        (oauth_provider IS NOT NULL AND email IS NOT NULL) OR
        -- Email-only
        (oauth_provider IS NULL AND email IS NOT NULL AND password_hash IS NOT NULL)
    )
);

CREATE UNIQUE INDEX uq_users_oauth 
    ON users(oauth_provider, oauth_id) 
    WHERE oauth_provider IS NOT NULL;

OAuth Flow

Пример: Яндекс OAuth

async def yandex_oauth_callback(code: str, db: AsyncSession):
    # 1. Получаем access_token
    token_data = await yandex_api.get_token(code)
    
    # 2. Получаем данные пользователя
    user_data = await yandex_api.get_user_info(token_data.access_token)
    
    # 3. Проверяем/создаём аккаунт
    user = await db.execute(
        select(User).where(
            User.oauth_provider == 'yandex',
            User.oauth_id == user_data.id
        )
    )
    
    if not user:
        # Новый пользователь
        user = User(
            email=user_data.email,
            oauth_provider='yandex',
            oauth_id=user_data.id
        )
        db.add(user)
        await db.commit()
    
    # 4. Создаём JWT session
    return create_jwt_session(user.id)

Google OAuth

Аналогично Яндексу, с заменой endpoint-ов.

Различия

Параметр Яндекс Google
OAuth endpoint oauth.yandex.ru oauth2.googleapis.com
User info login.yandex.ru www.googleapis.com/oauth2/v2/userinfo
Scope login:email, profile email, profile

Apple OAuth (отложено)

Apple будет реализован ближе к коммерческой версии.

Требования

  • App Store Developer Account ($99/год)
  • Private Key для подписи (в Keychain)
  • Тот же принцип: один пользователь = один провайдер

Защита от привязки чужого аккаунта

Проблема

Злоумышленник может попытаться привязать Google к чужому email.

Решение

  1. Email verification

    • OAuth возвращает verified email
    • Привязка только verified email
  2. Separate tables

    • OAuth и email разделены логически
    • Разные flows для входа
  3. Audit logging

    • Все попытки OAuth логируются
    • Подозрительная активность → SecurityAgent

Последствия

Положительные

  • Простая модель данных
  • Безопасность выше
  • Понятно для пользователя
  • Легко реализовать

Отрицательные

  • Пользователь не может "добавить" Google к существующему аккаунту
  • При потере доступа к провайдеру — сложнее восстановить
  • Нельзя merge аккаунты

Workaround для пользователя

При потере доступа к OAuth провайдеру:

  1. Обращение в поддержку
  2. Подтверждение личности
  3. Смена email (если нужно)
  4. Сброс пароля на email

Конфигурация

# .env
OAUTH_YANDEX_ID=your_yandex_client_id
OAUTH_YANDEX_SECRET=your_yandex_secret
OAUTH_YANDEX_REDIRECT_URI=http://localhost:8020/auth/yandex/callback

OAUTH_GOOGLE_ID=your_google_client_id
OAUTH_GOOGLE_SECRET=your_google_secret
OAUTH_GOOGLE_REDIRECT_URI=http://localhost:8020/auth/google/callback

# Apple - зарезервировано для будущего
# OAUTH_APPLE_ID=
# OAUTH_APPLE_TEAM_ID=

Ответственный

Decision maker: Owner Review date: При добавлении Apple OAuth


Создано: 2026-05-10 См. также: docs/backlog/oauth-schema-note.md