Временные одноразовые токены

Временные одноразовые токены (One-Time Tokens, OTT) используются для повышения безопасности взаимодействия между клиентом и сервером. Они позволяют:

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

В контексте библиотеки Iron такие токены строятся на основе криптографически защищённых контейнеров (sealing), которые включают полезную нагрузку и метаданные, защищённые шифрованием и проверкой целостности.


Принцип работы в Iron

Iron реализует механизм seal/unseal, где:

  • seal — сериализует и шифрует объект
  • unseal — расшифровывает и валидирует данные

Каждый токен содержит:

  • полезную нагрузку (payload)
  • метку времени создания
  • параметры безопасности (алгоритмы, соль, ключи)

Одноразовость достигается за счёт:

  1. короткого времени жизни (TTL)
  2. хранения факта использования (например, в Redis или памяти)
  3. включения уникального идентификатора (nonce)

Базовая генерация токена

import Iron from '@hapi/iron';

const password = 'super-secret-key';

async function generateToken(data) {
  const sealed = await Iron.seal(data, password, Iron.defaults);
  return sealed;
}

Особенности:

  • data — любой сериализуемый объект
  • password — криптографический ключ
  • Iron.defaults — параметры безопасности (можно кастомизировать)

Расшифровка и проверка

async function verifyToken(token) {
  try {
    const unsealed = await Iron.unseal(token, password, Iron.defaults);
    return unsealed;
  } catch (err) {
    return null;
  }
}

Если токен:

  • повреждён
  • просрочен
  • подписан другим ключом

→ будет выброшено исключение


Ограничение времени жизни (TTL)

Iron поддерживает встроенный механизм TTL:

const options = {
  ...Iron.defaults,
  ttl: 60 * 1000 // 1 минута
};

const token = await Iron.seal({ userId: 123 }, password, options);

При попытке расшифровки:

await Iron.unseal(token, password, options);

если время истекло — токен считается недействительным.


Реализация одноразовости

Iron сам по себе не отслеживает, использовался ли токен ранее. Для полной одноразовости требуется дополнительная логика.

Подход 1: Хранение nonce

import { v4 as uuidv4 } from 'uuid';

const usedTokens = new Set();

async function createOneTimeToken(data) {
  const nonce = uuidv4();

  const token = await Iron.seal(
    { ...data, nonce },
    password,
    Iron.defaults
  );

  return token;
}

Проверка:

async function consumeToken(token) {
  const data = await Iron.unseal(token, password, Iron.defaults);

  if (usedTokens.has(data.nonce)) {
    throw new Error('Token already used');
  }

  usedTokens.add(data.nonce);

  return data;
}

Подход 2: Использование Redis

Для распределённых систем:

import Redis from 'ioredis';

const redis = new Redis();

async function consumeToken(token) {
  const data = await Iron.unseal(token, password, Iron.defaults);

  const exists = await redis.get(data.nonce);

  if (exists) {
    throw new Error('Token already used');
  }

  await redis.set(data.nonce, '1', 'EX', 60);

  return data;
}

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

  • устойчивость к перезапуску сервера
  • масштабируемость
  • централизованное хранение

Структура payload

Рекомендуется включать:

{
  userId: 123,
  action: 'password_reset',
  nonce: 'uuid',
  issuedAt: Date.now()
}

Ключевые элементы:

  • userId — идентификатор пользователя
  • action — тип операции
  • nonce — уникальность
  • issuedAt — дополнительная проверка времени

Защита от повторного использования

Даже при коротком TTL возможна атака повтором. Поэтому:

  • всегда использовать nonce
  • хранить использованные токены
  • ограничивать TTL (30–300 секунд)

Настройка параметров безопасности

Iron позволяет гибко управлять алгоритмами:

const options = {
  encryption: {
    algorithm: 'aes-256-cbc',
    saltBits: 256,
    iterations: 10000
  },
  integrity: {
    algorithm: 'sha256',
    saltBits: 256,
    iterations: 10000
  },
  ttl: 60000
};

