Переход с JWT на Iron

В архитектуре серверных приложений часто возникает необходимость пересмотра способа хранения и передачи защищённых данных между клиентом и сервером. JSON Web Token (JWT) долгое время используется как стандарт де-факто для stateless-аутентификации, однако в ряде сценариев он начинает проявлять ограничения, связанные с безопасностью и управлением жизненным циклом токена.

Основные проблемы JWT в прикладных системах:

  • невозможность мгновенной инвалидации токена без дополнительного хранилища
  • риск утечки данных, закодированных внутри payload (base64 не является шифрованием)
  • необходимость усложнённой схемы refresh-токенов
  • уязвимость к ошибкам реализации проверки подписи

В отличие от JWT, подход Iron, реализованный в библиотеке @hapi/iron, строится не на подписанном, а на зашифрованном и сериализованном контейнере данных. Это принципиально меняет модель угроз: содержимое не просто защищено от подделки, но и полностью скрыто от клиента.


Модель безопасности Iron

Iron реализует концепцию sealed data — «запечатанных» данных. Внутри используется симметричное шифрование, а не только подпись.

Ключевые свойства:

  • конфиденциальность данных (в отличие от JWT payload)
  • невозможность модификации без ключа
  • встроенная защита от повторного воспроизведения (через salt и timestamp)
  • единый механизм упаковки и распаковки

В основе используется криптографический алгоритм, а не просто HMAC.


Сравнение JWT и Iron

JWT

JWT состоит из трёх частей:

  • header
  • payload
  • signature

Payload доступен любому, кто имеет токен.

header.payload.signature

Даже при наличии подписи содержимое не защищено от чтения.


Iron

Iron хранит данные в зашифрованном виде:

  • невозможно извлечь payload без ключа
  • структура токена не предполагает читаемости
  • используется сериализация + шифрование + MAC

Установка и базовая настройка Iron

Библиотека:

npm install @hapi/iron

Импорт:

import Iron from '@hapi/iron';

Базовые операции: seal и unseal

Запечатывание данных (аналог генерации токена)

const password = 'super-secure-password-min-32-chars';

const data = {
  userId: 42,
  role: 'admin'
};

const sealed = await Iron.seal(
  data,
  password,
  Iron.defaults
);

console.log(sealed);

Результат — строка, содержащая полностью зашифрованный объект.


Распаковка данных

const unsealed = await Iron.unseal(
  sealed,
  password,
  Iron.defaults
);

console.log(unsealed);

На выходе возвращается исходный объект:

{
  userId: 42,
  role: 'admin'
}

Переходная архитектура с JWT на Iron

Переход не сводится к простой замене библиотек. Меняется сама модель хранения сессий.

Было (JWT модель)

const token = jwt.sign(
  { userId: 42, role: 'admin' },
  secret,
  { expiresIn: '15m' }
);

Сервер при каждом запросе:

  • декодирует JWT
  • проверяет подпись
  • читает payload

Стало (Iron модель)

const session = await Iron.seal(
  { userId: 42, role: 'admin' },
  password,
  Iron.defaults
);

Сервер:

  • расшифровывает данные
  • получает объект сессии
  • не выполняет проверку подписи как отдельный шаг (она встроена)

Интеграция Iron в middleware

Пример middleware для Node.js (Express):

import Iron from '@hapi/iron';

const password = process.env.IRON_PASSWORD;

export async function sessionMiddleware(req, res, next) {
  const sealed = req.headers['x-session'];

  if (!sealed) {
    return res.status(401).send('No session');
  }

  try {
    const session = await Iron.unseal(
      sealed,
      password,
      Iron.defaults
    );

    req.session = session;
    next();
  } catch (err) {
    res.status(401).send('Invalid session');
  }
}

Управление временем жизни сессии

Iron не навязывает TTL как JWT. Однако время жизни можно реализовать внутри данных.

const session = {
  userId: 42,
  role: 'admin',
  exp: Date.now() + 15 * 60 * 1000
};

Проверка:

if (session.exp < Date.now()) {
  throw new Error('Session expired');
}

Ротация ключей

Одно из ключевых преимуществ Iron — простота смены ключей без сложной инфраструктуры.

Подход:

  • поддержка массива ключей
  • попытка расшифровки с каждым ключом
const passwords = [
  'old-password-1',
  'current-password-2'
];

let session;

for (const pwd of passwords) {
  try {
    session = await Iron.unseal(sealed, pwd, Iron.defaults);
    break;
  } catch {}
}

Миграционная стратегия

Этап 1: параллельная поддержка JWT и Iron

Сервер принимает оба формата:

  • если JWT → старая логика
  • если Iron → новая логика

Этап 2: выпуск Iron-сессий

После аутентификации создаются только Iron-токены:

const token = await Iron.seal(userData, password, Iron.defaults);

Этап 3: отключение JWT

JWT-валидация удаляется, остаётся только unseal.


Типичные ошибки при переходе

Использование слишком короткого пароля

Iron требует криптографически стойкую длину ключа.

Плохой вариант:

const password = '12345';

Хранение чувствительных данных без необходимости

Iron защищает данные, но не отменяет принцип минимизации:

  • не хранить лишние поля в сессии
  • избегать персональных данных без необходимости

Игнорирование обработки ошибок unseal

try {
  await Iron.unseal(token, password, Iron.defaults);
} catch {
  // обязательная обработка
}

Производительность

Iron использует симметричное шифрование, что делает его:

  • быстрее, чем RSA-подписи JWT в некоторых конфигурациях
  • более стабильным при больших payload
  • предсказуемым по latency

Основная нагрузка приходится на:

  • криптографические операции
  • сериализацию JSON

Когда переход оправдан

Переход с JWT на Iron имеет смысл при:

  • необходимости полной конфиденциальности payload
  • отказе от client-readable токенов
  • работе с чувствительными данными в сессиях
  • отсутствии требования к внешней валидации токена сторонними сервисами

Архитектурный эффект перехода

После замены JWT на Iron модель системы меняется:

  • исчезает публичная структура токена
  • уменьшается поверхность атаки через декодирование
  • упрощается контроль целостности сессии
  • повышается контроль над жизненным циклом данных на сервере