DPoP (Demonstration of Proof-of-Possession) представляет собой механизм криптографической привязки HTTP-запроса к конкретной паре ключей клиента. Основная цель — устранить возможность использования украденного access token без владения приватным ключом, который использовался при его получении.
В контексте JavaScript-библиотеки Jose реализация DPoP строится вокруг JWS (JSON Web Signature), где каждый запрос сопровождается отдельным подписанным JWT, называемым DPoP proof.
DPoP proof — это JWT, подписанный приватным ключом клиента. Он передаётся в HTTP-заголовке:
DPoP: eyJhbGciOiJFZERTQSIsInR5cCI6ImRwbCtqd3QifQ...
JWT состоит из трёх частей:
В библиотеке Jose заголовок формируется с указанием алгоритма и типа токена:
typ: всегда dpop+jwtalg: алгоритм подписи (чаще всего ES256,
RS256, EdDSA)jwk: публичный ключ клиента в формате JWKПример:
{
"typ": "dpop+jwt",
"alg": "ES256",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
}
Ключевой момент: сервер использует jwk
для привязки access token к конкретному ключу клиента.
Payload содержит обязательные поля, которые обеспечивают защиту от повторного использования и подделки запроса:
htu — HTTP URI запросаhtm — HTTP метод (GET, POST и т.д.)iat — время выпуска токена (issued at)jti — уникальный идентификатор токена (для защиты от
replay-атак)Пример payload:
{
"htu": "https://api.example.com/resource",
"htm": "POST",
"iat": 1710000000,
"jti": "b7f1c2c3-8d2a-4d5e-9f1c-1a2b3c4d5e6f"
}
Подпись формируется приватным ключом клиента. В библиотеке Jose это
реализуется через SignJWT.
Пример генерации DPoP proof:
import { generateKeyPair, SignJWT, exportJWK } from 'jose'
const { privateKey, publicKey } = await generateKeyPair('ES256')
const jwk = await exportJWK(publicKey)
const dpop = await new SignJWT({
htu: 'https://api.example.com/resource',
htm: 'POST',
iat: Math.floor(Date.now() / 1000),
jti: crypto.randomUUID()
})
.setProtectedHeader({
typ: 'dpop+jwt',
alg: 'ES256',
jwk
})
.sign(privateKey)
После успешной авторизации сервер возвращает access token, содержащий подтверждение привязки ключа:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "DPoP",
"cnf": {
"jkt": "sha256-thumbprint-of-jwk"
}
}
Поле cnf.jkt — это SHA-256 thumbprint публичного ключа,
использованного при создании DPoP proof.
Первый этап — декодирование и проверка формата:
import { jwtVerify, importJWK } from 'jose'
Проверяется:
typ = dpop+jwtjwk в headerconst publicKey = await importJWK(dpopHeader.jwk, 'ES256')
Этот ключ используется для проверки подписи.
await jwtVerify(dpop, publicKey)
Если подпись не совпадает — запрос отклоняется.
После криптографической верификации выполняется логическая проверка:
htm === req.method
Несовпадение означает попытку подмены запроса.
htu === request.url
Проверяется полное совпадение URI, включая схему и путь.
Math.abs(now - iat) < allowedSkew
Обычно допускается небольшое окно (например, 5 минут).
jti должен быть уникальным. Сервер хранит уже
использованные значения:
if (usedJtiSet.has(payload.jti)) {
throw new Error('Replay attack detected')
}
После проверки DPoP proof сервер связывает его с access token через thumbprint:
import { calculateJwkThumbprint } from 'jose'
const thumbprint = await calculateJwkThumbprint(publicKey)
Сравнение:
thumbprint === access_token.cnf.jkt
Если значение не совпадает, токен считается украденным или поддельным.
При каждом запросе сервер выполняет полный цикл:
DPoPhtu и htmiatjtijkt с access tokenПример middleware:
import { jwtVerify, importJWK, calculateJwkThumbprint } from 'jose'
export async function verifyDpop(req) {
const dpop = req.headers['dpop']
const token = req.headers['authorization']?.replace('Bearer ', '')
const { payload, protectedHeader } = await jwtVerify(dpop, async (header) => {
return await importJWK(header.jwk, header.alg)
})
if (payload.htu !== req.url) {
throw new Error('Invalid htu')
}
if (payload.htm !== req.method) {
throw new Error('Invalid htm')
}
const thumbprint = await calculateJwkThumbprint(protectedHeader.jwk)
const accessPayload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString())
if (accessPayload.cnf?.jkt !== thumbprint) {
throw new Error('Key mismatch')
}
return true
}
DPoP добавляет несколько уровней защиты:
htu (открывает возможность replay
на другом endpoint)jtiiat, позволяющее replay-атакиcnf.jktПри использовании jwtVerify возможны исключения:
JWSSignatureVerificationFailedJWTExpiredJOSEErrorКаждое из них требует отдельной обработки на уровне middleware, иначе система остаётся уязвимой к частичным обходам проверки.
cnf.jktDPoP особенно актуален в:
В связке с Jose он обеспечивает строгую криптографическую модель доверия без хранения сессионных секретов на сервере.