Skip to main content

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:

{
"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
}
ClaimDescription
jtiUnique identifier for this proof (prevents replay)
htmHTTP method of the request (uppercase)
htuTarget URI of the request (scheme + authority + path, no query)
iatIssued-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

  1. Fresh proof per request — Each API call needs a new DPoP proof with a unique jti
  2. Method must match — The htm claim must match the HTTP method of the request
  3. URI must match — The htu claim must match the request URL (without query parameters)
  4. Short-lived — Proofs are validated against iat; clock skew tolerance is typically 60 seconds
  5. 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"
}