Защита API: JWT vs OAuth2 - выбираем правильно

В современной веб-разработке безопасность API является критически важной. Два наиболее популярных механизма — JWT и OAuth2 — часто вызывают путаницу. В этой статье мы разберем когда использовать каждый из них, как их комбинировать и какие подводные камни избегать.

Основные понятия: Аутентификация vs Авторизация

Аутентификация (Authentication) - процесс проверки подлинности пользователя. "Кто вы?"

Авторизация (Authorization) - процесс проверки прав доступа. "Что вам разрешено делать?"

JWT (JSON Web Token)

JWT — это стандарт для создания токенов доступа, основанный на JSON. Токен содержит полезную нагрузку (payload) с данными пользователя и подписан цифровой подписью.

Структура JWT

header.payload.signature

// Пример декодированного JWT:
{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "roles": ["user", "admin"]
}

Типичный flow аутентификации с JWT

// 1. Клиент отправляет credentials
POST /api/auth/login
{
  "username": "user",
  "password": "pass"
}

// 2. Сервер проверяет и возвращает JWT
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 3600
}

// 3. Клиент использует токен в заголовках
GET /api/protected
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Преимущества JWT

Недостатки JWT

OAuth 2.0

OAuth 2.0 — это протокол авторизации, который позволяет приложениям получать ограниченный доступ к пользовательским данным без раскрытия паролей.

Ключевые роли в OAuth2

Authorization Code Flow (наиболее безопасный)

// 1. Редирект на страницу авторизации
https://auth-server.com/authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=CALLBACK_URL&
  scope=read:user&
  state=RANDOM_STRING

// 2. Пользователь авторизуется и получает code
// 3. Обмен code на access token
POST /oauth/token
{
  "grant_type": "authorization_code",
  "client_id": "CLIENT_ID",
  "client_secret": "CLIENT_SECRET",
  "code": "AUTHORIZATION_CODE",
  "redirect_uri": "CALLBACK_URL"
}

// 4. Получение access token
{
  "access_token": "ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "REFRESH_TOKEN"
}

Когда использовать OAuth2

Сравнительная таблица: JWT vs OAuth2

Критерий JWT OAuth2
Тип Стандарт для токенов Фреймворк авторизации
Основная цель Аутентификация Делегированная авторизация
Состояние Stateless Stateful (требует хранилище)
Сложность Простая реализация Сложная настройка
Использование Внутренние API Публичные API, сторонние приложения
Отзыв токенов Сложно (через blacklist) Легко (через revocation endpoint)

Комбинирование JWT и OAuth2

Часто лучшим решением является комбинация обоих подходов. OAuth2 для потока авторизации и JWT для токенов доступа.

Гибридная архитектура

// Authorization Server выдает JWT токены
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer", 
  "expires_in": 3600,
  "refresh_token": "REFRESH_TOKEN"
}

// Resource Server проверяет JWT без обращения к Authorization Server
function verifyToken(token) {
  try {
    const decoded = jwt.verify(token, PUBLIC_KEY);
    return decoded; // { sub: "user123", scope: ["read", "write"] }
  } catch (error) {
    throw new Error('Invalid token');
  }
}

Security Best Practices

Для JWT

// Хорошая практика - короткоживущие access tokens
const accessToken = jwt.sign(
  { userId: user.id, roles: user.roles },
  process.env.JWT_SECRET,
  { expiresIn: '15m' }
);

const refreshToken = jwt.sign(
  { userId: user.id },
  process.env.REFRESH_SECRET, 
  { expiresIn: '7d' }
);

Для OAuth2

Common Pitfalls и как их избежать

1. Хранение чувствительных данных в JWT

// Плохо
{
  "userId": 123,
  "password": "hashed_password", // Не хранить!
  "creditCard": "1234-5678-9012-3456" // Опасно!
}

// Хорошо
{
  "sub": "user123",
  "roles": ["user"],
  "permissions": ["read:profile"]
}

2. Слишком длительное время жизни токенов

// Плохо
{ expiresIn: '30d' } // 30 дней - слишком долго

// Хорошо  
{ expiresIn: '15m' } // 15 минут + refresh token

3. Неправильная валидация audience

// Хорошая практика - проверять audience
jwt.verify(token, secret, {
  audience: 'https://api.myapp.com',
  issuer: 'https://auth.myapp.com'
});

4. Отсутствие защиты от replay атак

// Используйте jti (JWT ID) и храните использованные токены
{
  "jti": "unique-token-id",
  "iat": 1516239022,
  "exp": 1516242622
}

Практические сценарии выбора

Сценарий 1: Монолитное веб-приложение

Рекомендация: Сессии или простые JWT

Используйте httpOnly cookies с короткоживущими JWT для stateless аутентификации.

Сценарий 2: Мобильное приложение + API

Рекомендация: JWT с refresh tokens

Access token: 15-30 минут, refresh token: 7-30 дней с возможностью отзыва.

Сценарий 3: Публичное API для сторонних разработчиков

Рекомендация: OAuth2 с JWT токенами

Authorization Code Flow with PKCE для безопасной авторизации.

Сценарий 4: Микросервисная архитектура

Рекомендация: JWT для межсервисной коммуникации

Каждый сервис может самостоятельно проверять JWT без центрального сервера.

Реализация middleware для Express.js

// JWT middleware
const authenticateJWT = (req, res, next) => {
  const authHeader = req.headers.authorization;
  
  if (authHeader) {
    const token = authHeader.split(' ')[1];
    
    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
      if (err) {
        return res.sendStatus(403);
      }
      
      req.user = user;
      next();
    });
  } else {
    res.sendStatus(401);
  }
};

// RBAC middleware
const requireRole = (role) => {
  return (req, res, next) => {
    if (!req.user || !req.user.roles.includes(role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
};

// Использование
app.get('/api/admin', authenticateJWT, requireRole('admin'), (req, res) => {
  res.json({ message: 'Admin access granted' });
});

Заключение

Используйте JWT когда:

Используйте OAuth2 когда:

Комбинируйте когда:

Правильный выбор механизма безопасности зависит от конкретных требований вашего приложения. Начинайте с простых решений и усложняйте по мере необходимости, всегда следуя security best practices.