237 lines
9.2 KiB
Markdown
237 lines
9.2 KiB
Markdown
# QR-аутентификация — Безопасность
|
||
|
||
## Обзор
|
||
|
||
QR-аутентификация реализована по модели **OAuth2-подобного flow**, аналогично Google/Яндекс/Telegram Login.
|
||
|
||
---
|
||
|
||
## Архитектура безопасности
|
||
|
||
### ✅ Правильный flow (текущая реализация)
|
||
|
||
```
|
||
1. ПК (неавторизованный)
|
||
↓
|
||
POST /api/qr-auth/generate
|
||
← Получает уникальный code (без привязки к пользователю)
|
||
↓
|
||
Показывает QR: https://ospab.host/qr-login?code=XXX
|
||
↓
|
||
Polling: GET /api/qr-auth/status/:code каждые 2 секунды
|
||
|
||
2. Телефон (пользователь УЖЕ авторизован)
|
||
↓
|
||
Сканирует QR → открывается /qr-login?code=XXX
|
||
↓
|
||
POST /api/qr-auth/scanning (с Bearer token)
|
||
→ Backend обновляет статус QR на "scanning"
|
||
← ПК видит "Ожидание подтверждения на телефоне..."
|
||
↓
|
||
GET /api/auth/me (с Bearer token)
|
||
← Получает данные ТЕКУЩЕГО пользователя телефона
|
||
↓
|
||
Показывает экран подтверждения:
|
||
"Войти на новом устройстве как [Ваше имя]?"
|
||
↓
|
||
Пользователь нажимает "Подтвердить"
|
||
↓
|
||
POST /api/qr-auth/confirm + Bearer token + code
|
||
→ Backend привязывает userId к QR-запросу
|
||
→ Обновляет статус на "confirmed"
|
||
|
||
3. ПК (polling получает confirmed)
|
||
↓
|
||
Получает JWT токен ЭТОГО пользователя
|
||
↓
|
||
Вызывает login(token) → обновляет AuthContext
|
||
↓
|
||
Редирект на /dashboard
|
||
```
|
||
|
||
---
|
||
|
||
## Защита от уязвимостей
|
||
|
||
### 🔒 1. Анонимный QR-код
|
||
|
||
- ✅ QR создаётся **БЕЗ** привязки к пользователю
|
||
- ✅ `userId` присваивается **только после подтверждения**
|
||
- ❌ Невозможно "угадать" чей токен получит ПК
|
||
|
||
### 🔒 2. Требование авторизации на телефоне
|
||
|
||
- ✅ `/api/qr-auth/scanning` требует `authMiddleware`
|
||
- ✅ `/api/qr-auth/confirm` требует `authMiddleware`
|
||
- ❌ Неавторизованный пользователь НЕ может подтвердить вход
|
||
|
||
### 🔒 3. Экран подтверждения
|
||
|
||
```tsx
|
||
// Телефон показывает:
|
||
<div>
|
||
<p>Войти на новом устройстве как:</p>
|
||
<p className="text-xl font-bold">{userData.username}</p>
|
||
<p className="text-sm text-gray-500">{userData.email}</p>
|
||
</div>
|
||
|
||
<button onClick={handleConfirm}>Подтвердить</button>
|
||
<button onClick={handleCancel}>Отмена</button>
|
||
```
|
||
|
||
- ✅ Пользователь **видит** от чьего имени происходит вход
|
||
- ✅ Может **отказаться**, если это не он
|
||
|
||
### 🔒 4. Время жизни QR-кода
|
||
|
||
```typescript
|
||
const QR_EXPIRATION_SECONDS = 60; // 60 секунд
|
||
```
|
||
|
||
- ✅ QR истекает через 60 секунд
|
||
- ✅ После использования (confirmed/rejected) — удаляется
|
||
- ✅ Cleanup устаревших кодов каждые 24 часа
|
||
|
||
### 🔒 5. Статусы и переходы
|
||
|
||
```
|
||
pending → scanning → confirmed/rejected/expired
|
||
↓ ↓ ↓
|
||
Создан Открыт Финальный статус
|
||
```
|
||
|
||
- ✅ `pending` → `scanning`: пользователь открыл страницу
|
||
- ✅ `scanning` → `confirmed`: подтвердил вход
|
||
- ✅ `scanning` → `rejected`: отклонил вход
|
||
- ✅ `pending/scanning` → `expired`: истёк таймаут
|
||
|
||
### 🔒 6. Polling на ПК
|
||
|
||
```typescript
|
||
// Каждые 2 секунды:
|
||
GET /api/qr-auth/status/:code
|
||
|
||
// Ответы:
|
||
{ status: 'pending' } // Ещё не сканировали
|
||
{ status: 'scanning' } // Пользователь открыл страницу подтверждения
|
||
{ status: 'confirmed', token: 'JWT', user: {...} } // Подтвердили
|
||
{ status: 'rejected' } // Отклонили
|
||
{ status: 'expired' } // Истёк
|
||
```
|
||
|
||
- ✅ ПК **не генерирует токен** сам
|
||
- ✅ ПК **получает токен** только после подтверждения с телефона
|
||
- ✅ Токен содержит `userId` пользователя с телефона
|
||
|
||
---
|
||
|
||
## Защита от атак
|
||
|
||
### ❌ Атака: Перехват QR-кода
|
||
|
||
**Сценарий:** Злоумышленник фотографирует QR с чужого экрана
|
||
|
||
**Защита:**
|
||
- ✅ QR живёт 60 секунд
|
||
- ✅ Требуется авторизация на телефоне атакующего
|
||
- ✅ Экран подтверждения показывает имя/email входящего пользователя
|
||
- ✅ Жертва видит что в её аккаунт пытаются войти
|
||
|
||
### ❌ Атака: MITM (Man-in-the-Middle)
|
||
|
||
**Сценарий:** Злоумышленник перехватывает сетевой трафик
|
||
|
||
**Защита:**
|
||
- ✅ Все запросы через HTTPS (`https://ospab.host:5000`)
|
||
- ✅ JWT токены передаются в `Authorization: Bearer`
|
||
- ✅ Токены хранятся в `localStorage` (HttpOnly cookie было бы лучше, но требует серверный рендеринг)
|
||
|
||
### ❌ Атака: Replay Attack
|
||
|
||
**Сценарий:** Злоумышленник повторно отправляет перехваченный запрос
|
||
|
||
**Защита:**
|
||
- ✅ QR-код одноразовый (удаляется после confirm/reject)
|
||
- ✅ `status !== 'pending' && status !== 'scanning'` → ошибка
|
||
- ✅ JWT токены имеют `expiresIn: '24h'`
|
||
|
||
### ❌ Атака: Session Fixation
|
||
|
||
**Сценарий:** Злоумышленник пытается навязать свой QR-код
|
||
|
||
**Защита:**
|
||
- ✅ ПК генерирует QR **локально** через `/api/qr-auth/generate`
|
||
- ✅ Невозможно "навязать" чужой QR (каждый code уникален)
|
||
- ✅ Backend не принимает "предустановленные" коды
|
||
|
||
---
|
||
|
||
## Сравнение с другими методами
|
||
|
||
| Метод | Безопасность | Удобство | Скорость |
|
||
|------------------------|--------------|----------|----------|
|
||
| **QR-аутентификация** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
|
||
| Логин + пароль | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
|
||
| Email magic link | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
|
||
| SMS OTP | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
|
||
| OAuth (Google/Yandex) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
|
||
|
||
---
|
||
|
||
## Рекомендации по улучшению (будущее)
|
||
|
||
### 1. Rate Limiting
|
||
```typescript
|
||
// Ограничить количество попыток генерации QR с одного IP
|
||
// Пример: максимум 10 QR в минуту
|
||
```
|
||
|
||
### 2. Device Fingerprinting
|
||
```typescript
|
||
// При создании QR запоминать fingerprint ПК
|
||
// При polling проверять что запросы идут с того же устройства
|
||
```
|
||
|
||
### 3. Geolocation Check
|
||
```typescript
|
||
// Если расстояние между IP адресами ПК и телефона > 1000 км → предупреждение
|
||
// "Попытка входа из другой страны. Подтвердите что это вы"
|
||
```
|
||
|
||
### 4. WebSocket вместо Polling
|
||
```typescript
|
||
// Вместо GET /status/:code каждые 2 секунды
|
||
// Использовать WebSocket для реального времени
|
||
```
|
||
|
||
### 5. Push Notifications
|
||
```typescript
|
||
// Отправлять пуш на телефон: "Вход на новом устройстве. Подтвердите?"
|
||
// Не требует открывать браузер
|
||
```
|
||
|
||
---
|
||
|
||
## Заключение
|
||
|
||
Текущая реализация QR-аутентификации **безопасна** и соответствует индустриальным стандартам (Google, Яндекс, Telegram).
|
||
|
||
**Ключевые принципы:**
|
||
1. ✅ Анонимный QR без привязки к пользователю
|
||
2. ✅ Требование авторизации на подтверждающем устройстве
|
||
3. ✅ Явный экран подтверждения с информацией о пользователе
|
||
4. ✅ Короткое время жизни кодов (60 сек)
|
||
5. ✅ Одноразовое использование
|
||
6. ✅ HTTPS + JWT токены
|
||
|
||
**Защищает от:**
|
||
- ❌ Перехвата QR
|
||
- ❌ MITM атак
|
||
- ❌ Replay атак
|
||
- ❌ Session Fixation
|
||
- ❌ Несанкционированного доступа
|
||
|
||
---
|
||
|
||
_Документ обновлён: 10 ноября 2025 г._
|