Generate a DPoP Proof and Obtain a DPoP-Bound Access Token
This guide walks through generating a DPoP proof JWT and using it to obtain a DPoP-bound access token from Trimble Identity.
For conceptual background on DPoP, cryptographic bindings, and resource-server validation, see Implementing DPoP for Enhanced API Security.
Step 1: Generate an Asymmetric Key Pair
The client application generates an asymmetric cryptographic key pair to be used with DPoP. Supported signing algorithms are specified in the well-known configuration for OpenID Connect (OIDC).
- The private key is kept secret by the client.
- The public key will be included in the DPoP proof JWT.
- A new key pair should be generated for each access token request.
Example: Key Generation (Python)
from cryptography.hazmat.primitives.asymmetric import ec
# Generate EllipticCurve P-256 keyskey_type = "EC"key_crv = "P-256"signing_alg = "ES256"keys = ec.generate_private_key(ec.SECP256R1())pub = keys.public_key()Step 2: Create the DPoP Proof JWT
A DPoP proof is a standard JWT with claims and headers relevant to the DPoP flow, signed by the private key generated in Step 1.
The JWT consists of a header and payload with information about the token request. Once created, the JWT is signed by the private key from Step 1.
The DPoP Proof: Structure and Integrity
The DPoP proof is a meticulously structured JSON Web Signature (JWS), signed by the client’s private key.
2.1. JOSE Header: Cryptographic Manifest
The JSON Object Signing and Encryption (JOSE) Header defines the critical cryptographic parameters. It MUST contain the following parameters to ensure proof integrity and identification:
| Parameter | Requirement | Business Impact / Validation |
|---|---|---|
| typ | dpop+jwt (Mandatory) | Enables immediate server identification and prevents the misuse of other JWT types. |
| alg | Asymmetric Algorithm | Enforces true proof-of-possession. Symmetric algorithms are forbidden as they defeat the purpose. |
| jwk | Public Key in JWK format (Mandatory) | The public key is embedded for signature validation. This design choice is critical for self-contained validation, eliminating the need for external key lookups. |
Supported asymmetric algorithms include ES256, ES384, ES512, RS256, RS384, RS512, PS256, PS384, PS512 and EdDSA.
2.2. Payload Claims: Request and Anti-Replay Binding
The payload claims tie the proof to a single, specific HTTP transaction and are central to preventing replay attacks.
| Claim | Requirement | Security Function |
|---|---|---|
| jti (JWT ID) | Always Mandatory | A unique identifier used by the server to detect and prevent replay attacks. (Lifetime: 5 minutes) |
| htm (HTTP Method) | Always Mandatory | Binds the proof to the request’s HTTP method (e.g., POST), preventing method reuse. |
| htu (HTTP URI) | Always Mandatory | Binds the proof to the request’s URI (excluding query/fragment parts). |
| iat (Issued At) | Always Mandatory | Timestamp for “freshness” checks, mitigating delayed replay. |
| ath (Access Token Hash) | Conditional | Mandatory only for requests to protected resources. This is the Proof-to-Token binding. |
Signature
Once the DPoP JWT is created, the client application signs it with the private key associated with the public key included in the JWT header (jwk claim).
Example: Creating a DPoP Proof JWT (Python)
from cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric import ecimport base64import datetimeimport jsonimport jwtimport mathimport uuid
TID_MTLS_TOKEN_ENDPOINT = "https://mtls.stage.id.trimblecloud.com/oauth/token"
# Key generation (repeated from Step 1)key_type = "EC"key_crv = "P-256"signing_alg = "ES256"keys = ec.generate_private_key(ec.SECP256R1())pub = keys.public_key()
## Encode public key as required for JWK## (JWK specs: https://datatracker.ietf.org/doc/html/rfc7517)
# url-safe-base64-padding-removed encoded 'x' value - BIG-ENDIANec_x_val = pub.public_numbers().x.to_bytes(math.ceil(pub.key_size / 8), 'big')b64_x_val = base64.urlsafe_b64encode(ec_x_val).decode('ascii').rstrip('=')
# url-safe-base64-padding-removed encoded 'y' value - BIG-ENDIANec_y_val = pub.public_numbers().y.to_bytes(math.ceil(pub.key_size / 8), 'big')b64_y_val = base64.urlsafe_b64encode(ec_y_val).decode('ascii').rstrip('=')
ec_key_id = uuid.uuid4().hex
# Construct JWKjwk_headers = { "kty": key_type, "x": b64_x_val, "y": b64_y_val, "crv": key_crv, "kid": ec_key_id}
## Construct JWTjwt_id = uuid.uuid4().hexnow_timestamp = int(datetime.datetime.now().timestamp())
jwt_headers = { "typ": "dpop+jwt", "jwk": jwk_headers}
jwt_payload = { "jti": jwt_id, "iat": now_timestamp, "htm": "POST", "htu": TID_MTLS_TOKEN_ENDPOINT}
private_key_pem = keys.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption())
# Sign JWTencoded_jwt = jwt.encode( jwt_payload, private_key_pem, algorithm=signing_alg, headers=jwt_headers)Example: Decoded DPoP JWT
Header
{ "alg": "ES256", "jwk": { "crv": "P-256", "kty": "EC", "x": "7S0WsYMekxDTCRYK_e-jkH4_ba01LcfFT0PmbMjm6Z-lla4QZap2cOmTtGx5S1_f", "y": "rjJIxzHusvMNEJR4btr09CY_cOafNpZwn1PbqLfIa8BAJPg-HaQ66Gssijx_ErlT" }, "typ": "dpop+jwt"}Payload
{ "jti": "aa2af8c5-da36-4b47-9397-9f539e93470d", "htm": "POST", "htu": "https://mtls.stage.id.trimblecloud.com/oauth/token", "iat": 1768839464}Step 3: Request a DPoP-Bound Access Token from Trimble Identity
The request is sent to the mTLS version of the /token endpoint.
- Endpoint:
POST <baseurl>/oauth/token - Authentication: mTLS for client authentication.
Headers
| Header | Value |
|---|---|
DPoP | <DPoP Proof JWT> (created in Step 2) |
Supported grant types
| Grant Type | Use case |
|---|---|
urn:ietf:params:oauth:grant-type:subject-delegation | |
urn:ietf:params:oauth:grant-type:token-exchange | |
refresh_token |
Step 4: Trimble Identity Validates the DPoP Proof and Returns an Access Token
Trimble Identity receives the request and:
- Verifies the DPoP proof JWT signature using the public key provided in the JWT.
- Validates the
htm,htu,jti, andiatclaims.
If the request is valid, the server creates an access token bound to the public key provided in the JWT.
Access Token Claims
| Claim | Description |
|---|---|
cnf | Values that bind the access token to the client that requested it (sender-constrains the token). |
cnf.x5t#S256 | The SHA-256 thumbprint of the X.509 client certificate (mTLS certificate) used to make the request. |
cnf.jkt | The hash of the public key provided in the DPoP proof. |
Claims relevant to DPoP are listed above. See JWT Definitions for details on additional claims found in the access token.
Step 5: Generate a New DPoP Proof JWT for the Resource Request
Every API request made with a DPoP-bound access token must include a new DPoP proof. This is similar to creating the DPoP proof in Step 2, but with different values.
Using the same keys created in Step 1, generate a new DPoP proof JWT. The payload claims htm and htu must be updated to reflect the API endpoint being called, and a new claim, ath, is included in the payload. The JWT is then signed with the client’s private key.
Additional JWT Payload Claim
| Claim | Description |
|---|---|
ath | Base64url-encoded SHA-256 hash of the access token. Binds the proof to the specific token. |
Step 6: Submit a Request to the Resource Server (via API Cloud)
The request is sent to the resource server through API Cloud.
- Endpoint: Endpoint of the resource server.
Headers
| Header | Value |
|---|---|
DPoP | <DPoP Proof JWT> (created in Step 5) |
Authorization | DPoP <access_token> |
Request Body
As required by the endpoint.
Example Request
GET /executionsHost: https://cloud.stage.api.trimblecloud.com/Processing/api/1Content-Type: application/x-www-form-urlencodedDPoP: <DPoP Proof>Authorization: DPoP <access token>Example Access Token Response
{ "token_type": "DPoP", "expires_in": 3600, "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIi... <truncated for brevity>", "issued_token_type": "urn:ietf:params:oauth:token-type:jwt"}Example Decoded Access Token
Header
{ "alg": "RS256", "kid": "2", "typ": "JWT"}Payload
{ "iss": "https://stage.id.trimblecloud.com", "exp": 1768843090, "nbf": 1768839490, "iat": 1768839490, "jti": "35e9a6e6ea7649bab18e4f3e816bfa8a", "jwt_ver": 2, "sub": "73eb11b5-87f3-4253-ad93-368a2acb4128", "identity_type": "user", "amr": [ "client_credentials", "subject_delegated_access" ], "azp": "6783952d-12f0-4257-ac26-ce16355736c2", "auth_time": 1768839490, "cnf": { "x5t#S256": "f3fe143b5c070763c067f114dd66ae089683e94882a3a5c79f6218f465ef5994", "jkt": "dHU9tQDHNagHvFFHEEZFyUv1eGMNiTsYZzTP_Cvd2wc" }, "data_region": "us", "account_id": "3983a7bc-beaa-54ba-8d7e-ee640c082f3a", "aud": [ "695d741b-2f4a-4cb8-8d97-e81fd4dbd765" ], "scope": "iam", "act": { "sub": "6783952d-12f0-4257-ac26-ce16355736c2" }}Step 7: API Cloud and Resource Server Validate the DPoP Proof
API Cloud receives the request and validates the DPoP proof and access token. If valid, API Cloud forwards the request to the resource server, which performs the same validation:
- Verifies the DPoP proof JWT signature using the public key provided in the JWT (
jwkclaim). - Validates the
htm,htu,jti,iat, andathclaims. - Verifies that the public key in the DPoP proof JWT (
jwkclaim) matches the public key hash in the access token (cnf.jktclaim).
For full details on cryptographic bindings, the complete validation protocol, and error handling, see Implementing DPoP — RS Validation Protocol.