Создание продуманной аутентификации API с JWT: Безопасность за рамками туториалов

Ключевым элементом современных веб-архитектур стало использование JSON Web Tokens (JWT) для управления доступом. Часто приходится видеть, как система, построенная по "5-минутному гайду", открывает серьёзные уязвимости. Рассмотрим недооцененные аспекты их применения — от ротации секретов до борьбы с чёрными списками.

От генерации до промежуточного ПО: шаблоны ошибок

Начнём с классического экспресс-мидлвара:

javascript
// Типичная опасная реализация
app.post('/login', (req, res) => {
  const user = authenticate(req.body);
  const token = jwt.sign({ sub: user.id }, 'your-256-bit-secret', { expiresIn: '30d' });
  res.json({ token });
});

// Проверка в мидлваре
const authenticateJWT = (req, res, next) => {
  const token = req.header('Authorization')?.split(' ')[1];
  if (!token) return res.sendStatus(401);

  jwt.verify(token, 'your-256-bit-secret', (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
};

Две критические проблемы:

  1. Жёстко закодированный секрет
  2. Отсутствие проверки отозванных токенов

Динамические истории вращения ключей

Статичные секреты — гарантированная угроза при компрометации. Решение:

javascript
// Генерация ротируемого секрета
const keyPairs = [{
  keyId: '2024-06-k1',
  publicKey: readFileSync('public_key.pem'),
  privateKey: readFileSync('private_key.pem')
}];

app.post('/login', (req, res) => {
  const user = authenticate(req.body);
  const currentKey = keyPairs[0]; // Используем активный ключ
  const token = jwt.sign(
    { sub: user.id },
    currentKey.privateKey,
    { 
      algorithm: 'RS256',
      expiresIn: '15m',
      issuer: 'api.example.com',
      keyid: currentKey.keyId
    }
  );
  res.json({ token: token, keyId: currentKey.keyId });
});

// Проверяющий мидлвар
const verifyToken = (token) => {
  const header = jwt.decode(token, { complete: true })?.header;
  if (!header?.kid) throw new Error('Invalid token');
  
  const keyPair = keyPairs.find(k => k.keyId === header.kid);
  if (!keyPair) throw new Error('Key not found');
  
  return jwt.verify(token, keyPair.publicKey, {
    algorithms: ['RS256'],
    issuer: 'api.example.com'
  });
};

Ротация ключей ежемесячно позволяет:

  • Автоматизировать вывод старых ключей из эксплуатации
  • Настроить разный уровень защиты токенов в базе данных
  • Идентифицировать источник угроз при компрометации одного из ключей

Динамическое аннулирование токенов через blacklist

В отличие от сессий, stateless-токены сложнее отзывать. Паттерн с "чёрными списками" требует ювелирной реализации:

javascript
// Redis хранилище для отозванных токенов
const revokedTokens = createRedisClient();

// Регистрация отозванного токена с TTL экспирации
const revokeToken = async (token) => {
  const { jwtid, exp } = jwt.decode(token);
  await revokedTokens.set(`revoked:${jwtid}`, 'true', 'PX', exp * 1000 - Date.now());
};

// Обновлённый верификатор
const verifyToken = async (token) => {
  const decoded = verifyToken(token);

  // Проверка на аннулирование
  const keyId = decoded.jti;
  const isRevoked = await revokedTokens.get(`revoked:${keyId}`);
  if (isRevoked) throw new Error('Token revoked');

  return decoded;
};

// Отмена токена при смене пароля пользователя
app.post('/reset-password', authenticateJWT, async (req, res) => {
  await revokeToken(req.headers.authorization.split(' ')[1]);
  // ...смена пароля
  res.sendStatus(204);
});

Мониторинг размера хранилища критичен — учитывайте период жизни токена при установке TTL.

Тактика для короткой жизни токенов

Сокращайте время жизни access-токенов до минут, применяя refresh-механизм:

javascript
// Параметры токенов
const ACCESS_TOKEN_EXP = '15m';
const REFRESH_TOKEN_EXP = '7d';

// Логин возвращает пару токенов
app.post('/login', (req, res) => {
  const user = authenticate(req.body);
  const accessToken = generateAccessToken(user.id);
  const refreshToken = jwt.sign({ jti: generateId() }, refreshSecret, {
    subject: user.id,
    expiresIn: REFRESH_TOKEN_EXP
  });
  
  // Безопасная передача refreshToken в httpOnly cookie
  res.cookie('refresh_token', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'Strict',
    maxAge: 604800000 // 7 дней
  }).json({ accessToken });
});

// Обновление пары токенов
app.post('/refresh', async (req, res) => {
  const refreshToken = req.cookies.refresh_token;
  try {
    const payload = jwt.verify(refreshToken, refreshSecret);
    
    // Отзываем использованный refresh токен
    await revokeToken(refreshToken);
    
    const newAccessToken = generateAccessToken(payload.sub);
    const newRefreshToken = jwt.sign({ jti: generateId() }, refreshSecret, {
      subject: payload.sub,
      expiresIn: REFRESH_TOKEN_EXP
    });
    
    res.cookie('refresh_token', newRefreshToken, secureCookieOptions)
      .json({ accessToken: newAccessToken });
  } catch (err) {
    res.clearCookie('refresh_token').sendStatus(401);
  }
});

Развертывание экосистемы

Реализовать такие практики — только начало. Мониторинг должен включать:

  1. Графики частоты отзывов токенов
  2. Сигналы на внезапные всплески аутентификационных запросов
  3. Алгоритм автоматической ротации ключей при росте ошибок верификации
  4. 1 ключ для высококритичных систем, 4 ключа для общедоступного API — запаса на случай компрометации

Микросервисная архитектура требует централизованного кейманагера и логауншеров:

mermaid
sequenceDiagram
    participant C as Client
    participant A as Auth Service
    participant B as Business Service
    participant R as Revoked Tokens Store

    C->>A: Login
    A->>C: AccessToken + HttpOnly Cookie
    C->>B: Request with AccessToken
    B->>A: Introspect token
    A->>R: Проверка отзыва
    R->>A: Статус токена
    A->>B: User data
    B->>C: Response

При проектировании узлов интроспекции токенов ключевое внимание уделяйте:

  • Географическому расположению относительно сервисов
  • Обеспечению стабильного времени ответа
  • Поддержанию нескольких мест хранения чёрных списков

Результат продуманной стратегии

Применяя динамическую криптографию и механизмы аннулирования, достигаем баланса между производительностью API и контролем безопасности. Не забывайте, что даже лучшие реализации требуют:

  • Автоматического перевыпуска ключей каждые 30-90 дней
  • "Затенения" активности пользователей при обновлении токенов
  • Идентификаторов токенов в трендах мониторинга

Хорошая новость: современные платформы (Kong, Apollo GraphQL) вторично предоставляют инструменты для управления устройствами проверки. Код примеров иллюстрирует концепции, а не слепое копирование — адаптируйте тактику под данные угроз. Не на миллион пользователей? Логируйте отзывы в оперативную память приложения. Особенность JWT не в их простоте использования — в возможности решать комплексные задачи аутентификации без фатальных компромиссов.