---
title: "Bind long-lived agent tokens to a key with · Agent Wranglers"
url: https://agentwranglers.se/playbook/agents-as-users/delegated-access-and-consent/dpop-bound-agent-tokens
id: AGT-41
level: Recommended
applies: public-api
updated: 2026-10-27
build: a2a6bdfed830
tokens: 1066
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Bind long-lived agent tokens to a key with DPoP

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Delegated access and consent](https://agentwranglers.se/playbook/agents-as-users/delegated-access-and-consent.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-27.

- ID: AGT-41
- Level: Recommended
- Chapter: [Delegated access and consent](https://agentwranglers.se/playbook/agents-as-users/delegated-access-and-consent.md)
- Rule: Accept DPoP proofs at your token endpoint and API. A token issued with a proof carries the key's thumbprint in `cnf.jkt`, and every call with it needs a fresh proof signed by that key.
- Check: Get a DPoP-bound token for a test agent. A call with the token and no proof gets 401. A proof for another URL gets 401. The same proof sent twice works once.
- Applies when: The project offers an API to others.
- How: Agents run on servers for hours, and their tokens land in logs, traces and prompt transcripts. A bearer token works for anyone who copies it. A DPoP token works only with the private key, which never leaves the agent. Check each proof's `htm` and `htu` against the request, its `iat` within 60 seconds, and its `jti` against a cache of recent ones. Require `ath`, the hash of the access token, in every proof sent to the API. Answer a missing or wrong proof with 401 and `WWW-Authenticate: DPoP`. The MCP roadmap lists DPoP as work in progress, so keep plain bearer tokens working for MCP clients until the specification adopts it. Keep the tokens short as well, as [Give agent grants short lives](https://agentwranglers.se/playbook/agents-as-users/delegated-access-and-consent/short-agent-grants.md) says.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
import { calculateJwkThumbprint, jwtVerify, importJWK, decodeProtectedHeader } from 'jose';
const PROOF_MAX_AGE_S = 60;

export async function checkDpop(proof, token, method, url, seenJti) {
  const { jwk } = decodeProtectedHeader(proof);
  const { payload } = await jwtVerify(proof, await importJWK(jwk), { typ: 'dpop+jwt', maxTokenAge: PROOF_MAX_AGE_S });
  const fresh = await seenJti.addIfAbsent(payload.jti, PROOF_MAX_AGE_S);
  return fresh && payload.htm === method && payload.htu === url
    && payload.ath === sha256Base64url(token.raw) && token.cnf?.jkt === await calculateJwkThumbprint(jwk);
}
```

### Python example

```python
import time
import jwt
from jwt.algorithms import ECAlgorithm

PROOF_MAX_AGE_S = 60

def check_dpop(proof: str, token: dict, method: str, url: str, seen_jti) -> bool:
    jwk = jwt.get_unverified_header(proof)["jwk"]
    claims = jwt.decode(proof, ECAlgorithm.from_jwk(jwk), algorithms=["ES256"])
    fresh = abs(time.time() - claims["iat"]) <= PROOF_MAX_AGE_S and seen_jti.add_if_absent(claims["jti"], PROOF_MAX_AGE_S)
    return (fresh and claims["htm"] == method and claims["htu"] == url
            and claims["ath"] == sha256_base64url(token["raw"]) and token["cnf"]["jkt"] == jwk_thumbprint(jwk))
```

## Sources

- [RFC 9449 (OAuth 2.0 Demonstrating Proof of Possession)](https://www.rfc-editor.org/rfc/rfc9449)

## Related rules

- AGT-10 [Give agent grants short lives](https://agentwranglers.se/playbook/agents-as-users/delegated-access-and-consent/short-agent-grants.md) in Agents as users

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/agents-as-users/delegated-access-and-consent/dpop-bound-agent-tokens">Agent Wranglers agents as users playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Require PKCE and exact redirect addresses for MCP clients](https://agentwranglers.se/playbook/agents-as-users/delegated-access-and-consent/pkce-and-exact-redirects.md)
- [Delegated access and consent](https://agentwranglers.se/playbook/agents-as-users/delegated-access-and-consent.md)
