Skip to main content
An agent token (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

Pass cnf when you create the agent. The value is the RFC 7638 thumbprint of the agent’s public key:
Over HTTP, send the same member in the body of POST /agents:
The agent identity returned by 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 sends Authorization: DPoP <token> and a DPoP header. The client helper does both and retries once when the server asks for a nonce:
In your own routes, requireAgent does the same checks:

What gets checked

  • The proof: typ is dpop+jwt, the algorithm is on the allow list, the header holds a public jwk, the signature verifies, htm and htu match the request (query and fragment ignored), iat is inside the window, ath is the SHA-256 of the token, and the jti has not been seen before.
  • The binding: the thumbprint of the proof key equals cnf.jkt on the agent.
  • A bound token sent as Bearer with 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 required is set).
The binding check also sits inside 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:
Only set 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:
The route needs a signed-in caller and only reveals agents inside that caller’s scope. A token that is unknown, revoked, expired or out of scope answers { "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 jkt and x5t#S256 needs 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 cnf value in the database is treated as a binding nobody can satisfy. It never turns into an unbound token.
This is also the hook the device features use for IoT clients: a device that holds a certificate or a key can be bound the same way.

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 iat values inside the skew window. Use requireNonce and 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: true plus either method gives you that property for agent tokens. It does not by itself make a deployment FAPI conformant.
Last modified on October 19, 2026