Аутентификация и авторизация: API Key, JWT, OAuth 2.0
Аутентификация отвечает на вопрос «кто ты?», авторизация, «что тебе можно делать?». Это разные концепции, хотя их часто путают.
API Key
Самый простой способ. Клиент получает секретный ключ и передаёт его в каждом запросе.
GET /api/v1/data
X-API-Key: sk_live_abc123def456xyz789
Или через query parameter (хуже, ключ попадает в логи сервера и историю браузера):
GET /api/v1/data?api_key=sk_live_abc123 ← не делай так в продакшне
Как хранить на сервере
Никогда не храни API ключи в открытом виде. Только в хэшированном:
// При создании ключа
const rawKey = crypto.randomBytes(32).toString('hex');
const hashedKey = await bcrypt.hash(rawKey, 10);
await db.save({ hashedKey, userId });
// Пользователю показываем rawKey один раз
// При запросах сравниваем: bcrypt.compare(incomingKey, hashedKey)
Когда использовать
- Серверные интеграции (твой сервер вызывает чужой API)
- Простые случаи без пользовательских сессий
- Публичные API с биллингом по использованию (Stripe, OpenAI)
API Key не подходит для браузерного кода, ключ будет виден всем в исходниках.
Лучшие практики
- Разные ключи для разных окружений (
sk_test_vssk_live_) - Возможность отозвать ключ без изменения кода
- Ограничение по IP или домену-источнику
- Логирование каждого запроса с привязкой к ключу
JWT (JSON Web Token)
JWT, компактный самодостаточный токен. Три части разделённые точкой:
header.payload.signature
eyJhbGci... .eyJzdWIi... .SflKxw...
Header (алгоритм подписи):
{ "alg": "HS256", "typ": "JWT" }
Payload (данные о пользователе):
{
"sub": "42",
"role": "admin",
"iat": 1710494580,
"exp": 1710498180
}
Signature, HMAC или RSA подпись, гарантирует что токен не изменён.
Важно: JWT не шифруется
Payload можно декодировать на jwt.io без ключа. JWT гарантирует целостность (никто не изменил), но не конфиденциальность. Не клади в JWT пароли, платёжные данные, секреты.
Access + Refresh токены
Короткоживущий access + долгоживущий refresh:
Access token: 15 минут
Refresh token: 30 дней
Схема:
1. POST /auth/login → { access_token, refresh_token }
2. Запросы с access_token в Authorization: Bearer ...
3. Получаем 401 → POST /auth/refresh с refresh_token
4. Получаем новый access_token
5. Если refresh тоже истёк → логин заново
Зачем два токена? Короткий access снижает риск при перехвате. Refresh можно отозвать в БД (в отличие от JWT, он stateless). Пользователь не вводит пароль каждые 15 минут.
Где хранить токены в браузере
| Место | Защита от XSS | Защита от CSRF | Рекомендация |
|---|---|---|---|
httpOnly cookie |
✅ JS не может читать | ⚠️ Нужен CSRF-токен | ✅ Лучший вариант |
localStorage |
❌ Любой JS читает | ✅ Недоступен автоматически | ⚠️ Только при контроле XSS |
sessionStorage |
❌ Любой JS читает | ✅ | ❌ Теряется при закрытии вкладки |
Большинство production-приложений используют httpOnly cookie для refresh и memory (переменная) для access.
Отзыв JWT
Главная боль JWT, его нельзя отозвать до истечения срока. Решения:
1. Blacklist (чёрный список):
// При logout, добавляем jti (JWT ID) в Redis
await redis.setex(`blacklist:${jti}`, expiresIn, '1');
// При каждом запросе, проверяем
const blacklisted = await redis.get(`blacklist:${jti}`);
if (blacklisted) return res.status(401).json({ error: 'Token revoked' });
2. Короткий TTL, если access живёт 15 минут, максимальный ущерб от утечки ограничен.
3. Refresh token rotation, при каждом использовании refresh выдаётся новый, старый инвалидируется.
OAuth 2.0
OAuth 2.0, протокол делегированной авторизации. Позволяет пользователю дать приложению доступ к своим данным в другом сервисе, не передавая пароль.
Примеры: «Войти через Google», «Подключить GitHub», «Авторизоваться через VK».
Ключевые участники
- Resource Owner, пользователь
- Client, твоё приложение
- Authorization Server, Google, GitHub (выдаёт токены)
- Resource Server, API с данными пользователя
Authorization Code Flow
1. Редирект на Google:
https://accounts.google.com/oauth2/auth
?client_id=XXX&redirect_uri=https://yourapp.com/callback
&scope=email profile&response_type=code&state=csrf_token
2. Пользователь разрешает → Google редиректит:
https://yourapp.com/callback?code=AUTH_CODE&state=csrf_token
3. Твой бэкенд меняет code на токен:
POST https://oauth2.googleapis.com/token
{ code, client_id, client_secret, redirect_uri }
4. Получаем access_token, используем для запросов к Google API
Почему не передать токен напрямую в шаге 2? Code живёт секунды и обменивается на сервере с client_secret. Так секрет не попадает в браузер.
Client Credentials Flow
Для M2M (machine-to-machine), без участия пользователя:
POST /oauth/token
grant_type=client_credentials&client_id=XXX&client_secret=YYY&scope=read:orders
Ответ:
{ "access_token": "...", "expires_in": 3600 }
Сравнительная таблица
| API Key | JWT | OAuth 2.0 | |
|---|---|---|---|
| Сложность | Низкая | Средняя | Высокая |
| Пользовательский контекст | Нет | Да | Да |
| Отзыв токена | Да (в БД) | Сложно | Да |
| «Войти через Google» | ❌ | ❌ | ✅ |
| Серверные интеграции | ✅ | ✅ | ✅ |
| Браузерные приложения | ❌ | ✅ | ✅ |
Итого
- API Key, серверные интеграции, простые случаи. Храни хэшированным, никогда на клиенте
- JWT, стандарт для большинства REST API с пользователями. Access + Refresh,
httpOnly cookieдля хранения - OAuth 2.0, когда нужна федеративная аутентификация или доступ к ресурсам третьих сторон
- JWT нельзя просто «отозвать», используй blacklist в Redis или короткий TTL + refresh rotation