Роли и разрешения в claims

Claims в JWT представляют собой структурированный способ передачи информации о пользователе и контексте его авторизации. В контексте библиотеки jose (JavaScript Object Signing and Encryption) работа с claims становится центральным элементом построения систем аутентификации и авторизации, особенно когда требуется разграничение ролей и прав доступа.

JWT (JSON Web Token) состоит из трёх частей: заголовка, полезной нагрузки (payload) и подписи. Именно payload содержит claims — набор утверждений, описывающих субъект (sub), время жизни токена (exp), а также пользовательские данные.

В стандартной практике выделяют два типа claims:

  • Registered claims — стандартные поля (iss, sub, aud, exp, iat)
  • Custom claims — пользовательские данные, включая роли и разрешения

Для систем авторизации ключевое значение имеют именно custom claims, так как они позволяют внедрить модель RBAC (Role-Based Access Control) или более гибкую ABAC (Attribute-Based Access Control).

Роли как часть claims

Роль пользователя обычно представляется в виде строки или массива строк внутри JWT:

{
  "sub": "user_123",
  "roles": ["admin", "editor"]
}

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

import { SignJWT } from 'jose';

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

const token = await new SignJWT({
  roles: ['admin', 'editor']
})
  .setProtectedHeader({ alg: 'HS256' })
  .setSubject('user_123')
  .setIssuedAt()
  .setExpirationTime('2h')
  .sign(secret);

Здесь роль встроена непосредственно в payload. Такой подход позволяет серверу без обращения к базе данных принимать решения о доступе.

Проверка токена и извлечение ролей

После получения токена на стороне сервера необходимо его верифицировать и извлечь claims:

import { jwtVerify } from 'jose';

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

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

console.log(payload.roles);

На этом этапе система получает массив ролей, который далее используется для контроля доступа к ресурсам.

Разрешения (permissions) как более детализированный уровень контроля

В отличие от ролей, permissions позволяют описывать конкретные действия. Если роль — это абстракция (admin, user), то разрешение — это конкретная операция (read:posts, delete:comment).

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

{
  "sub": "user_123",
  "roles": ["editor"],
  "permissions": ["post:create", "post:edit", "comment:delete"]
}

В jose это также добавляется через payload:

const token = await new SignJWT({
  roles: ['editor'],
  permissions: ['post:create', 'post:edit']
})
  .setProtectedHeader({ alg: 'HS256' })
  .setSubject('user_123')
  .setExpirationTime('1h')
  .sign(secret);

Модель проверки доступа на основе roles

Базовая проверка роли после декодирования токена:

function hasRole(payload, role) {
  return payload.roles?.includes(role);
}

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

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

if (!hasRole(payload, 'admin')) {
  throw new Error('Доступ запрещён');
}

Такой подход широко используется для административных панелей и защищённых API.

Модель проверки permissions

Для более гранулярного контроля используется проверка permissions:

function hasPermission(payload, permission) {
  return payload.permissions?.includes(permission);
}

Пример применения:

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

if (!hasPermission(payload, 'post:delete')) {
  throw new Error('Недостаточно прав');
}

Комбинирование ролей и разрешений

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

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

function can(payload, permission) {
  if (payload.roles?.includes('admin')) {
    return true;
  }

  return payload.permissions?.includes(permission);
}

Такой подход позволяет роли admin иметь полный доступ без перечисления всех permissions.

Расширенные claims и контекстные ограничения

JWT claims могут содержать дополнительные атрибуты, влияющие на доступ:

{
  "sub": "user_123",
  "roles": ["editor"],
  "permissions": ["post:create"],
  "tenant": "company_a",
  "region": "eu-west"
}

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

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

function canAccessTenant(payload, tenant) {
  return payload.tenant === tenant;
}

Иммутабельность claims и проблема обновления прав

Одной из ключевых особенностей JWT является неизменяемость токена после выдачи. Это означает, что изменения ролей или permissions не отражаются до истечения срока жизни токена.

Для обхода этой проблемы применяются:

  • короткоживущие токены (15–60 минут)
  • refresh tokens
  • централизованная проверка blacklist токенов
  • хранение критичных прав на сервере

Использование jose для безопасного подписания claims

При работе с ролями и разрешениями важно обеспечить защиту от подмены данных. В jose это достигается выбором алгоритма подписи:

new SignJWT(payload)
  .setProtectedHeader({ alg: 'HS256' })

или более безопасного варианта с RSA:

new SignJWT(payload)
  .setProtectedHeader({ alg: 'RS256' })

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

Ошибки проектирования claims в системах ролей

Частые проблемы:

  • чрезмерное разрастание roles вместо перехода к permissions
  • хранение чувствительных данных в payload
  • отсутствие версии схемы claims
  • невозможность отзыва токена при изменении прав
  • дублирование логики авторизации на клиенте и сервере

Особенно критично размещение секретной информации в JWT, так как токен легко декодируется без подписи.

Версионирование claims

Для контроля изменений структуры токена часто вводят поле версии:

{
  "sub": "user_123",
  "roles": ["user"],
  "ver": 2
}

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

Практическая модель авторизации на основе jose

Комбинированный пример:

import { jwtVerify } from 'jose';

async function authorize(token, requiredPermission) {
  const secret = new TextEncoder().encode('super-secret-key');
  const { payload } = await jwtVerify(token, secret);

  const isAdmin = payload.roles?.includes('admin');
  const hasPerm = payload.permissions?.includes(requiredPermission);

  if (!isAdmin && !hasPerm) {
    throw new Error('Forbidden');
  }

  return payload;
}

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

Связь claims с архитектурой микросервисов

В распределённых системах JWT claims становятся источником истины для каждого сервиса. Это исключает необходимость синхронных запросов к центральной базе авторизации.

Каждый сервис самостоятельно проверяет:

  • подпись токена
  • роли пользователя
  • разрешения на действие
  • контекст (tenant, region, scope)

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