В архитектуре серверных приложений часто возникает необходимость пересмотра способа хранения и передачи защищённых данных между клиентом и сервером. JSON Web Token (JWT) долгое время используется как стандарт де-факто для stateless-аутентификации, однако в ряде сценариев он начинает проявлять ограничения, связанные с безопасностью и управлением жизненным циклом токена.
Основные проблемы JWT в прикладных системах:
В отличие от JWT, подход Iron, реализованный в библиотеке
@hapi/iron, строится не на подписанном, а на
зашифрованном и сериализованном контейнере данных. Это
принципиально меняет модель угроз: содержимое не просто защищено от
подделки, но и полностью скрыто от клиента.
Iron реализует концепцию sealed data — «запечатанных» данных. Внутри используется симметричное шифрование, а не только подпись.
Ключевые свойства:
В основе используется криптографический алгоритм, а не просто HMAC.
JWT состоит из трёх частей:
Payload доступен любому, кто имеет токен.
header.payload.signature
Даже при наличии подписи содержимое не защищено от чтения.
Iron хранит данные в зашифрованном виде:
Библиотека:
npm install @hapi/iron
Импорт:
import Iron from '@hapi/iron';
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'
}
Переход не сводится к простой замене библиотек. Меняется сама модель хранения сессий.
const token = jwt.sign(
{ userId: 42, role: 'admin' },
secret,
{ expiresIn: '15m' }
);
Сервер при каждом запросе:
const session = await Iron.seal(
{ userId: 42, role: 'admin' },
password,
Iron.defaults
);
Сервер:
Пример 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 {}
}
Сервер принимает оба формата:
После аутентификации создаются только Iron-токены:
const token = await Iron.seal(userData, password, Iron.defaults);
JWT-валидация удаляется, остаётся только unseal.
Iron требует криптографически стойкую длину ключа.
Плохой вариант:
const password = '12345';
Iron защищает данные, но не отменяет принцип минимизации:
try {
await Iron.unseal(token, password, Iron.defaults);
} catch {
// обязательная обработка
}
Iron использует симметричное шифрование, что делает его:
Основная нагрузка приходится на:
Переход с JWT на Iron имеет смысл при:
После замены JWT на Iron модель системы меняется: