В системах, где используется jose, ключи подписания и
шифрования JWT перестают быть статическим параметром и становятся частью
инфраструктуры безопасности. Разделение окружений — development,
staging, production — требует отдельного подхода к генерации, хранению и
использованию криптографических ключей, поскольку компрометация в одном
окружении не должна затрагивать остальные.
В библиотеке jose поддерживаются оба подхода:
симметричные алгоритмы (например, HS256) и асимметричные (RS256, ES256,
EdDSA). Выбор напрямую влияет на стратегию управления ключами в разных
окружениях.
Симметричный подход (HS256) Используется единый секрет для подписи и проверки токенов. В контексте окружений он допустим только в development, где:
Пример использования:
import { SignJWT, jwtVerify } from 'jose'
const secret = new TextEncoder().encode('dev-secret-key')
const token = await new SignJWT({ role: 'admin' })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('2h')
.sign(secret)
const { payload } = await jwtVerify(token, secret)
В staging и production такой подход создаёт единый вектор компрометации: утечка секрета означает полный контроль над токенами.
Для staging и production стандартом становится использование пары ключей:
Это позволяет безопасно распространять публичную часть в любые сервисы без риска подделки токенов.
Генерация ключевой пары:
import { generateKeyPair } from 'jose'
const { publicKey, privateKey } = await generateKeyPair('RS256')
Экспорт в JWK формат для хранения:
import { exportJWK } from 'jose'
const privateJwk = await exportJWK(privateKey)
const publicJwk = await exportJWK(publicKey)
Development допускает компромиссы ради скорости разработки:
.envТипичный вариант:
const devSecret = new TextEncoder().encode(process.env.DEV_JWT_SECRET)
Характерная особенность: ключ не меняется между сессиями, что позволяет стабильно воспроизводить поведение системы, но делает окружение непригодным для любых реальных данных.
Staging служит зоной проверки безопасности и поведения системы под нагрузкой. Здесь уже применяется асимметрия, но с упрощённой инфраструктурой:
Импорт ключей:
import { importJWK, SignJWT } from 'jose'
const privateKey = await importJWK(JSON.parse(process.env.STAGING_PRIVATE_JWK))
const token = await new SignJWT({ scope: 'test' })
.setProtectedHeader({ alg: 'RS256', kid: 'staging-key-1' })
.setIssuedAt()
.setExpirationTime('1h')
.sign(privateKey)
Ключевым элементом становится kid (Key ID), позволяющий
различать версии ключей при ротации.
Production-среда требует строгой модели управления жизненным циклом ключей:
jose позволяет работать с JWK напрямую, что удобно для
реализации JWKS:
import { exportJWK } from 'jose'
const publicJwk = await exportJWK(publicKey)
publicJwk.use = 'sig'
publicJwk.alg = 'RS256'
publicJwk.kid = 'prod-key-2026-01'
В production обычно формируется набор ключей:
{
"keys": [
{
"kty": "RSA",
"kid": "prod-key-2026-01",
"use": "sig",
"alg": "RS256",
"n": "...",
"e": "AQAB"
}
]
}
Production почти всегда работает с несколькими активными ключами одновременно. Это необходимо для плавной ротации без инвалидирования существующих токенов.
Модель:
kid определяет, каким ключом подписан JWTПодписание:
const token = await new SignJWT({ userId: 123 })
.setProtectedHeader({ alg: 'RS256', kid: 'prod-key-2026-01' })
.setIssuedAt()
.setExpirationTime('15m')
.sign(privateKey)
Проверка:
import { jwtVerify, createRemoteJWKSet } from 'jose'
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'))
const { payload } = await jwtVerify(token, JWKS)
Ключевая ошибка архитектуры — использование одинаковых ключей для dev, staging и production. Это приводит к следующим рискам:
Правильная модель:
kid префикс (dev-,
staging-, prod-)Типичная матрица выбора:
Пример выбора алгоритма:
const alg = process.env.NODE_ENV === 'production' ? 'RS256' : 'HS256'
Однако более строгая модель исключает HS256 вне локальной среды полностью.
joseНа практике часто встречаются следующие проблемы:
kid при ротацииКаждая из этих ошибок приводит к снижению уровня доверия к системе аутентификации.
Production-интеграция обычно строится вокруг:
Ключи никогда не должны попадать в репозиторий. Даже зашифрованные варианты ухудшают управляемость ротации.
Разделение окружений в jose — это не только техническая
мера, но и граница доверия:
Каждое окружение формирует собственный контур ключей, несовместимый с другими по дизайну, а не по договорённости.