Схема аутентификации на основе JWT

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

Структура JWT состоит из трёх частей:

  • Header — содержит информацию о типе токена и алгоритме подписи
  • Payload — содержит набор утверждений (claims), таких как идентификатор пользователя, роль, срок действия
  • Signature — криптографическая подпись, подтверждающая целостность токена

Формат представления:

header.payload.signature

Каждая часть кодируется в Base64URL, что обеспечивает компактность и удобство передачи через HTTP-заголовки.


Библиотека Jose и её роль в работе с JWT

Библиотека jose представляет собой современную реализацию стандартов JOSE (JSON Object Signing and Encryption). Она поддерживает:

  • JWS (подписанные токены)
  • JWE (зашифрованные токены)
  • JWK (JSON Web Keys)
  • JWT поверх JWS

Ключевая особенность — строгая поддержка спецификаций RFC и работа через асинхронные API, что делает её подходящей для современных Node.js-приложений.

Установка:

npm install jose

Модель аутентификации с JWT

JWT-аутентификация строится на принципе stateless-сервера: сервер не хранит сессии, вся необходимая информация находится в токене.

Основные этапы:

  1. Пользователь отправляет логин и пароль
  2. Сервер проверяет учетные данные
  3. При успехе создаётся JWT
  4. Токен возвращается клиенту
  5. Клиент хранит токен и отправляет его в каждом запросе
  6. Сервер проверяет подпись и извлекает данные

Создание JWT с использованием jose

Для подписи токена используется класс SignJWT.

Пример генерации токена (HS256)

import { SignJWT } from 'jose';

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

const token = await new SignJWT({ role: 'user', id: 123 })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('2h')
  .sign(secret);

Основные элементы:

  • setProtectedHeader — определяет алгоритм подписи
  • setIssuedAt — время создания токена
  • setExpirationTime — срок действия
  • sign — финальная подпись

Payload не должен содержать чувствительные данные (например, пароли), так как он лишь кодируется, но не шифруется.


Проверка и декодирование токена

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

import { jwtVerify } from 'jose';

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

const { payload, protectedHeader } = await jwtVerify(token, secret);

Результат включает:

  • payload — данные токена
  • protectedHeader — информация о алгоритме

При несоответствии подписи или истечении срока действия будет выброшена ошибка.


Асимметричная криптография (RS256)

В более безопасных системах применяется пара ключей: приватный и публичный.

Генерация токена с RSA

import { SignJWT } from 'jose';
import { readFileSync } from 'fs';

const privateKey = readFileSync('./private.pem', 'utf8');

const token = await new SignJWT({ id: 1, role: 'admin' })
  .setProtectedHeader({ alg: 'RS256' })
  .setIssuedAt()
  .setExpirationTime('1h')
  .sign(privateKey);

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

import { jwtVerify } from 'jose';
import { readFileSync } from 'fs';

const publicKey = readFileSync('./public.pem', 'utf8');

const { payload } = await jwtVerify(token, publicKey);

Преимущество RS256 заключается в том, что публичный ключ может быть безопасно распространён среди множества сервисов, в то время как приватный остаётся только у сервера авторизации.


Middleware для проверки JWT в API

Типичная интеграция в Node.js (например, Express):

import { jwtVerify } from 'jose';

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

export async function authMiddleware(req, res, next) {
  try {
    const header = req.headers.authorization;

    if (!header) {
      return res.status(401).send('Token required');
    }

    const token = header.split(' ')[1];

    const { payload } = await jwtVerify(token, secret);

    req.user = payload;

    next();
  } catch (err) {
    res.status(401).send('Invalid token');
  }
}

После успешной проверки данные пользователя становятся доступны в req.user.


Claims: структура данных внутри JWT

Payload токена содержит claims, которые делятся на категории:

Registered claims

Стандартизированные поля:

  • iss — издатель токена
  • sub — субъект (обычно ID пользователя)
  • aud — аудитория
  • exp — время истечения
  • iat — время выпуска

Public claims

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

  • role
  • permissions

Private claims

Специфичные для приложения данные, не стандартизированные.


Срок жизни токена и refresh-механизм

JWT обычно имеет ограниченный срок действия. Это снижает риск компрометации.

Распространённая схема:

  • Access token — короткий срок (15 минут – 2 часа)
  • Refresh token — длительный срок (дни или недели)

Refresh token используется для получения нового access token без повторного логина.


Ошибки и обработка исключений в jose

При работе с jwtVerify возможны типовые ошибки:

  • JWTExpired — токен истёк
  • JWSSignatureVerificationFailed — неверная подпись
  • JWTInvalid — некорректный формат

Пример обработки:

import { jwtVerify } from 'jose';

try {
  const { payload } = await jwtVerify(token, secret);
} catch (err) {
  if (err.code === 'ERR_JWT_EXPIRED') {
    // обработка истёкшего токена
  }
}

Безопасность при работе с JWT

Ключевые принципы:

  • Использование HTTPS обязательно
  • Короткое время жизни access token
  • Хранение refresh token в HttpOnly cookies
  • Использование RS256 вместо HS256 в распределённых системах
  • Отказ от хранения чувствительных данных в payload
  • Ротация ключей подписи

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


Шифрование JWT (JWE)

Помимо подписи, jose поддерживает шифрование токенов.

JWE используется, когда требуется скрыть содержимое payload.

Основные этапы:

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

Это добавляет дополнительный уровень защиты поверх подписи.


Интеграция JWT в распределённые системы

В микросервисной архитектуре JWT применяется как единый механизм идентификации между сервисами.

Типовая схема:

  • Auth Service выпускает токен
  • API Gateway проверяет токен
  • Микросервисы доверяют payload без повторной аутентификации

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


Хранение ключей и ротация

Практика безопасного управления ключами включает:

  • хранение приватных ключей в secure vault (например, HashiCorp Vault)
  • периодическую ротацию ключей
  • поддержку нескольких активных ключей одновременно
  • использование kid (Key ID) в header токена для выбора нужного ключа

Пример header:

{
  "alg": "RS256",
  "kid": "key-2026-01"
}

Типовые ошибки проектирования JWT-систем

  • использование слишком долгоживущих access token
  • хранение JWT в localStorage без защиты от XSS
  • отсутствие механизма отзыва токенов
  • включение чувствительных данных в payload
  • использование симметричного ключа в распределённой архитектуре

Эти ошибки приводят к снижению уровня безопасности и усложняют масштабирование системы.