9.2 KiB
9.2 KiB
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. Экран подтверждения
// Телефон показывает:
<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-кода
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 на ПК
// Каждые 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
// Ограничить количество попыток генерации QR с одного IP
// Пример: максимум 10 QR в минуту
2. Device Fingerprinting
// При создании QR запоминать fingerprint ПК
// При polling проверять что запросы идут с того же устройства
3. Geolocation Check
// Если расстояние между IP адресами ПК и телефона > 1000 км → предупреждение
// "Попытка входа из другой страны. Подтвердите что это вы"
4. WebSocket вместо Polling
// Вместо GET /status/:code каждые 2 секунды
// Использовать WebSocket для реального времени
5. Push Notifications
// Отправлять пуш на телефон: "Вход на новом устройстве. Подтвердите?"
// Не требует открывать браузер
Заключение
Текущая реализация QR-аутентификации безопасна и соответствует индустриальным стандартам (Google, Яндекс, Telegram).
Ключевые принципы:
- ✅ Анонимный QR без привязки к пользователю
- ✅ Требование авторизации на подтверждающем устройстве
- ✅ Явный экран подтверждения с информацией о пользователе
- ✅ Короткое время жизни кодов (60 сек)
- ✅ Одноразовое использование
- ✅ HTTPS + JWT токены
Защищает от:
- ❌ Перехвата QR
- ❌ MITM атак
- ❌ Replay атак
- ❌ Session Fixation
- ❌ Несанкционированного доступа
Документ обновлён: 10 ноября 2025 г.