Scope-based авторизация

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, оставляя обработку на стороне приложения.


Проверка JWT через jose

Базовая операция — верификация токена с проверкой подписи и извлечением 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

После декодирования токена необходимо привести 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 []
}

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


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

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))
}

Интеграция с middleware

В серверных приложениях 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 и доменная модель доступа

В реальных системах scopes часто структурируются по доменам:

  • users:read
  • users:write
  • orders:read
  • orders:refund
  • admin:*

Такой подход позволяет строить иерархии доступа без внедрения сложных 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))
}

Безопасность обработки scopes

Использование jose гарантирует криптографическую целостность токена, однако безопасность авторизации зависит от логики проверки:

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

Особое внимание требуется при работе с внешними identity-провайдерами, где формат scope может отличаться.


Ошибки проектирования scope-based систем

Распространённые проблемы:

  • чрезмерно крупные scopes (all вместо гранулярных прав)
  • дублирование логики авторизации на разных слоях
  • отсутствие централизованной функции проверки
  • смешивание roles и scopes без разграничения
  • хранение бизнес-логики в токене вместо сервиса авторизации

Использование jose в цепочке доверия

В архитектурах с распределённой аутентификацией jose выполняет роль проверяющего слоя между внешним identity provider и внутренней системой:

  1. Получение JWT от клиента
  2. Проверка подписи через JWKS
  3. Валидация claims (iss, aud, exp)
  4. Извлечение scopes
  5. Применение политик доступа

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


Расширенные модели поверх scopes

Поверх базовой модели часто строятся дополнительные уровни:

  • policy-based access control (PBAC)
  • attribute-based access control (ABAC)
  • комбинация scopes + context (IP, device, time)

Пример контекстной проверки:

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
}

Оптимизация проверки в высоконагруженных системах

При высокой частоте запросов критично снижать стоимость проверки:

  • кэширование результата jwtVerify на короткий TTL
  • предвычисление Set из scopes
  • минимизация строковых операций
  • отказ от глубоких копирований payload
function buildScopeSet(scopes) {
  return new Set(scopes)
}

function hasScopeFast(scopeSet, scope) {
  return scopeSet.has(scope)
}

Поведение при истечении токена

Проверка exp выполняется на уровне jose автоматически при jwtVerify. При необходимости можно расширять контроль:

  • допустимый clock skew
  • ревокация через blacklist
  • короткоживущие access tokens с refresh token flow

Практическая модель применения scopes

Scope-based авторизация особенно эффективна в API-first системах:

  • микросервисы
  • публичные REST API
  • GraphQL gateways
  • serverless архитектуры

Каждый endpoint связывается с минимально необходимым набором scopes, что упрощает аудит и контроль доступа.

app.get('/messages', requireScope('messages:read'), handler)
app.post('/messages', requireScope('messages:write'), handler)

Комбинация jose и политики доступа

При усложнении системы логика проверки 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) от бизнес-логики доступа.