# DPoP — sender-constrained tokens

DPoP (Demonstrating Proof-of-Possession,
[RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449)) binds an access
token to a private key that only your application holds. A token obtained with
DPoP is useless to anyone who does not also hold your key, so a leaked token
cannot be replayed. **DPoP is optional.** If you do not send a DPoP proof, you
get an ordinary `Bearer` token, which is fine for most integrations.

Use DPoP when your security policy requires proof-of-possession — for example,
when tokens travel across less-trusted networks.

## How it works

1. Your application holds an asymmetric **key pair**.
2. With each request, you send a short-lived **DPoP proof**: a JWT signed with
your private key. Its header carries the matching public key (JWK).
3. Hyperproof ties the access token to the thumbprint of that key
(`cnf.jkt`). Each call after establishing the key pair must present a fresh
proof signed by the same key.


Supported proof-signing algorithms: **ES256, ES384, ES512, RS256, PS256**
(ES256 is recommended). A proof is valid for **60 seconds**, and each proof's
`jti` may be used only once.

## The proof JWT

**Header**

| Field | Value |
|  --- | --- |
| `typ` | `dpop+jwt` |
| `alg` | your signing algorithm, e.g. `ES256` |
| `jwk` | your **public** key (public members only) |


**Claims**

| Claim | Meaning |
|  --- | --- |
| `htm` | HTTP method of the request, e.g. `POST` |
| `htu` | HTTP URL of the request (without query or fragment) |
| `iat` | issued-at time (Unix seconds) |
| `jti` | unique random id (replay protection) |
| `ath` | base64url SHA-256 hash of the access token — **required on API calls**; omit it on the token request |


## Step 1 — Get a DPoP-bound token

Send a proof for the token endpoint (`htm=POST`, `htu=…/oauth/token`, no
`ath`) in the `DPoP` header of the token request. The response `token_type`
comes back as `DPoP`.

```python
# Python — using PyJWT (cryptography backend)
import jwt, time, uuid, hashlib, base64
from cryptography.hazmat.primitives.asymmetric import ec

private_key = ec.generate_private_key(ec.SECP256R1())
public_numbers = private_key.public_key().public_numbers()

def b64u(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode()

jwk = {"kty": "EC", "crv": "P-256",
       "x": b64u(public_numbers.x.to_bytes(32, "big")),
       "y": b64u(public_numbers.y.to_bytes(32, "big"))}

def dpop_proof(htm, htu, access_token=None):
    claims = {"htm": htm, "htu": htu, "iat": int(time.time()), "jti": uuid.uuid4().hex}
    if access_token:
        claims["ath"] = b64u(hashlib.sha256(access_token.encode()).digest())
    return jwt.encode(claims, private_key, algorithm="ES256",
                      headers={"typ": "dpop+jwt", "jwk": jwk})

token_url = "https://api.expent.ai/tprm-api/oauth/token"
# POST token_url with header DPoP: dpop_proof("POST", token_url)
# and form grant_type=client_credentials  ->  token_type "DPoP"
```

## Step 2 — Call the API with the DPoP token

On every request, send **both** the token (with the `DPoP` auth scheme, not
`Bearer`) **and** a fresh proof that includes `ath`:

```bash
curl -X POST https://api.expent.ai/tprm-api/vendors/search \
  -H "Authorization: DPoP ACCESS_TOKEN" \
  -H "DPoP: <proof with htm=POST, htu=…/vendors/search, ath=SHA256(ACCESS_TOKEN)>" \
  -H "Content-Type: application/json" -d '{}'
```

A missing or invalid proof returns `401` with client_code `auth.invalid_dpop`.

> **Tip:** In the interactive reference, every operation shows ready-to-run
request samples in curl, Python, JavaScript, and more. The samples are
generated automatically from the spec.