Skip to content

Access IAM APIs with DPoP

DPoP (Demonstration of Proof-of-Possession) is a security mechanism that cryptographically binds an access token to the key pair held by the client. This prevents a stolen token from being replayed by any other party — the token is only usable together with a fresh proof signed by the original private key.

This guide covers Phase 2 — generating a DPoP proof and calling IAM APIs. For Phase 1 (key pair + DPoP-bound token from Trimble Identity), see TID — Obtain a DPoP Proof and DPoP-Bound Access Token.

  1. A long-lived asymmetric key pair saved as dpop_private_key.pem — the same EC P-256 key used in TID Phase 1 when the access token was issued.
  2. A DPoP-bound access token with a cnf.jkt claim matching your public key thumbprint.

DPoP end-to-end flow

A DPoP proof is a signed JWT. Generate a fresh proof for every request (single-use, expires in 5 minutes).

Parameter Value
typ dpop+jwt
alg Asymmetric algorithm — recommended: ES256. Also supported: ES384, ES512, RS256, RS384, RS512, PS256, PS384, PS512, EdDSA
jwk Your public key in JWK format
Claim Required Purpose
jti Always Unique ID per request (e.g. UUID)
iat Always Current Unix timestamp (±30 s clock skew)
htm Always HTTP method in uppercase
htu Always The full URL you are calling — scheme + host + path (omit query string and fragment)
ath IAM API calls only base64url(SHA-256(access_token)) — hash the raw JWT string exactly as sent in Authorization: DPoP <access_token> (no DPoP prefix); omit for TID token requests
Header Value
Authorization DPoP <access_token>
DPoP <dpop_proof_jwt>

Example:

GET /iam/v1/users HTTP/1.1
Host: cloud.stage.api.trimblecloud.com
Authorization: DPoP eyJhbGciOiJSUzI1NiIs...
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs...
  1. Proof header (typ, alg, jwk) and signature
  2. ath matches the access token hash
  3. cnf.jkt in the token matches the proof jwk thumbprint (RFC 7638)
  4. htm and htu match the request
  5. iat is within 5 minutes; jti has not been replayed

Generate a DPoP proof the same way as TID Phase 1 — but for IAM, with two differences:

  1. Set htm and htu to the IAM API call you are making.
  2. Add ath — base64url(SHA-256(access_token)), where access_token is the raw JWT string.

The examples below use ES256 (EC P-256) with the same private key from TID Phase 1.

/// tab | Python

import base64, datetime, hashlib, jwt, math, uuid
from cryptography.hazmat.primitives import serialization
# 1. Your values
signing_alg = "ES256"
access_token = "<dpop-bound-access-token>"
api_url = "https://cloud.stage.api.trimblecloud.com/platform/iam/v1/users"
http_method = "GET"
# 2. Load private key (same EC P-256 key from TID Phase 1)
with open("dpop_private_key.pem", "rb") as f:
private_key_pem = f.read()
private_key = serialization.load_pem_private_key(private_key_pem, password=None)
# 3. Derive public JWK from the private key
pub = private_key.public_key()
sz = math.ceil(pub.key_size / 8)
b64 = lambda n: base64.urlsafe_b64encode(n.to_bytes(sz, "big")).decode().rstrip("=")
nums = pub.public_numbers()
jwk = {"kty": "EC", "crv": "P-256", "x": b64(nums.x), "y": b64(nums.y)}
# 4. Build DPoP proof JWT
ath = base64.urlsafe_b64encode(
hashlib.sha256(access_token.encode("utf-8")).digest()
).decode().rstrip("=")
dpop_proof = jwt.encode(
{
"jti": str(uuid.uuid4()),
"iat": int(datetime.datetime.now(datetime.timezone.utc).timestamp()),
"htm": http_method.upper(),
"htu": api_url,
"ath": ath,
},
private_key_pem,
algorithm=signing_alg,
headers={"typ": "dpop+jwt", "jwk": jwk},
)
# 5. Send the IAM request
# import requests
# response = requests.get(api_url, headers={
# "Authorization": f"DPoP {access_token}",
# "DPoP": dpop_proof,
# })

///

