Создание обёртки для проверки токена

В системах, где используется сериализация и защита данных на уровне токенов, библиотека Iron применяется для «запечатывания» (seal) и «распечатывания» (unseal) структурированных данных с криптографической защитой. В контексте авторизации токен обычно представляет собой сериализованный объект с метаданными пользователя, временем жизни и контрольной подписью.

Обёртка проверки токена строится как слой абстракции над iron.unseal, инкапсулирующий:

  • валидацию структуры токена
  • обработку ошибок расшифровки
  • проверку срока жизни (TTL)
  • унификацию результата (payload или ошибка)
  • защиту от некорректных входных данных

Такой слой позволяет изолировать криптографическую логику от бизнес-логики приложения.


Базовый механизм работы Iron

В основе работы Iron лежит симметричное шифрование с дополнительной защитой целостности данных.

Основные операции:

  • iron.seal(data, password, options) — преобразует объект в защищённую строку
  • iron.unseal(sealed, password, options) — восстанавливает исходный объект

Ключевые параметры:

  • password — секретный ключ
  • ttl — время жизни токена
  • integrity — алгоритм проверки целостности
  • encryption — алгоритм шифрования

Пример структуры токена:

{
  userId: 42,
  role: "admin",
  iat: 1700000000
}

После применения seal получается строка, содержащая зашифрованный payload.


Проектирование обёртки проверки токена

Обёртка должна обеспечивать единый контракт проверки токена:

  • вход: строка токена
  • выход: объект payload или ошибка
  • отсутствие проброса низкоуровневых исключений наружу

Базовая структура модуля:

import Iron from '@hapi/iron';

class TokenService {
  constructor({ password, ttl }) {
    this.password = password;
    this.ttl = ttl;
  }

  async verify(token) {
    return null;
  }
}

export default TokenService;

Валидация входных данных

Первый слой защиты — проверка формата токена до попытки расшифровки.

validateTokenFormat(token) {
  if (typeof token !== 'string') {
    throw new Error('INVALID_TOKEN_TYPE');
  }

  if (token.length < 10) {
    throw new Error('TOKEN_TOO_SHORT');
  }
}

Такая проверка снижает нагрузку на криптографический слой и отсекает очевидно некорректные значения.


Реализация безопасной проверки через unseal

Основная логика обёртки строится вокруг Iron.unseal, который может выбрасывать исключения при:

  • неверном ключе
  • повреждённой строке
  • истёкшем TTL
  • несоответствии алгоритма

Инкапсуляция этих ошибок:

async verify(token) {
  this.validateTokenFormat(token);

  try {
    const result = await Iron.unseal(
      token,
      this.password,
      Iron.defaults
    );

    return {
      valid: true,
      payload: result
    };
  } catch (err) {
    return {
      valid: false,
      error: this.normalizeError(err)
    };
  }
}

Нормализация ошибок

Криптографические библиотеки часто возвращают низкоуровневые сообщения, которые нельзя напрямую отдавать наружу.

Создаётся слой нормализации:

normalizeError(err) {
  if (err.message.includes('Bad hmac')) {
    return 'INVALID_SIGNATURE';
  }

  if (err.message.includes('Expired')) {
    return 'TOKEN_EXPIRED';
  }

  if (err.message.includes('Malformed')) {
    return 'MALFORMED_TOKEN';
  }

  return 'UNKNOWN_TOKEN_ERROR';
}

Это позволяет стандартизировать поведение системы независимо от внутренней реализации Iron.


Добавление проверки срока жизни

Хотя Iron поддерживает TTL внутри unseal, часто требуется дополнительный контроль на уровне приложения.

checkPayloadValidity(payload) {
  if (!payload || typeof payload !== 'object') {
    throw new Error('INVALID_PAYLOAD');
  }

  if (!payload.iat) {
    throw new Error('MISSING_IAT');
  }

  const now = Math.floor(Date.now() / 1000);

  if (now - payload.iat > this.ttl) {
    throw new Error('TOKEN_EXPIRED');
  }

  return payload;
}

Эта проверка полезна, если токен мог быть создан вне стандартного механизма или требуется дополнительная бизнес-логика.


Полная версия обёртки

Объединённая реализация слоя проверки:

import Iron from '@hapi/iron';

class TokenService {
  constructor({ password, ttl }) {
    this.password = password;
    this.ttl = ttl;
  }

  validateTokenFormat(token) {
    if (typeof token !== 'string') {
      throw new Error('INVALID_TOKEN_TYPE');
    }

    if (token.length < 10) {
      throw new Error('TOKEN_TOO_SHORT');
    }
  }

  normalizeError(err) {
    if (err.message.includes('Bad hmac')) {
      return 'INVALID_SIGNATURE';
    }
    if (err.message.includes('Expired')) {
      return 'TOKEN_EXPIRED';
    }
    if (err.message.includes('Malformed')) {
      return 'MALFORMED_TOKEN';
    }
    return 'UNKNOWN_TOKEN_ERROR';
  }

  checkPayloadValidity(payload) {
    if (!payload || typeof payload !== 'object') {
      throw new Error('INVALID_PAYLOAD');
    }

    if (!payload.iat) {
      throw new Error('MISSING_IAT');
    }

    const now = Math.floor(Date.now() / 1000);

    if (now - payload.iat > this.ttl) {
      throw new Error('TOKEN_EXPIRED');
    }

    return payload;
  }

  async verify(token) {
    try {
      this.validateTokenFormat(token);

      const payload = await Iron.unseal(
        token,
        this.password,
        Iron.defaults
      );

      const validated = this.checkPayloadValidity(payload);

      return {
        valid: true,
        payload: validated
      };
    } catch (err) {
      return {
        valid: false,
        error: this.normalizeError(err)
      };
    }
  }
}

export default TokenService;

Интеграция обёртки в слой авторизации

Обёртка обычно используется в middleware или сервисе авторизации.

Пример использования:

const tokenService = new TokenService({
  password: process.env.IRON_PASSWORD,
  ttl: 3600
});

const result = await tokenService.verify(req.headers.authorization);

if (!result.valid) {
  return res.status(401).json({ error: result.error });
}

req.user = result.payload;

Расширение обёртки для ролей и прав доступа

После базовой проверки токена логично расширить слой авторизации:

hasRole(payload, role) {
  return payload.role === role;
}

И использование:

if (!tokenService.hasRole(result.payload, 'admin')) {
  return res.status(403).json({ error: 'FORBIDDEN' });
}

Устойчивость к изменениям схемы токена

При развитии системы структура payload может изменяться. Обёртка позволяет централизованно адаптировать изменения:

  • добавление новых полей (scope, permissions)
  • миграция версий токенов
  • изменение алгоритма проверки TTL

Пример поддержки версий:

if (payload.ver !== 2) {
  throw new Error('UNSUPPORTED_TOKEN_VERSION');
}

Централизация криптографической логики

Использование обёртки вокруг Iron позволяет добиться ключевого свойства архитектуры:

  • бизнес-логика не зависит от деталей шифрования
  • замена алгоритма возможна без изменения контроллеров
  • обработка ошибок унифицирована
  • проверка токенов становится предсказуемой и тестируемой