Важно:

  • увеличение iterations повышает безопасность
  • увеличивает нагрузку на CPU
  • баланс подбирается исходя из требований системы

Пример: токен сброса пароля

Генерация:

async function createResetToken(userId) {
  return await Iron.seal(
    {
      userId,
      action: 'reset_password',
      nonce: uuidv4()
    },
    password,
    { ...Iron.defaults, ttl: 15 * 60 * 1000 }
  );
}

Проверка:

async function validateResetToken(token) {
  const data = await Iron.unseal(token, password, Iron.defaults);

  if (data.action !== 'reset_password') {
    throw new Error('Invalid token type');
  }

  // Проверка nonce в хранилище

  return data.userId;
}

Токен отправляется пользователю:

const token = await Iron.seal(
  {
    userId: 123,
    action: 'login',
    nonce: uuidv4()
  },
  password,
  { ...Iron.defaults, ttl: 5 * 60 * 1000 }
);

Ссылка:

https://example.com/login?token=...

После перехода:

  • токен проверяется
  • nonce помечается использованным
  • создаётся сессия

Ошибки и уязвимости

1. Отсутствие nonce → токен можно использовать повторно

2. Слишком большой TTL → увеличивает окно атаки

3. Использование слабого пароля → риск расшифровки токенов

4. Отсутствие проверки действия (action) → токен может использоваться не по назначению

5. Отсутствие централизованного хранилища → одноразовость нарушается в кластерной среде


Рекомендации по использованию

  • TTL: 1–15 минут
  • всегда включать nonce
  • хранить использованные токены
  • разделять токены по типам (action)
  • использовать разные ключи для разных типов токенов
  • регулярно ротировать секреты

Масштабирование

При работе в нескольких инстансах:

  • использовать Redis или аналог
  • синхронизировать ключи шифрования
  • учитывать задержки репликации

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

Операции Iron включают:

  • PBKDF2 (вывод ключа)
  • симметричное шифрование
  • HMAC

Это делает их:

  • безопасными
  • но относительно затратными

Оптимизации:

  • кэширование результатов при необходимости
  • снижение iterations при высокой нагрузке
  • использование пулов обработки

Расширенные сценарии

1. Подписание API-запросов

Каждый запрос содержит одноразовый токен:

  • защита от replay-атак
  • контроль времени

2. Подтверждение операций

Например:

  • удаление аккаунта
  • изменение email

3. Межсервисная аутентификация

  • токены с коротким TTL
  • проверка подлинности сервиса

Сравнение с JWT

Характеристика Iron JWT
Шифрование Да Обычно нет
Подпись Да Да
Одноразовость Нужно реализовать Нужно реализовать
Простота Средняя Высокая
Безопасность payload Высокая (зашифрован) Низкая (base64)

Iron предпочтителен, когда:

  • требуется скрыть данные
  • важна конфиденциальность payload

Архитектурный паттерн

  1. Генерация токена (с nonce)
  2. Отправка клиенту
  3. Получение токена сервером
  4. Расшифровка
  5. Проверка TTL
  6. Проверка nonce (одноразовость)
  7. Выполнение действия
  8. Инвалидация nonce

Тестирование

Проверяются:

  • истечение TTL
  • повторное использование
  • изменение токена (tampering)
  • неверный ключ

Пример:

test('token expires', async () => {
  const token = await generateToken({ a: 1 });

  await new Promise(r => setTimeout(r, 2000));

  await expect(verifyToken(token)).rejects.toThrow();
});

Безопасное хранение ключей

  • использовать переменные окружения
  • избегать хранения в коде
  • применять системы управления секретами (Vault, KMS)

Итоговая модель безопасности

Временные одноразовые токены в Iron обеспечивают:

  • конфиденциальность (шифрование)
  • целостность (подпись)
  • ограниченность по времени (TTL)
  • защиту от повторного использования (через nonce)

При правильной реализации становятся мощным инструментом для безопасных одноразовых операций в веб-приложениях.