Initial commit: VoIdeaAI - voice-first AI idea assistant
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user