Создание токенов с истёкшим сроком для тестирования

В процессе разработки и тестирования систем аутентификации важно проверять поведение приложения при работе с недействительными токенами. Один из ключевых сценариев — обработка токенов с истёкшим сроком действия. Это позволяет убедиться, что:

  • корректно срабатывают механизмы отказа в доступе
  • система инициирует обновление токена (refresh flow)
  • отсутствуют уязвимости, связанные с повторным использованием устаревших токенов

Библиотека jose в JavaScript предоставляет гибкие инструменты для управления временем жизни токенов и позволяет намеренно создавать уже истёкшие токены.


Основы управления временем жизни JWT

JWT (JSON Web Token) содержит стандартные поля (claims), связанные со временем:

  • exp (expiration time) — время истечения токена
  • iat (issued at) — время выпуска
  • nbf (not before) — токен не действителен до указанного момента

Ключевым для тестирования является именно поле exp.

Пример структуры payload:

{
  "sub": "user123",
  "exp": 1710000000,
  "iat": 1709990000
}

Создание токена с истёкшим сроком

В библиотеке jose используется класс SignJWT. Для создания истёкшего токена необходимо задать время exp в прошлом.

Пример

import { SignJWT } from 'jose'

const secret = new TextEncoder().encode('super-secret-key')

const expiredToken = await new SignJWT({ userId: '123' })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('-10s')
  .sign(secret)

console.log(expiredToken)

Ключевой момент

Метод .setExpirationTime() принимает:

  • строку ('10s', '2h', '1d')
  • число (timestamp в секундах)
  • отрицательное значение (для создания уже истёкшего токена)

В данном случае '−10s' означает, что токен истёк 10 секунд назад.


Альтернативный способ: явное задание timestamp

Иногда требуется полный контроль над временем. В этом случае можно вручную задать exp.

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

const expiredToken = await new SignJWT({ userId: '123' })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt(now - 100)
  .setExpirationTime(now - 50)
  .sign(secret)

Здесь:

  • токен был создан 100 секунд назад
  • срок действия истёк 50 секунд назад

Проверка истёкшего токена

Для верификации используется функция jwtVerify.

import { jwtVerify } from 'jose'

try {
  await jwtVerify(expiredToken, secret)
} catch (err) {
  console.error(err)
}

Ожидаемая ошибка

При проверке истёкшего токена библиотека выбрасывает ошибку:

JWTExpired: "exp" claim timestamp check failed

Управление проверкой времени (clock tolerance)

В реальных системах возможны рассинхронизации времени между сервисами. Для тестирования таких сценариев используется параметр clockTolerance.

await jwtVerify(expiredToken, secret, {
  clockTolerance: 60 // секунд
})

Если токен истёк менее чем на 60 секунд назад, он будет считаться валидным.

Это полезно для:

  • тестирования граничных условий
  • симуляции сетевых задержек
  • проверки устойчивости системы

Игнорирование срока действия

Для некоторых тестов требуется принудительно игнорировать истечение срока.

await jwtVerify(expiredToken, secret, {
  ignoreExp: true
})

Это позволяет:

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

Использование фиктивного времени (clockTimestamp)

Для полного контроля над временем проверки можно задать “виртуальное текущее время”.

await jwtVerify(expiredToken, secret, {
  clockTimestamp: Math.floor(Date.now() / 1000) - 1000
})

Это позволяет:

  • тестировать поведение системы “в прошлом”
  • проверять корректность логики истечения
  • воспроизводить баги, связанные со временем

Практические сценарии тестирования

1. Проверка middleware аутентификации

Создание истёкшего токена и отправка его в API:

fetch('/api/protected', {
  headers: {
    Authorization: `Bearer ${expiredToken}`
  }
})

Ожидаемое поведение:

  • HTTP 401 Unauthorized
  • сообщение об истёкшем токене

2. Тестирование refresh-token механизма

Последовательность:

  1. Отправка истёкшего access token
  2. Получение ошибки
  3. Использование refresh token
  4. Получение нового access token

3. Проверка защиты от replay-атак

Истёкшие токены не должны приниматься даже при повторной отправке:

  • проверяется отсутствие кеширования
  • исключается повторное использование

4. Юнит-тесты

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

test('expired token should throw error', async () => {
  await expect(jwtVerify(expiredToken, secret))
    .rejects
    .toThrow('exp')
})

Частые ошибки при тестировании

Неверный формат времени

.setExpirationTime(Date.now())

Ошибка: используется миллисекунды вместо секунд.

Правильно:

.setExpirationTime(Math.floor(Date.now() / 1000))

Отсутствие iat

Некоторые системы требуют iat для корректной логики:

.setIssuedAt()

Слишком большое смещение времени

Если токен истёк слишком давно, clockTolerance не поможет.


Рекомендации по организации тестов

  • использовать отдельные ключи для тестирования
  • изолировать сценарии с истёкшими токенами
  • комбинировать с тестами на nbf и iat
  • проверять поведение на границах (±1 секунда)
  • автоматизировать генерацию токенов

Генерация набора тестовых токенов

Практика показывает эффективность создания набора токенов с разными состояниями:

const tokens = {
  valid: await new SignJWT({}).setExpirationTime('1h').sign(secret),
  expired: await new SignJWT({}).setExpirationTime('-1h').sign(secret),
  almostExpired: await new SignJWT({}).setExpirationTime('1s').sign(secret),
}

Это упрощает покрытие тестами и повышает читаемость.


Отладка и логирование

При работе с истёкшими токенами важно логировать:

  • текущее серверное время
  • значение exp
  • разницу во времени

Пример:

const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64'))

console.log({
  now: Math.floor(Date.now() / 1000),
  exp: payload.exp,
  diff: payload.exp - Math.floor(Date.now() / 1000)
})

Безопасность при работе с тестовыми токенами

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

Расширенные техники

Массовая генерация истёкших токенов

Для нагрузочного тестирования:

const tokens = await Promise.all(
  Array.from({ length: 1000 }, () =>
    new SignJWT({ user: 'test' })
      .setExpirationTime('-1m')
      .sign(secret)
  )
)

Комбинирование с другими claims

new SignJWT({
  role: 'admin',
  permissions: ['read', 'write']
})
  .setExpirationTime('-30s')

Позволяет тестировать сложные сценарии авторизации.


Использование разных алгоритмов

.setProtectedHeader({ alg: 'RS256' })

Проверка поведения системы при разных способах подписи.


Глубокое понимание механизмов истечения токенов и возможностей библиотеки jose позволяет точно моделировать критические сценарии, выявлять ошибки в логике аутентификации и повышать надёжность системы.