# 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 ```sql 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 ```python 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 --- ## Конфигурация ```bash # .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*