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 REST API request must include a fresh DPoP proof JWT in the DPoP header.

DPoP applies to the REST API only

DPoP secures calls to the REST API. The MQTT streaming service does not use DPoP - there you authenticate by supplying your access token as the MQTT connection password. See the authentication overview for the two token models.

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 a PS256 (RSASSA-PSS, SHA-256) key pair - a 2048-bit (or larger) RSA key. Store the private key securely and never expose it.

const { generateKeyPairSync } = require('crypto');

const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
});

// 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": "PS256",
"typ": "dpop+jwt",
"jwk": {
"kty": "RSA",
"n": "...",
"e": "AQAB"
}
}

Payload​

{
"jti": "unique-random-id",
"htm": "GET",
"htu": "https://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: 'PS256',
typ: 'dpop+jwt',
jwk: publicJwk,
})
.sign(privateKey);

return proof;
}

Using the DPoP proof​

Attach the proof to every API request:

curl -X GET https://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"
}