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.
Prerequisites
Section titled “Prerequisites”- 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. - A DPoP-bound access token with a
cnf.jktclaim matching your public key thumbprint.
End-to-End Flow
Section titled “End-to-End Flow”
Step 1 — Build the DPoP Proof JWT
Section titled “Step 1 — Build the DPoP Proof JWT”A DPoP proof is a signed JWT. Generate a fresh proof for every request (single-use, expires in 5 minutes).
Header
Section titled “Header”| 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 |
Payload
Section titled “Payload”| 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 |
Step 2 — Send the Request
Section titled “Step 2 — Send the Request”| Header | Value |
|---|---|
Authorization |
DPoP <access_token> |
DPoP |
<dpop_proof_jwt> |
Example:
GET /iam/v1/users HTTP/1.1Host: cloud.stage.api.trimblecloud.comAuthorization: DPoP eyJhbGciOiJSUzI1NiIs...DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs...Step 3 — What IAM Validates
Section titled “Step 3 — What IAM Validates”- Proof header (
typ,alg,jwk) and signature athmatches the access token hashcnf.jktin the token matches the proofjwkthumbprint (RFC 7638)htmandhtumatch the requestiatis within 5 minutes;jtihas not been replayed
Code Examples
Section titled “Code Examples”Generate a DPoP proof the same way as TID Phase 1 — but for IAM, with two differences:
- Set
htmandhtuto the IAM API call you are making. - Add
ath—base64url(SHA-256(access_token)), whereaccess_tokenis 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, uuidfrom cryptography.hazmat.primitives import serialization
# 1. Your valuessigning_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 keypub = 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 JWTath = 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 valuesvar 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 keyvar 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 JWTvar 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);///
Key Rules
Section titled “Key Rules”| 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 |
Error Reference
Section titled “Error Reference”| 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 |