ta_...) is a bearer credential. Whoever holds the string can use it. Binding it to a key fixes that: the token stores the thumbprint of a key the agent holds, and every request must carry a short-lived proof signed with that key. A token copied out of a log file stops working.
This page covers the REST authorize route and requireAgent. For tokens issued by the MCP OAuth server, see DPoP and requireMcpAuth. Both use the same proof format and the same cnf claim (RFC 7800).
Binding is opt-in per agent. A token without a binding behaves exactly as before.
Turn it on
With more than one instance, point
secondaryStorage at the database or Redis. Otherwise the replay cache lives in each process and a proof could be replayed against another instance.
Bind a token to a key
Passcnf when you create the agent. The value is the RFC 7638 thumbprint of the agent’s public key:
POST /agents:
agent.get and agent.list shows cnf. It is part of the public identity, not a secret.
Rotating a token keeps its binding by default, so a leaked token is replaced by one the thief cannot use either. To move the agent to a new key, or drop the binding:
POST /agents/:id/rotate takes the same cnf in an optional JSON body. PATCH /agents/:id does not change a binding.
Call with a proof
The agent sendsAuthorization: DPoP <token> and a DPoP header. The client helper does both and retries once when the server asks for a nonce:
requireAgent does the same checks:
What gets checked
- The proof:
typisdpop+jwt, the algorithm is on the allow list, the header holds a publicjwk, the signature verifies,htmandhtumatch the request (query and fragment ignored),iatis inside the window,athis the SHA-256 of the token, and thejtihas not been seen before. - The binding: the thumbprint of the proof key equals
cnf.jkton the agent. - A bound token sent as
Bearerwith no proof is refused. A bound token is never usable as a plain bearer token. - A token with no binding ignores any proof header and works as before (unless
requiredis set).
authorizeByToken, not only in the HTTP layer. A caller who skips the HTTP handler and calls theauth.authorizeByToken(token, request) on a bound token gets confirmation_required. Pass the proofs your own transport verified through the context:
confirmation from data you verified yourself.
Errors
Refused bound tokens are written to the audit log with the decision code, so a pattern of stolen-token attempts is visible.
Introspection
POST /agents/introspect with { "token": "ta_..." } answers in the shape of RFC 7662 and includes cnf, so a resource server that checks tokens itself can verify the proof:
{ "active": false }. In code: await theauth.agent.introspect(token). Introspection does not see your request, so the resource server still has to verify the proof itself.
Client certificates and other proofs
The binding is a map.jkt is the DPoP member. x5t#S256 is the member for mutual TLS (RFC 8705): the thumbprint of the client certificate. theAuth does not terminate TLS, so you tell it how to read the proof from a request. Typically your proxy forwards the verified certificate thumbprint in a header:
- When a token is bound to several members, every one must match. A token with
jktandx5t#S256needs both a DPoP proof and the certificate. - A token bound to a member that has no extractor is always refused.
- An extractor that throws counts as proof of nothing.
- A damaged
cnfvalue in the database is treated as a binding nobody can satisfy. It never turns into an unbound token.
Limits
- DPoP protects against a copied token. It does not help if the attacker can run code inside the agent process and use the key through it (RFC 9449 section 11).
- Without nonces, a stolen key can pre-sign proofs with future
iatvalues inside the skew window. UserequireNonceand short token lifetimes if that is in your threat model. - FAPI 2.0 asks authorization servers to issue only sender constrained tokens and offers DPoP or mutual TLS for it.
required: trueplus either method gives you that property for agent tokens. It does not by itself make a deployment FAPI conformant.