Приватные и публичные claims

В спецификации JWT (JSON Web Token, RFC 7519), на которой основана работа библиотеки Jose в JavaScript, понятие claims является центральным элементом структуры токена. Claims представляют собой набор утверждений (свойств), содержащихся в полезной нагрузке (payload) JWT и описывающих субъект, контекст и дополнительные данные токена.

JWT-токен логически состоит из трёх частей:

  • Header (заголовок)
  • Payload (полезная нагрузка)
  • Signature (подпись)

Именно в payload находятся claims — пары ключ-значение, формирующие семантику токена.

Библиотека Jose работает строго в рамках этой модели и предоставляет инструменты для создания, подписи, шифрования и проверки JWT, JWS и JWE, где claims являются основным объектом обработки на уровне payload.

Claims в JWT делятся на три категории:

  • зарегистрированные (registered claims)
  • публичные (public claims)
  • приватные (private claims)

Эта классификация не является особенностью Jose, но напрямую влияет на то, как данные формируются, валидируются и интерпретируются при использовании функций библиотеки.


Зарегистрированные claims как основа структуры

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

К ним относятся:

  • iss (issuer) — издатель токена
  • sub (subject) — субъект токена
  • aud (audience) — получатель
  • exp (expiration time) — срок действия
  • nbf (not before) — время начала действия
  • iat (issued at) — время выпуска
  • jti (JWT ID) — уникальный идентификатор

В Jose эти поля не обрабатываются автоматически как обязательные, но могут проверяться через параметры в функциях jwtVerify, jwtDecrypt, jwtSign и аналогичных.


Публичные claims

Публичные claims (public claims) предназначены для использования в межсистемных сценариях, где важно избежать конфликтов имен и обеспечить интероперабельность.

Основные характеристики публичных claims:

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

Типичный подход — использование URI в качестве ключа:

{
  "https://example.com/role": "admin",
  "https://example.com/permissions": ["read", "write"]
}

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

Работа с публичными claims в Jose

Jose не накладывает ограничений на структуру payload. Любые дополнительные поля считаются частью claims и включаются в подписываемый или шифруемый объект.

Пример создания JWT с публичными claims:

import { SignJWT } from 'jose'

const token = await new SignJWT({
  'https://example.com/role': 'admin',
  'https://example.com/scopes': ['users:read', 'users:write']
})
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('2h')
  .sign(secretKey)

При верификации Jose возвращает payload без изменения структуры:

import { jwtVerify } from 'jose'

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

console.log(payload['https://example.com/role'])

Публичные claims в таком контексте становятся частью проверяемой подписи и не отличаются по механике от других полей payload.


Приватные claims

Приватные claims (private claims) предназначены для внутреннего использования между согласованными сторонами. Они не стандартизированы и не должны пересекаться с зарегистрированными или публичными именами.

Характерные особенности приватных claims:

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

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

{
  "role": "admin",
  "userId": 42,
  "plan": "premium",
  "internalFlags": {
    "beta": true
  }
}

Приватные claims в контексте Jose

Jose рассматривает приватные claims как часть payload без какой-либо семантической интерпретации. Библиотека не различает, является ли ключ публичным или приватным — ответственность за структуру лежит на разработчике.

Пример подписания токена с приватными claims:

const token = await new SignJWT({
  userId: 42,
  role: 'admin',
  plan: 'premium',
  internalFlags: { beta: true }
})
  .setProtectedHeader({ alg: 'RS256' })
  .setSubject('42')
  .setIssuedAt()
  .sign(privateKey)

При проверке:

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

if (payload.role === 'admin') {
  // доступ к административным функциям
}

Разграничение публичных и приватных claims на уровне архитектуры

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

Ключевые различия:

Тип claims Область применения Риск конфликта Формат
Публичные Межсистемный обмен Средний URI или стандартизированные ключи
Приватные Внутренние системы Высокий при интеграциях Любые строки

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


Влияние claims на подпись и шифрование

Все claims, независимо от типа, входят в вычисление подписи JWS или в шифруемый блок JWE.

При JWS (подпись):

  • весь payload участвует в формировании подписи
  • изменение любого claim делает подпись недействительной

При JWE (шифрование):

  • claims полностью шифруются
  • структура payload скрыта до расшифрования

Пример JWE с приватными claims:

import { EncryptJWT } from 'jose'

const token = await new EncryptJWT({
  role: 'admin',
  permissions: ['write', 'delete']
})
  .setProtectedHeader({ alg: 'RSA-OAEP', enc: 'A256GCM' })
  .setExpirationTime('1h')
  .encrypt(publicKey)

После расшифровки:

import { jwtDecrypt } from 'jose'

const { payload } = await jwtDecrypt(token, privateKey)

console.log(payload.permissions)

Конфликты имен и стратегия проектирования claims

При использовании Jose в реальных системах основная проблема возникает не на уровне криптографии, а на уровне организации структуры claims.

Типичные ошибки:

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

Рекомендуемые подходы:

  • использование URI для публичных claims
  • выделение префиксов для приватных данных (app_, internal_)
  • минимизация количества top-level полей
  • отделение технических claims (exp, iat) от бизнес-данных

Пример структурированного payload:

{
  "sub": "42",
  "exp": 1710000000,
  "https://api.example.com/roles": ["admin"],
  "app_userProfile": {
    "theme": "dark"
  },
  "app_permissions": ["read", "write"]
}

Обработка claims при валидации в Jose

При вызове jwtVerify возможна дополнительная проверка claims через параметры:

await jwtVerify(token, key, {
  issuer: 'https://auth.example.com',
  audience: 'api.example.com'
})

В этом случае registered claims участвуют в автоматической проверке, а публичные и приватные claims проверяются вручную после получения payload.


Семантическая нейтральность библиотеки Jose

Jose не вводит различий между типами claims на уровне API. Это принципиальная особенность архитектуры:

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

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