DPoP Proof-of-Possession
DPoP (Demonstrating Proof-of-Possession, RFC 9449) binds access tokens to a specific client key pair. Every authenticated request must include a fresh DPoP proof JWT in the DPoP header.
Why DPoP?
Without DPoP, a stolen access token can be used by any party. DPoP ensures that only the holder of the private key can use the token, even if the token is intercepted.
Key pair generation
Generate an ES256 (P-256 ECDSA) key pair. Store the private key securely and never expose it.
const { generateKeyPairSync } = require('crypto');
const { publicKey, privateKey } = generateKeyPairSync('ec', {
namedCurve: 'P-256',
});
// Export the public key as JWK for the DPoP header
const jwk = publicKey.export({ format: 'jwk' });
DPoP proof structure
A DPoP proof is a JWT with the following structure:
Header
{
"alg": "ES256",
"typ": "dpop+jwt",
"jwk": {
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "..."
}
}
Payload
{
"jti": "unique-random-id",
"htm": "GET",
"htu": "https://api.data.jlr-vcdp.com/v1/vehicles/SALGA2BN6PH123456",
"iat": 1705312600
}
| Claim | Description |
|---|---|
jti | Unique identifier for this proof (prevents replay) |
htm | HTTP method of the request (uppercase) |
htu | Target URI of the request (scheme + authority + path, no query) |
iat | Issued-at timestamp (seconds since epoch) |
Creating a DPoP proof (Node.js)
const { SignJWT } = require('jose');
const { v4: uuidv4 } = require('uuid');
async function createDPoPProof(privateKey, publicJwk, method, url) {
const proof = await new SignJWT({
jti: uuidv4(),
htm: method,
htu: url,
iat: Math.floor(Date.now() / 1000),
})
.setProtectedHeader({
alg: 'ES256',
typ: 'dpop+jwt',
jwk: publicJwk,
})
.sign(privateKey);
return proof;
}
Using the DPoP proof
Attach the proof to every API request:
curl -X GET https://api.data.jlr-vcdp.com/v1/vehicles/SALGA2BN6PH123456 \
-H "Authorization: DPoP eyJhbGciOiJSUzI1NiIs..." \
-H "DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0..."
Important rules
- Fresh proof per request — Each API call needs a new DPoP proof with a unique
jti - Method must match — The
htmclaim must match the HTTP method of the request - URI must match — The
htuclaim must match the request URL (without query parameters) - Short-lived — Proofs are validated against
iat; clock skew tolerance is typically 60 seconds - Same key pair — Use the same key pair that was used when obtaining the access token
Error responses
If the DPoP proof is invalid, the API returns 401 Unauthorized:
{
"type": "/errors/invalid-dpop-proof",
"title": "Invalid DPoP Proof",
"status": 401,
"detail": "The DPoP proof JWT is expired or malformed"
}