DPoP — proof of possession
The messpunkt.io production API will require DPoP (RFC 9449) on every request. A stolen access token is useless without the private key the token is bound to. The sandbox currently accepts plain Bearer tokens as well, so Swagger UI and curl work without extra setup — but partners should implement DPoP before GA.
What DPoP does in one paragraph
Your client generates an ECDSA (ES256) or RSA (RS256) key pair once and keeps it in memory or a secure store. On every API call, you sign a short-lived JWT — the DPoP proof — that contains the HTTP method, full URL, a unique jti, and the current timestamp. You send it in a DPoP header alongside the access token. The server checks the signature against the public-key thumbprint bound into the access token (cnf.jkt claim). If they match, the request is authentic.
Request shape
GET /v1/whoami HTTP/2
Host: api.sandbox.messpunkt.io
Authorization: DPoP eyJhbGciOiJSUzI1NiIs... ← the access token
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZy... ← the proof, a separate JWT
Proof contents
| Header | Value |
|---|---|
typ | dpop+jwt |
alg | ES256 (recommended) or RS256 |
jwk | The public key in JWK form |
| Claim | Value |
jti | Random UUID — unique per request |
htm | Request method, e.g. GET |
htu | Request URL without query string, e.g. https://api.sandbox.messpunkt.io/v1/whoami |
iat | Current Unix time (seconds) |
ath | Base64url(SHA-256(access_token)) — access-token hash |
Working code
Same request, four languages. Pick your stack, copy, adapt.
// dotnet add package Microsoft.IdentityModel.JsonWebTokens
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
var ec = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var key = new ECDsaSecurityKey(ec);
var jwk = JsonWebKeyConverter.ConvertFromECDsaSecurityKey(key);
jwk.Use = "sig"; jwk.Alg = "ES256";
string Proof(string method, string url, string accessToken)
{
using var sha = SHA256.Create();
var ath = Base64UrlEncoder.Encode(
sha.ComputeHash(Encoding.UTF8.GetBytes(accessToken)));
var descriptor = new SecurityTokenDescriptor {
Claims = new Dictionary<string, object> {
["jti"] = Guid.NewGuid().ToString("N"),
["htm"] = method,
["htu"] = url,
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
["ath"] = ath,
},
SigningCredentials = new SigningCredentials(
key, SecurityAlgorithms.EcdsaSha256),
TokenType = "dpop+jwt",
AdditionalHeaderClaims = new Dictionary<string, object> {
["jwk"] = new { kty = jwk.Kty, crv = jwk.Crv, x = jwk.X, y = jwk.Y }
},
};
return new JsonWebTokenHandler().CreateToken(descriptor);
}
var method = HttpMethod.Get;
var url = "https://api.sandbox.messpunkt.io/v1/whoami";
var accessToken = "..."; // from /oauth/token
var req = new HttpRequestMessage(method, url);
req.Headers.Add("Authorization", $"DPoP {accessToken}");
req.Headers.Add("DPoP", Proof(method.Method, url, accessToken));
var http = new HttpClient();
var res = await http.SendAsync(req);
Console.WriteLine(await res.Content.ReadAsStringAsync());
// npm i jose
import { generateKeyPair, SignJWT, exportJWK, calculateJwkThumbprint } from 'jose';
import { createHash, randomUUID } from 'crypto';
const { privateKey, publicKey } = await generateKeyPair('ES256', { extractable: true });
const publicJwk = await exportJWK(publicKey);
publicJwk.alg = 'ES256';
async function dpopProof(method, url, accessToken) {
const ath = createHash('sha256').update(accessToken).digest('base64url');
return await new SignJWT({
jti: randomUUID(),
htm: method,
htu: url,
iat: Math.floor(Date.now() / 1000),
ath,
})
.setProtectedHeader({ alg: 'ES256', typ: 'dpop+jwt', jwk: publicJwk })
.sign(privateKey);
}
const accessToken = '...'; // from /oauth/token
const url = 'https://api.sandbox.messpunkt.io/v1/whoami';
const proof = await dpopProof('GET', url, accessToken);
const res = await fetch(url, {
headers: {
Authorization: `DPoP ${accessToken}`,
DPoP: proof,
},
});
console.log(await res.json());
# pip install authlib httpx
from authlib.jose import JsonWebKey, jwt
from cryptography.hazmat.primitives.asymmetric import ec
from hashlib import sha256
import base64, time, uuid, httpx
priv = ec.generate_private_key(ec.SECP256R1())
jwk_priv = JsonWebKey.import_key(
priv.private_bytes_raw() if False else priv, {"kty": "EC", "crv": "P-256", "alg": "ES256", "use": "sig"})
jwk_pub = jwk_priv.as_dict(is_private=False)
def dpop_proof(method: str, url: str, access_token: str) -> str:
ath = base64.urlsafe_b64encode(sha256(access_token.encode()).digest()).rstrip(b"=").decode()
header = {"typ": "dpop+jwt", "alg": "ES256", "jwk": jwk_pub}
payload = {
"jti": str(uuid.uuid4()),
"htm": method,
"htu": url,
"iat": int(time.time()),
"ath": ath,
}
return jwt.encode(header, payload, jwk_priv).decode()
url = "https://api.sandbox.messpunkt.io/v1/whoami"
access_token = "..." # from /oauth/token
r = httpx.get(url, headers={
"Authorization": f"DPoP {access_token}",
"DPoP": dpop_proof("GET", url, access_token),
})
print(r.json())
# needs: openssl, jose-util (https://github.com/latchset/jose) or step-cli
# 1. one-time key generation
jose jwk gen -i '{"alg":"ES256"}' -o priv.jwk
jose jwk pub -i priv.jwk -o pub.jwk
# 2. per-request proof (swap ACCESS, URL, METHOD)
ACCESS="eyJhbGciOi..."
URL="https://api.sandbox.messpunkt.io/v1/whoami"
METHOD="GET"
ATH=$(printf "%s" "$ACCESS" | openssl dgst -binary -sha256 \
| openssl base64 | tr -d '=' | tr '/+' '_-')
PAYLOAD=$(jq -c -n \
--arg jti "$(uuidgen)" --arg htm "$METHOD" --arg htu "$URL" \
--arg ath "$ATH" --argjson iat "$(date +%s)" \
'{jti:$jti, htm:$htm, htu:$htu, iat:$iat, ath:$ath}')
HEADER=$(jq -c -n --slurpfile jwk pub.jwk \
'{typ:"dpop+jwt", alg:"ES256", jwk:$jwk[0]}')
PROOF=$(jose jws sig -I <(echo "$PAYLOAD") -k priv.jwk \
-s "$HEADER" -c -o -)
curl -H "Authorization: DPoP $ACCESS" -H "DPoP: $PROOF" "$URL"
Token-endpoint DPoP
The production /oauth/token endpoint will additionally require a DPoP proof when you exchange the code or refresh token. The proof has the same shape minus ath, and the returned access token will carry a cnf.jkt claim bound to the same key. Sandbox currently skips this.
Common pitfalls
- Clock skew.
iatmust be within ±60 seconds of the server clock. Use NTP. Don't reuse a proof from a paused debugger session. - URL mismatch.
htuis the target URL without query string and without port when default.https://api.sandbox.messpunkt.io/v1/whoami, nothttps://api.sandbox.messpunkt.io:443/v1/whoami. - Key reuse across processes. If your app scales horizontally, every instance must either share the key or register its own public key with the authorization server. The token is bound to a specific key thumbprint.
- Replay window. Each
jtimust be unique within a 5-minute window. Always useuuid.v4(). - ES256 vs RS256. Prefer ES256 (smaller proofs, faster sign). Only use RS256 if your HSM or legacy crypto library forces it.
Next
See also: Error handling · Quickstart · Full API reference