Короткоживущие access-токены и их ротация

Короткоживущие access-токены используются как основной механизм ограничения времени жизни сессии пользователя при работе с API. Их ключевая характеристика — минимальный срок действия (обычно от 5 до 30 минут), что снижает риск компрометации при утечке токена.

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

Использование короткого времени жизни решает несколько задач:

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

JWT как основа access-токенов и роль Jose

JWT представляет собой компактный токен, состоящий из трёх частей: header, payload и signature. Подпись обеспечивает целостность данных и проверку подлинности.

В JavaScript-экосистеме одной из основных библиотек для работы с JWT является jose. Она реализует современные стандарты JWS, JWE и JWT с поддержкой Web Crypto API и строгой типизацией операций криптографии.

Ключевая особенность библиотеки — отказ от небезопасных абстракций и предоставление низкоуровневого контроля над криптографическими операциями.

Пример создания access-токена:

import { SignJWT } from 'jose'

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

const accessToken = await new SignJWT({ role: 'user' })
  .setProtectedHeader({ alg: 'HS256' })
  .setSubject('user-123')
  .setIssuedAt()
  .setExpirationTime('15m')
  .sign(secret)

Здесь фиксируется ключевой принцип: время жизни токена задаётся явно и коротко.

Проблема долгоживущих токенов и необходимость ротации

Долгоживущие access-токены создают критическую уязвимость: при компрометации злоумышленник получает доступ на длительный период без возможности быстрого отзыва.

Решение — разделение токенов на два уровня:

  • Access token — короткий срок жизни, используется для доступа к API
  • Refresh token — долгоживущий, используется только для получения нового access token

Однако наличие refresh-токена вводит новую проблему: необходимость его защиты и регулярной ротации.

Ротация refresh-токенов

Ротация refresh-токенов заключается в том, что при каждом обновлении access-токена выдается новый refresh-токен, а старый немедленно инвалидируется.

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

  • предотвратить повторное использование украденного refresh-токена
  • отслеживать подозрительные цепочки обновлений
  • реализовать контроль сессий на сервере

Типичный поток выглядит следующим образом:

  1. Пользователь получает access + refresh токены
  2. Access токен истекает
  3. Клиент отправляет refresh токен
  4. Сервер проверяет refresh токен
  5. Выдаётся новый access и новый refresh токен
  6. Старый refresh токен становится недействительным

Реализация ротации с использованием Jose

Jose используется в основном для создания и проверки JWT, включая refresh-токены.

Генерация refresh-токена

import { SignJWT } from 'jose'

const refreshSecret = new TextEncoder().encode('refresh-secret')

const refreshToken = await new SignJWT({ type: 'refresh' })
  .setProtectedHeader({ alg: 'HS256' })
  .setSubject('user-123')
  .setIssuedAt()
  .setExpirationTime('7d')
  .sign(refreshSecret)

Refresh-токен имеет значительно больший срок жизни, но его использование строго ограничено серверной логикой.

Проверка refresh-токена

import { jwtVerify } from 'jose'

const { payload } = await jwtVerify(
  token,
  refreshSecret
)

if (payload.type !== 'refresh') {
  throw new Error('Invalid token type')
}

Связка access и refresh токенов

На уровне архитектуры важно связывать оба токена с одной сессией. Обычно для этого используется:

  • sessionId в payload
  • хранение refresh-токенов в базе данных
  • привязка к устройству или fingerprint

Пример payload refresh-токена:

{
  "sub": "user-123",
  "type": "refresh",
  "sid": "session-456"
}

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

При ротации критично предотвращать replay-атаки. Для этого применяется хранение состояния refresh-токенов:

  • список активных refresh-токенов
  • отметка “использован”
  • немедленная инвалидизация при повторной отправке

Пример логики:

const stored = await db.refreshTokens.find(tokenId)

if (!stored || stored.used) {
  throw new Error('Token reuse detected')
}

await db.refreshTokens.update(tokenId, { used: true })

Access-токены без состояния и их ограничения

Access-токены обычно не хранятся на сервере, что делает систему stateless. Однако это создаёт ограничение: невозможность мгновенной инвалидизации.

Компенсация достигается комбинацией:

  • короткого времени жизни
  • ротации refresh-токенов
  • blacklist механизма (опционально)

Использование JWS и JWE в контексте токенов

Jose поддерживает два ключевых стандарта:

  • JWS (JSON Web Signature) — подпись токенов
  • JWE (JSON Web Encryption) — шифрование содержимого

Для access-токенов обычно достаточно JWS. JWE применяется в случаях, когда payload содержит чувствительные данные.

Пример JWE:

import { EncryptJWT } from 'jose'

const encrypted = await new EncryptJWT({ role: 'admin' })
  .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
  .setIssuedAt()
  .setExpirationTime('10m')
  .encrypt(secretKey)

Практика разделения ответственности токенов

Архитектурно разделение выглядит следующим образом:

  • access token:

    • краткосрочный доступ
    • отсутствие хранения на сервере
    • минимальный payload
  • refresh token:

    • долгосрочная идентификация сессии
    • хранение на сервере
    • контроль ротации

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

Ошибки при реализации ротации токенов

На практике часто встречаются критические ошибки:

  • отсутствие инвалидизации старого refresh-токена
  • слишком длинный срок жизни access-токена
  • хранение refresh-токена в localStorage без защиты
  • отсутствие привязки токена к сессии
  • игнорирование повторного использования токена

Каждая из этих ошибок снижает эффективность всей модели безопасности.

Модель угроз и влияние коротких токенов

Использование короткоживущих access-токенов изменяет модель угроз. Даже при успешной атаке:

  • время действия токена ограничено
  • отсутствует доступ к refresh-токену
  • невозможно длительное поддержание сессии

Это делает систему устойчивой к перехвату токенов в сетевом трафике или XSS-атакам с ограниченным временем воздействия.