243 lines
7.2 KiB
Markdown
243 lines
7.2 KiB
Markdown
# 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* |