Верификация JWT: jwtVerify

В библиотеке jose основным способом проверки подписи JWT является функция jwtVerify, которая выполняет криптографическую проверку токена, валидацию стандартных полей и обеспечивает безопасное извлечение полезной нагрузки.

jwtVerify работает в связке с алгоритмами JWS (JSON Web Signature) и требует корректно предоставленного ключа в формате JWK, PEM или через JWKS-резолвер. В отличие от простого декодирования токена, здесь выполняется полная проверка подлинности подписи, что делает этот метод ключевым элементом безопасности при работе с JWT.


При вызове jwtVerify происходит последовательная обработка:

  • декодирование заголовка JWT
  • выбор алгоритма проверки подписи
  • извлечение ключа верификации
  • проверка криптографической подписи
  • проверка стандартных claims (exp, nbf, iss, aud)
  • возврат payload при успешной валидации

Функция возвращает объект с двумя основными частями:

  • payload — полезная нагрузка токена
  • protectedHeader — заголовок JWT

Минимальный пример использования

import { jwtVerify } from 'jose'

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

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

В данном примере используется симметричный ключ. Алгоритм (например, HS256) определяется автоматически из заголовка JWT, но может быть ограничен явно.


Ограничение допустимых алгоритмов

Для повышения безопасности рекомендуется фиксировать список разрешённых алгоритмов:

const { payload } = await jwtVerify(token, secret, {
  algorithms: ['HS256']
})

Это предотвращает атаки, связанные с подменой алгоритма в заголовке токена.


Проверка issuer, audience и subject

JWT часто используется в распределённых системах, где важно строго контролировать источник и назначение токена.

const { payload } = await jwtVerify(token, secret, {
  issuer: 'https://auth.example.com',
  audience: 'api.example.com',
  subject: 'user-123'
})

Параметры проверяются строго:

  • issuer — кто выпустил токен
  • audience — для кого предназначен токен
  • subject — субъект токена

Несовпадение любого значения приводит к выбросу ошибки.


Работа с временем: exp, nbf и clockTolerance

JWT содержит временные ограничения:

  • exp — срок истечения
  • nbf — не действителен до указанного времени
  • iat — время выпуска

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

const { payload } = await jwtVerify(token, secret, {
  clockTolerance: 5 // 5 секунд
})

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


Использование асимметричных ключей (RS256, ES256)

В случае асимметричной криптографии используется публичный ключ:

import { jwtVerify, importSPKI } from 'jose'

const publicKey = await importSPKI(
  `-----BEGIN PUBLIC KEY-----
  ...
  -----END PUBLIC KEY-----`,
  'RS256'
)

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

Преимущество такого подхода:

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

Работа с JWKS (JSON Web Key Set)

В распределённых системах ключи часто публикуются через JWKS endpoint. В jose предусмотрен механизм автоматического получения ключей.

import { jwtVerify, createRemoteJWKSet } from 'jose'

const JWKS = createRemoteJWKSet(
  new URL('https://auth.example.com/.well-known/jwks.json')
)

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

При каждом запросе библиотека:

  • извлекает kid из заголовка JWT
  • находит соответствующий ключ в JWKS
  • кэширует ключи для повышения производительности

Обработка ошибок верификации

jwtVerify выбрасывает исключения при любой проблеме с токеном.

Основные типы ошибок:

  • JWTExpired — токен просрочен
  • JWSSignatureVerificationFailed — неверная подпись
  • JWTInvalid — некорректный формат
  • JWTClaimValidationFailed — ошибка в claims

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

try {
  const { payload } = await jwtVerify(token, secret)
} catch (err) {
  console.error(err.code)
}

Структурированная обработка ошибок позволяет точно определять причину отказа.


Проверка нестандартных claims

Помимо стандартных полей можно валидировать пользовательские claims через параметр requiredClaims и ручную проверку:

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

if (!payload.role || payload.role !== 'admin') {
  throw new Error('Insufficient role')
}

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


Контроль строгой типизации заголовка

Иногда требуется проверка типа токена:

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

if (protectedHeader.typ !== 'JWT') {
  throw new Error('Invalid token type')
}

Это защищает от подмены формата токена в мультипротокольных системах.


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

В серверных приложениях jwtVerify часто применяется в промежуточных слоях:

export async function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '')

  if (!token) {
    res.status(401).end()
    return
  }

  try {
    const { payload } = await jwtVerify(token, JWKS)
    req.user = payload
    next()
  } catch {
    res.status(401).end()
  }
}

Такой подход обеспечивает централизованную проверку доступа.


Особенности производительности

При интенсивной нагрузке критично учитывать:

  • кэширование JWKS ключей
  • повторное использование TextEncoder
  • ограничение допустимых алгоритмов
  • минимизацию операций в middleware

jwtVerify оптимизирован для серверных сценариев, но зависимость от криптографических операций остаётся основным фактором затрат.


Безопасные практики использования

При интеграции проверки JWT необходимо учитывать:

  • всегда фиксировать algorithms
  • избегать доверия к неограниченным JWKS без HTTPS
  • проверять iss и aud в любом публичном API
  • не использовать payload как источник доверенных данных без проверки подписи
  • не хранить чувствительные данные в JWT payload

JWT предназначен для передачи утверждений, а не секретов.