/// tab | Java

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.JWTClaimsSet;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.*;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.*;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
public class DpopIam {
public static void main(String[] args) throws Exception {
// 1. Your values
String signingAlg = "ES256";
String accessToken = "<dpop-bound-access-token>";
String apiUrl = "https://cloud.stage.api.trimblecloud.com/platform/iam/v1/users";
String httpMethod = "GET";
// 2. Load private key (same EC P-256 key from TID Phase 1)
String privateKeyPem = Files.readString(Path.of("dpop_private_key.pem"));
Security.addProvider(new BouncyCastleProvider());
PrivateKeyInfo keyInfo = (PrivateKeyInfo) new PEMParser(new StringReader(privateKeyPem)).readObject();
ECPrivateKey privateKey = (ECPrivateKey) new JcaPEMKeyConverter()
.setProvider("BC").getPrivateKey(keyInfo);
// 3. Derive public JWK from the private key
ECNamedCurveParameterSpec curve = ECNamedCurveTable.getParameterSpec("secp256r1");
ECPoint q = curve.getG().multiply(privateKey.getS()).normalize();
ECPublicKey publicKey = (ECPublicKey) KeyFactory.getInstance("EC", "BC")
.generatePublic(new ECPublicKeySpec(q, curve));
ECKey ecKey = new ECKey.Builder(Curve.P_256, publicKey).privateKey(privateKey).build();
// 4. Build DPoP proof JWT
byte[] tokenHash = MessageDigest.getInstance("SHA-256")
.digest(accessToken.getBytes(StandardCharsets.UTF_8));
String ath = Base64URL.encode(tokenHash).toString();
JWTClaimsSet payload = new JWTClaimsSet.Builder()
.jwtID(UUID.randomUUID().toString())
.issueTime(Date.from(Instant.now()))
.claim("htm", httpMethod.toUpperCase())
.claim("htu", apiUrl)
.claim("ath", ath)
.build();
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.parse(signingAlg))
.type(new JOSEObjectType("dpop+jwt"))
.jwk(ecKey.toPublicJWK())
.build();
JWSObject jws = new JWSObject(header, new Payload(payload.toJSONObject()));
jws.sign(new DefaultJWSSignerFactory().createJWSSigner(ecKey, header.getAlgorithm()));
String dpopProof = jws.serialize();
// 5. Send the IAM request
// Authorization: DPoP <accessToken>
// DPoP: <dpopProof>
}
}

///

/// tab | C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using Jose;
// 1. Your values
var accessToken = "<dpop-bound-access-token>";
var apiUrl = "https://cloud.stage.api.trimblecloud.com/platform/iam/v1/users";
var httpMethod = "GET";
// 2. Load private key (same EC P-256 key from TID Phase 1)
using var privateKey = ECDsa.Create();
privateKey.ImportFromPem(File.ReadAllText("dpop_private_key.pem"));
// 3. Derive public JWK from the private key
var pub = privateKey.ExportParameters(false);
var publicJwk = new Dictionary<string, object>
{
["kty"] = "EC",
["crv"] = "P-256",
["x"] = Base64Url.Encode(pub.Q.X!),
["y"] = Base64Url.Encode(pub.Q.Y!),
};
// 4. Build DPoP proof JWT
var ath = Base64Url.Encode(SHA256.HashData(Encoding.UTF8.GetBytes(accessToken)));
var extraHeaders = new Dictionary<string, object>
{
["typ"] = "dpop+jwt",
["jwk"] = publicJwk,
};
var dpopProof = JWT.Encode(
new Dictionary<string, object>
{
["jti"] = Guid.NewGuid().ToString(),
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
["htm"] = httpMethod.ToUpperInvariant(),
["htu"] = apiUrl,
["ath"] = ath,
},
privateKey,
JwsAlgorithm.ES256,
extraHeaders);
// 5. Send the IAM request
// using var client = new HttpClient();
// client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("DPoP", accessToken);
// client.DefaultRequestHeaders.TryAddWithoutValidation("DPoP", dpopProof);
// var response = await client.GetAsync(apiUrl);

///

Rule Detail
Fresh proof per request New jti every call — reuse causes 400
Same key pair throughout Rotating keys after token issuance causes 401
Include ath for IAM only Omit ath when calling TID token endpoint
Use Authorization: DPoP DPoP-bound token with Bearer scheme is rejected
Scenario error_code HTTP Fix
DPoP header missing invalid_dpop_proof 400 Add DPoP header to every request
typ not dpop+jwt invalid_dpop_proof 400 Set typ exactly to dpop+jwt
Unsupported alg invalid_dpop_proof 400 Use a supported algorithm mentioned in Step 1 headers
jwk missing in header invalid_dpop_proof 400 Embed public JWK in the JWT header
htm or htu missing invalid_dpop_proof 400 Include both claims in the payload
htm mismatch invalid_dpop_proof 400 Ensure htm matches the HTTP method of the request
htu mismatch invalid_dpop_proof 400 Set htu to the full public URL you are calling (scheme + host + path)
ath missing invalid_dpop_proof 400 Include ath for IAM resource API calls
Proof older than 5 min invalid_dpop_proof 400 Generate a fresh proof before the request
Proof issued in the future invalid_dpop_proof 400 Ensure iat is current (IAM allows ±30 s clock skew)
jti replayed invalid_dpop_proof 400 Use a new UUID per request — never reuse
ath mismatch invalid_dpop_proof 401 Hash the exact raw JWT in Authorization: DPoP <token> (base64url, no padding)
cnf.jkt mismatch invalid_token 401 Use the same private key from token issuance
DPoP token with Bearer invalid_token 401 Use Authorization: DPoP