7.2 KiB
7.2 KiB
ADR-003: OAuth схема авторизации
Статус: принято Дата: 2026-05-10
Контекст
Проект VoIdea поддерживает несколько способов авторизации:
- Email + пароль
- OAuth провайдеры (Яндекс, Google, Apple)
Необходимо определить правила привязки провайдеров к аккаунтам.
Решение
Один пользователь = один провайдер
Нельзя привязать Google к аккаунту, зарегистрированному через Яндекс.
Правила
Основные
- При регистрации через OAuth — аккаунт привязан к этому провайдеру навсегда
- При регистрации через email — можно использовать только email + пароль
- Нельзя добавить второй провайдер — даже если email совпадает
Примеры
| Действие | Результат |
|---|---|
| Регистрация через Яндекс → Вход через Google | ❌ Ошибка: создай новый аккаунт |
| Регистрация через Google → Вход через Яндекс | ❌ Ошибка: создай новый аккаунт |
| Регистрация через email → Вход через Яндекс | ❌ Ошибка: это разные аккаунты |
| Регистрация через Яндекс → Повторный вход через Яндекс | ✅ Работает |
Обоснование
Почему один провайдер
-
Безопасность
- Меньше точек входа для атак
- Сложнее украсть аккаунт
- Чёткая атрибуция действий
-
Простота реализации
- Не нужно merge аккаунтов
- Не нужно решать конфликты данных
- Понятная модель данных
-
Privacy
- Данные не смешиваются между провайдерами
- Пользователь понимает что использует
-
Яндекс 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-ов.
Различия
| Параметр | Яндекс | |
|---|---|---|
| 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.
Решение
-
Email verification
- OAuth возвращает verified email
- Привязка только verified email
-
Separate tables
- OAuth и email разделены логически
- Разные flows для входа
-
Audit logging
- Все попытки OAuth логируются
- Подозрительная активность → SecurityAgent
Последствия
Положительные
- Простая модель данных
- Безопасность выше
- Понятно для пользователя
- Легко реализовать
Отрицательные
- Пользователь не может "добавить" Google к существующему аккаунту
- При потере доступа к провайдеру — сложнее восстановить
- Нельзя merge аккаунты
Workaround для пользователя
При потере доступа к OAuth провайдеру:
- Обращение в поддержку
- Подтверждение личности
- Смена email (если нужно)
- Сброс пароля на 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