Scope-based авторизация в системах, использующих JSON Web Token, строится вокруг идеи разделения прав доступа на мелкие, декларативные единицы — scopes. Каждый токен несёт в себе не только идентификационную информацию о субъекте, но и набор разрешений, определяющих, какие действия доступны в рамках конкретного контекста.
В экосистеме JavaScript библиотека jose предоставляет инструменты для работы с JWT, JWS и JWE, обеспечивая криптографически корректное создание, проверку и декодирование токенов. В контексте scope-based авторизации основная роль jose заключается в безопасной верификации токена и извлечении полезной нагрузки, содержащей список разрешений.
Scopes обычно представляются в виде строки или массива строк внутри payload токена. Наиболее распространённый формат — строка с пробелами:
{
"sub": "user_123",
"scope": "read:messages write:messages admin:settings"
}
Альтернативный формат — массив:
{
"sub": "user_123",
"scope": ["read:messages", "write:messages", "admin:settings"]
}
Выбор структуры зависит от соглашений в системе авторизации, однако библиотека jose не накладывает ограничений на формат payload, оставляя обработку на стороне приложения.
Базовая операция — верификация токена с проверкой подписи и извлечением payload. В современных версиях jose используется модульная структура API:
import { jwtVerify, createRemoteJWKSet } from 'jose'
const JWKS = createRemoteJWKSet(
new URL('https://auth.example.com/.well-known/jwks.json')
)
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://auth.example.com',
audience: 'api.example.com'
})
Результатом является объект payload, содержащий claims, включая scope.
После декодирования токена необходимо привести scopes к единому виду для последующей проверки:
function normalizeScopes(payload) {
if (!payload.scope) return []
if (Array.isArray(payload.scope)) {
return payload.scope
}
if (typeof payload.scope === 'string') {
return payload.scope.split(' ').filter(Boolean)
}
return []
}
Нормализация критична для унификации проверки доступа, особенно в системах, где токены поступают из разных источников.
Scope-based авторизация опирается на простое правило: доступ разрешён, если требуемый scope присутствует в списке токена.
function hasScope(tokenScopes, requiredScope) {
return tokenScopes.includes(requiredScope)
}
Для более сложных систем применяется проверка набора разрешений:
function hasAllScopes(tokenScopes, requiredScopes) {
return requiredScopes.every(scope => tokenScopes.includes(scope))
}
И альтернативная модель — доступ при наличии хотя бы одного разрешения:
function hasAnyScope(tokenScopes, requiredScopes) {
return requiredScopes.some(scope => tokenScopes.includes(scope))
}
В серверных приложениях scope-проверка обычно реализуется на уровне middleware.
Пример для Node.js:
import { jwtVerify, createRemoteJWKSet } from 'jose'
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/jwks'))
async function authMiddleware(req, res, next) {
try {
const token = req.headers.authorization?.replace('Bearer ', '')
const { payload } = await jwtVerify(token, JWKS)
req.user = {
id: payload.sub,
scopes: normalizeScopes(payload)
}
next()
} catch (e) {
res.status(401).send('Unauthorized')
}
}
Дальнейшая авторизация на уровне маршрутов:
function requireScope(scope) {
return (req, res, next) => {
if (!req.user?.scopes.includes(scope)) {
return res.status(403).send('Forbidden')
}
next()
}
}
В реальных системах scopes часто структурируются по доменам:
users:readusers:writeorders:readorders:refundadmin:*Такой подход позволяет строить иерархии доступа без внедрения сложных RBAC-матриц.
Обработка wildcard-прав:
function matchScope(tokenScope, requiredScope) {
if (tokenScope === requiredScope) return true
if (tokenScope.endsWith(':*')) {
const prefix = tokenScope.replace(':*', ':')
return requiredScope.startsWith(prefix)
}
return false
}
function hasScopeAdvanced(tokenScopes, requiredScope) {
return tokenScopes.some(scope => matchScope(scope, requiredScope))
}
Использование jose гарантирует криптографическую целостность токена, однако безопасность авторизации зависит от логики проверки:
iss и audОсобое внимание требуется при работе с внешними identity-провайдерами, где формат scope может отличаться.
Распространённые проблемы:
all вместо гранулярных
прав)В архитектурах с распределённой аутентификацией jose выполняет роль проверяющего слоя между внешним identity provider и внутренней системой:
Такая схема минимизирует зависимость от централизованных сессий и позволяет масштабировать авторизацию горизонтально.
Поверх базовой модели часто строятся дополнительные уровни:
Пример контекстной проверки:
function canAccess(payload, context) {
const scopes = normalizeScopes(payload)
const baseAccess = scopes.includes('resource:read')
const timeAccess = context.hour >= 9 && context.hour <= 18
return baseAccess && timeAccess
}
При высокой частоте запросов критично снижать стоимость проверки:
function buildScopeSet(scopes) {
return new Set(scopes)
}
function hasScopeFast(scopeSet, scope) {
return scopeSet.has(scope)
}
Проверка exp выполняется на уровне jose автоматически
при jwtVerify. При необходимости можно расширять
контроль:
Scope-based авторизация особенно эффективна в API-first системах:
Каждый endpoint связывается с минимально необходимым набором scopes, что упрощает аудит и контроль доступа.
app.get('/messages', requireScope('messages:read'), handler)
app.post('/messages', requireScope('messages:write'), handler)
При усложнении системы логика проверки scopes часто выносится в отдельный слой:
class AuthorizationService {
constructor(policy) {
this.policy = policy
}
isAllowed(user, action) {
const scopes = buildScopeSet(user.scopes)
return this.policy[action].every(s => scopes.has(s))
}
}
Такой подход отделяет криптографическую проверку токена (jose) от бизнес-логики доступа.