Claims в JWT представляют собой структурированный способ передачи информации о пользователе и контексте его авторизации. В контексте библиотеки jose (JavaScript Object Signing and Encryption) работа с claims становится центральным элементом построения систем аутентификации и авторизации, особенно когда требуется разграничение ролей и прав доступа.
JWT (JSON Web Token) состоит из трёх частей: заголовка, полезной нагрузки (payload) и подписи. Именно payload содержит claims — набор утверждений, описывающих субъект (sub), время жизни токена (exp), а также пользовательские данные.
В стандартной практике выделяют два типа claims:
Для систем авторизации ключевое значение имеют именно custom claims, так как они позволяют внедрить модель RBAC (Role-Based Access Control) или более гибкую ABAC (Attribute-Based Access Control).
Роль пользователя обычно представляется в виде строки или массива строк внутри 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 позволяют описывать конкретные действия. Если роль — это абстракция (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);
Базовая проверка роли после декодирования токена:
function hasRole(payload, role) {
return payload.roles?.includes(role);
}
Пример использования:
const { payload } = await jwtVerify(token, secret);
if (!hasRole(payload, 'admin')) {
throw new Error('Доступ запрещён');
}
Такой подход широко используется для административных панелей и защищённых API.
Для более гранулярного контроля используется проверка 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.
JWT claims могут содержать дополнительные атрибуты, влияющие на доступ:
{
"sub": "user_123",
"roles": ["editor"],
"permissions": ["post:create"],
"tenant": "company_a",
"region": "eu-west"
}
Это позволяет строить многоуровневые системы авторизации, где доступ зависит не только от роли, но и от контекста.
Пример проверки:
function canAccessTenant(payload, tenant) {
return payload.tenant === tenant;
}
Одной из ключевых особенностей JWT является неизменяемость токена после выдачи. Это означает, что изменения ролей или permissions не отражаются до истечения срока жизни токена.
Для обхода этой проблемы применяются:
При работе с ролями и разрешениями важно обеспечить защиту от подмены данных. В jose это достигается выбором алгоритма подписи:
new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
или более безопасного варианта с RSA:
new SignJWT(payload)
.setProtectedHeader({ alg: 'RS256' })
При использовании асимметричной криптографии приватный ключ остаётся на сервере авторизации, а публичный используется для проверки токена в API.
Частые проблемы:
Особенно критично размещение секретной информации в JWT, так как токен легко декодируется без подписи.
Для контроля изменений структуры токена часто вводят поле версии:
{
"sub": "user_123",
"roles": ["user"],
"ver": 2
}
Это позволяет серверу различать старые и новые форматы авторизации и корректно обрабатывать токены разных поколений.
Комбинированный пример:
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;
}
Такая модель позволяет гибко расширять систему без изменения базовой структуры токена.
В распределённых системах JWT claims становятся источником истины для каждого сервиса. Это исключает необходимость синхронных запросов к центральной базе авторизации.
Каждый сервис самостоятельно проверяет:
Это снижает задержки и повышает масштабируемость, но требует строгого контроля над сроком жизни токенов и их размером.