Метод client assertion с использованием private_key_jwt в OAuth 2.0 применяется для аутентификации клиента без передачи client_secret, заменяя его подписанным JWT, который доказывает владение приватным ключом. В контексте библиотеки jose в JavaScript этот механизм реализуется через создание и криптографическую подпись JWT с последующей отправкой его в token endpoint как часть запроса.
В классической схеме OAuth 2.0 клиент часто аутентифицируется с
помощью client_id и client_secret. Такой
подход имеет ограничения: секрет должен храниться на стороне приложения,
что повышает риск компрометации.
private_key_jwt решает эту проблему иначе:
Таким образом, вместо передачи секрета передаётся доказательство владения ключом.
Библиотека jose предоставляет инструменты для работы с:
Для private_key_jwt используются:
SignJWT — создание JWTimportPKCS8 или importJWK — импорт
приватного ключаJWT, используемый как client assertion, содержит стандартные claims:
iss — идентификатор клиента (client_id)sub — также client_idaud — URL token endpoint сервера авторизацииexp — время истечения (обычно короткое, 1–5 минут)iat — время выпуска токенаjti — уникальный идентификатор для предотвращения
повторного использованияКлючевой момент — строгое соответствие aud адресу token
endpoint, иначе сервер отклонит запрос.
Процесс создания client assertion включает загрузку приватного ключа и подпись payload.
import { SignJWT, importPKCS8 } from 'jose';
const privateKeyPem = `
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
`;
const privateKey = await importPKCS8(privateKeyPem, 'RS256');
const clientAssertion = await new SignJWT({})
.setProtectedHeader({ alg: 'RS256', typ: 'JWT' })
.setIssuer('your_client_id')
.setSubject('your_client_id')
.setAudience('https://auth.server.com/oauth/token')
.setJti(crypto.randomUUID())
.setIssuedAt()
.setExpirationTime('2m')
.sign(privateKey);
В данном случае payload может быть пустым объектом, поскольку все необходимые данные находятся в claims.
После генерации JWT он передаётся в запросе получения токена:
const params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
params.append('client_id', 'your_client_id');
params.append('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
params.append('client_assertion', clientAssertion);
const response = await fetch('https://auth.server.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params.toString()
});
const data = await response.json();
Параметр client_assertion_type фиксирован и указывает
серверу, что используется JWT bearer assertion.
Сервер выполняет несколько проверок:
exp не истёкiat допустим по времениaud совпадает с token endpointiss и sub соответствуют
зарегистрированному client_idjti не использовался ранее (защита от replay атак)Если хотя бы одно условие нарушено, запрос отклоняется.
На практике чаще всего используются:
RS256 проще в интеграции и шире поддерживается, ES256 даёт меньший размер подписи и более современную криптографию.
Пример с ES256:
import { importPKCS8, SignJWT } from 'jose';
const privateKey = await importPKCS8(ecPrivateKeyPem, 'ES256');
const assertion = await new SignJWT({})
.setProtectedHeader({ alg: 'ES256' })
.setIssuer(clientId)
.setSubject(clientId)
.setAudience(tokenEndpoint)
.setExpirationTime('120s')
.sign(privateKey);
Часто встречающиеся проблемы:
Несоответствие aud Если указан не точный URL token
endpoint, сервер отклоняет JWT даже при корректной подписи.
Слишком длинный exp JWT с большим временем жизни
увеличивает риск повторного использования.
Отсутствие jti Без уникального идентификатора невозможно
эффективно защищаться от replay атак.
Неверный алгоритм Если сервер ожидает RS256, а клиент подписывает ES256, проверка провалится.
Библиотека jose строго следует спецификациям JWT RFC 7519 и JWS RFC 7515, поэтому:
При работе в production важно:
jti для аудитаСхема использования private_key_jwt обычно выглядит так:
Такой подход полностью исключает необходимость хранения client_secret и повышает устойчивость к утечкам конфигурации.