I recently wrote a sentence I need to correct: “DPoP stops replay. The gap most teams miss is revocation lag once the token walked.” The second half points at a real problem. The first half is too loose. If only the token walked, a correctly implemented DPoP flow should make it useless to the thief. The harder problem begins when the attacker has the token and its proof key, can execute inside the client process, controls the device, or is already riding the live session. At that point the question is no longer who holds the token. It is how quickly the whole system can stop honoring its authority.
This distinction matters because token theft and stale authority are separate security axes. Sender-constraining makes a credential harder to carry away. Revocation is what happens after the credential, client or underlying decision can no longer be trusted. One mechanism limits portability. The other has to change reality across a distributed system.
A token can be cryptographically valid and operationally unauthorized.
First, what DPoP actually buys
A bearer token works like cash: whoever presents it can spend it. The resource server validates the signature, issuer, audience, expiry and scope, but it does not know whether the presenter is the client to which the token was issued. A token copied from a log, browser, proxy or telemetry pipeline can be replayed from another machine.
DPoP, standardized in RFC 9449, changes that. The client signs a short-lived proof for each request with a private key. The proof binds the request to an HTTP method, target URI, timestamp, unique identifier and, when an access token is present, a hash of that token. The authorization server binds the token to the public half of the same key. A thief who steals only the token cannot create the matching proof.
That is a major improvement, but the boundary is precise:
- Access token only: DPoP helps substantially. The token is no longer portable by itself.
-
Token and one captured proof: the proof is constrained to a method and URI, and servers can limit its lifetime, reject duplicate
jtivalues and require a nonce. - Token and private proof key: the attacker can present as the client.
- Client process, device or active session: the attacker may not need to extract the key at all. Malicious code can ask the legitimate signing context to produce valid proofs.
RFC 9449 says this directly: if hostile code runs in the client execution context, DPoP security is no longer guaranteed. A non-exportable key still prevents offline theft, but it does not prevent an attacker inside the process from using the key while the client is online. DPoP solves unauthorized token portability. It does not solve stale authority after the ability to execute has been compromised.
A signature proves history, not current intent
Self-contained JWT access tokens are attractive because an API can validate them locally. No network round trip to the authorization server. No central dependency in the hot path. Low latency, high availability, clean horizontal scaling.
The trade-off is hidden in the word self-contained. A signature proves that a trusted issuer made a statement and that the statement has not been altered. It does not prove that the issuer still wants the statement honored now. If a resource server validates only the JWT’s signature and embedded claims, a token can remain acceptable until exp even after the account is disabled, the role is removed or the device is declared compromised.
This is why JWT revocation is not a cryptographic operation. The token does not need to become malformed; the enforcement point needs fresher authorization state than the token contains.
RFC 7009 defines the OAuth token-revocation endpoint. It also acknowledges the operational gap: invalidation can have a propagation delay during which some servers know about the revocation and others do not. An HTTP 200 response says only that the endpoint processed the request without reporting an error — the RFC deliberately returns the same response for an invalid token. It does not prove that a particular token existed, or that every resource server has stopped accepting the authority.
This gives us two separate events:
- Revocation decision: the control plane records that a token, session, key, device or grant must no longer be trusted.
- Revocation enforcement: every covered gateway, API, region, cache, long-lived connection and worker actually rejects it.
Security lives in the interval between them.
The four planes that have to converge
“The token is revoked” compresses four different systems into one sentence. Treating them separately makes both architecture reviews and incidents much clearer:
- Decision plane: a person, policy engine or identity provider decides that a token, grant, session, device, workload or key is no longer trusted. This is where the reason, scope and effective time of the decision live.
- Distribution plane: the changed decision travels through event streams, introspection responses, replicated deny state, configuration stores or cache invalidations. This plane determines how quickly every region can learn the truth.
- Enforcement plane: gateways, APIs, sidecars, workers and connection handlers consult that truth and deny the next use. This is where fail-open behavior, stale caches and inconsistent policy versions become exploitable.
- Effects plane: work already admitted is stopped or contained: queued jobs are cancelled, streams are closed, delegated credentials are invalidated and irreversible side effects are prevented where still possible.
An implementation can succeed in the first plane and fail in the other three. The database row can say revoked_at=now() while a second region serves a cached active response, an already-authorized WebSocket continues carrying commands and a worker completes a privileged export. The endpoint is healthy. The security outcome is not.
This model also prevents a common category error: detection time and revocation time are not the same metric. If compromise begins at t0, the system detects it at t1, records the decision at t2 and the last covered path stops accepting authority at t3, then there are three separate intervals:
- Detection latency: t1 − t0.
- Decision latency: t2 − t1.
- Revocation Completion Time: t3 − t2.
The full compromise-to-denial window is t3 − t0. Improving event propagation does not compensate for an hour of detection latency, and faster detection does not help if enforcement then converges slowly. Report the intervals separately or the aggregate will hide which control actually failed.
Revocation is a distributed convergence guarantee
The useful metric is not “the revoke endpoint returned in 80 milliseconds.” I call the operational measure Revocation Completion Time: the time from the central revocation decision until the last execution path covered by policy is confirmed unable to exercise that authority.
In shorthand:
RCT = last covered enforcement point confirmed denying authority − central revocation decision
That last enforcement point is where architecture becomes threat model. It may be an API gateway with a local deny cache, a service mesh sidecar, a second region, a partner API, a WebSocket authorized only at connection time, or a background worker that accepted a job before the revocation event arrived.
RCT is meaningful only with a declared coverage set. “All enforcement points” is not an inventory. Name the gateways, services, regions, connection types, queues and delegated systems that are inside the guarantee. Then track convergence as a function of time:
Coverage(t) = covered enforcement points denying the authority at time t ÷ all enforcement points in policy scope
A dashboard showing 99% coverage can still represent failure if the missing 1% is the payments API. For that reason, completion should not be only a fleet-wide percentage. Partition it by risk class and require 100% of critical paths even if a lower aggregate threshold is acceptable for low-risk reads.
The service-level objective should state at least four things: the revocation subject, the coverage set, the percentile and the operation class. “p99 RCT under five seconds for privileged writes across every first-party region” is testable. “Tokens revoke quickly” is marketing copy.
OAuth token introspection in RFC 7662 can provide fresh central state: the resource server asks whether a token is active. But introspection adds a network dependency, so production systems cache the answer. The moment they do, the cache TTL becomes part of the security model. A sixty-second cache is not merely a performance setting. It is permission for a revoked decision to remain usable for up to another minute.
Push changes the transport, not the guarantee. OpenID CAEP 1.0 standardizes events such as session revoked, token claims change, credential change, device compliance change and risk level change. That is valuable: relying parties can react to changing risk without polling on every request. But an event can be delayed, duplicated, reordered or missed. A receiver can be offline. Enforcement can fail open. Push revocation still needs acknowledgement, reconciliation and testing.
The token is only the root of the problem
Revoking one string is often the easiest part. The harder question is what that string already created.
A compromised session may have minted refresh tokens, exchanged an access token for a delegated token, created an API key, opened a WebSocket, scheduled an export, started a background workflow, added a new administrator or issued a presigned download URL. Revoking the original token does not delete copied data, cancel a running job or remove persistence the attacker created while the token was valid.
Authority therefore forms a graph:
session → refresh-token family → access tokens → delegated credentials → jobs and side effects
A real kill switch needs a defined closure over that graph. Does “revoke this agent” mean one access token, one run, one device, every token in the family, every subagent, every delegated capability, every active job, or all credentials created by the run? If the answer is not explicit, the endpoint can work exactly as implemented while the incident continues through a child edge nobody remembered.
This is becoming active standards work. A current IETF Internet-Draft on attenuating authorization tokens for agentic delegation chains explores holder-derived tokens whose authority can only narrow at each hop. It is a draft, not a finished standard, but the direction is correct: agent authority has lineage, and the enforcement point needs to verify the whole chain rather than trust ambient credentials inside the agent runtime.
AI agents turn seconds into actions
For a human session, a thirty-second revocation delay may permit two or three meaningful actions. An agent can make hundreds of tool calls in the same window, create subagents and continue work asynchronously. The identity mechanism did not become weaker. The execution rate multiplied the cost of its stale window.
This is the part of AI agent security that “use short-lived credentials” understates. Time limits exposure only when the rate and impact of actions inside that time are also bounded.
I call that cost Revocation Exposure:
Revocation Exposure = Revocation Completion Time × accepted action rate × average irreversible impact
If impact cannot be normalized honestly, drop the last term and report actions at risk. That is still more useful than a naked TTL. At 0.1 meaningful actions per second, a thirty-second window exposes roughly three actions. At 100 accepted actions per second, it exposes 3,000. A fifteen-minute access token at the same rate is not “short-lived” in any operational sense. It contains room for 90,000 actions.
A worked example: four architectures, one agent
Assume an agent can sustain twenty accepted privileged actions per second after compromise. These are illustrative bounds, not benchmark results. The point is to expose what each architecture is promising:
- Local JWT validation with a five-minute remaining lifetime: the worst stale window is 300 seconds, so as many as 6,000 actions can be accepted before expiry. The API remains highly available, but the security bound is the remaining token lifetime.
- Introspection with a sixty-second active-response cache: the theoretical cache window permits up to 1,200 actions. Central revocation is fresher than local expiry, but the cache TTL has become an authorization budget.
- Push invalidation with a measured five-second p99: roughly 100 actions fit in the p99 window. That is a large improvement, provided missing events are detected by acknowledgements and reconciliation rather than assumed away.
- Fresh authorization on each irreversible action: stale acceptance can approach zero after the source-of-truth decision, but the check is now in the critical path. Availability, latency and failure semantics must be designed explicitly.
The example shows why “short-lived” is not a security level. Five minutes may be conservative for a human reading documents and catastrophic for an agent changing infrastructure at machine speed. The correct bound combines time with execution capacity.
For agents, token lifetime is not measured in minutes. It is measured in how many irreversible actions fit inside the window.
This is the same correction I make when measuring autonomy. A raw percentage is not enough; you need to adjust it for the gates that determine whether the work is safe to accept. Proof-Adjusted Autonomy measures the deployment side of that problem. Revocation Exposure measures the emergency brake: after trust changes, how much machine execution can still get through?
A May 2026 preprint on heartbeat-bound hierarchical credentials for agent swarms describes descendants that can continue privileged execution after an operator shutdown as “zombie agents.” The proposed protocol is research, not established practice, but the problem statement is exactly right: hierarchical agents turn revocation from a single credential lookup into bounded shutdown of a living execution tree.
Can a JWT be revoked immediately?
Not by changing the bytes already issued. A signed, self-contained JWT is immutable. “Immediate JWT revocation” means introducing current state at the enforcement point so that the otherwise-valid token is rejected. There are several ways to do it, each moving cost to a different place:
- Wait for expiry: no revocation dependency in the request path, but the remaining TTL is the stale-authority window. This is acceptable only when the action budget inside that window is acceptable.
- Introspect: ask an authorization service whether the token is active. This can make policy fresh, but adds latency and an availability dependency. Caching restores performance by reintroducing a bounded stale window.
- Check deny state: distribute revoked token IDs, session IDs, key thumbprints or grant versions to enforcement points. The read can be local and fast, but the deny state must be propagated, retained for the relevant token lifetime and reconciled after partitions.
- Version the authority: put a session, subject or policy version in the token and compare it with current state. One version bump can invalidate a family of tokens, but every verifier still needs sufficiently fresh version data.
- Push a security event: notify relying parties through a framework such as CAEP and update local enforcement immediately. This reduces normal-path latency but requires delivery guarantees, receiver health, acknowledgements and replay-safe processing.
The right answer is usually risk-tiered. Low-impact reads can tolerate a small stale budget and keep local verification. Privileged writes can require a fresh decision. Credential issuance, permission changes, money movement and destructive actions should normally fail closed when current trust cannot be established.
That budget deserves a name even if it does not need a brand: the stale authorization budget is the maximum time or number of actions for which an old allow decision may remain usable after current policy changes. It should be set per operation class, not inherited accidentally from a cache default.
Revocation is a kill graph, not a deny list
A flat deny list answers whether one identifier is blocked. An incident responder needs to know which authorities descended from the compromised root and which work they admitted. Model nodes such as user sessions, device sessions, workload identities, refresh-token families, access tokens, exchanged tokens, API keys, agent runs, subagents, queues and long-lived connections. Model edges such as issued, delegated, exchanged, spawned, scheduled and opened.
Then define revocation policies as graph operations:
- Leaf revocation: deny one access token because only that token leaked.
- Family revocation: invalidate a refresh-token family and every access token derived from it after reuse or session compromise.
- Run revocation: stop one agent run, its descendants, active tool leases and queued work without disabling the human principal.
- Device or key revocation: invalidate every session and token bound to a compromised device or proof key.
- Subject revocation: disable the account and close the full descendant set across issuers and services.
The graph also forces a distinction between authority and effects. You can prevent a token from authorizing the next request. You cannot cryptographically un-send an email, un-disclose a secret or un-transfer money. Compensation, cancellation and quarantine belong in the effects plane. Systems that perform irreversible work need idempotency keys, delayed commit boundaries or approval gates because revocation cannot reverse physics.
What I would build in production
No single control makes authority both fully autonomous and instantly changeable. The practical architecture is layered:
- Use short-lived, audience-restricted access tokens. Expiry is not revocation, but it caps the worst stale window when every online mechanism fails.
- Sender-constrain credentials. Use DPoP where application-layer proof fits; use mTLS for controlled service-to-service or B2B workloads. Bind or rotate refresh tokens as recommended by the OAuth 2.0 Security Best Current Practice.
- Issue identity per agent, task and tool. Shared API keys erase attribution and force revocation to take down every workload on the key.
- Keep scopes and action budgets narrow. “May update files A and B on branch X for issue Y, for ten minutes, without merging” is safer than “may write to GitHub.”
- Push risk changes, then reconcile. Distribute revocation events quickly, track acknowledgements and periodically compare local enforcement state with the source of truth.
- Require fresh policy for irreversible operations. Reading a low-risk object may tolerate cached state. Changing permissions, sending money, deleting data or minting a new credential should fail closed and use a fresh check or step-up authorization.
- Model descendant authority. Record token families, delegation parents, run IDs, device IDs and key thumbprints so one incident action can close the intended graph.
- Stop work, not only requests. Revocation must reach queues, active jobs, streaming sessions and credentials created by the compromised run.
The architecture above does not promise zero delay. It makes delay bounded, visible and proportional to risk.
Failure semantics are part of the authorization policy
Every online revocation mechanism eventually meets a partial failure. The dangerous behavior is usually not hidden in OAuth; it is hidden in what the service does when the supporting component is slow or unavailable.
- The introspection service times out: does a high-risk write fail closed, reuse the last active response or silently fall back to local JWT validation?
- The deny-state store is unavailable: can the service distinguish “not revoked” from “revocation status unknown”?
- A push event is dropped: is there a sequence number, missed-event detection and backfill, or does the receiver remain stale forever?
- A receiver was offline: does it reconcile before serving traffic, or start with an empty local cache and accept everything?
- Clocks disagree: which timestamp defines the decision, how much skew is tolerated and can a fast or slow clock extend acceptance?
- A WebSocket was authorized before revocation: are sensitive messages re-authorized, and can the control plane terminate the connection?
- A job is already queued or running: is authority rechecked at execution and before commit, or only when the job was submitted?
- The compromised run created a new API key: is that key connected to the incident graph, or has it become independent persistence?
Fail-open versus fail-closed is not one global switch. It should follow the operation’s reversibility. If revocation state is unavailable, a service may serve a public document while refusing to transfer funds or modify access control. The policy must be explicit enough that an outage does not quietly turn a five-second revocation SLO into the full token lifetime.
Test the emergency brake under failure
Most teams test revocation by asserting that POST /revoke returns 200. That tests the control-plane API, not the security property.
To measure token-revocation latency, the meaningful test issues a credential, exercises it through every region and enforcement path, triggers revocation, and continues sending requests until each path rejects it. It then checks refresh-token reuse, delegated tokens, long-lived connections, queued jobs and credentials created by the original session. Run the same test while delaying the event bus, dropping one notification, restarting a service, skewing a clock, taking the deny store offline and making introspection unavailable.
Report p50, p95, p99 and maximum RCT by operation class. The mean is almost decorative. An attacker needs only the slowest usable path. Then multiply the tail by the action rate and report Revocation Exposure in actions at risk or expected impact.
A useful test produces an evidence bundle, not only a chart:
- the exact credential and authority graph created for the scenario;
- the central decision timestamp from an authoritative clock;
- the last accepted and first rejected request for every covered path;
- event offsets, cache ages and policy versions observed at each enforcement point;
- the fate of active connections, queued work and descendant credentials;
- the calculated p50, p95, p99, maximum RCT and Revocation Exposure;
- the paths excluded from coverage and the reason they are excluded.
This is chaos engineering for authorization. It converts “we support revocation” from a feature checkbox into a falsifiable system guarantee.
A practical revocation SLO for AI agents
A production SLO should connect security, reliability and agent throughput. One reasonable template is:
For privileged agent writes, 100% of first-party enforcement points must deny the revoked run and its credential descendants within five seconds at p99 and thirty seconds maximum; no irreversible action may be admitted when current revocation state is unknown.
That sentence is intentionally demanding. It identifies the actor, action class, closure, coverage, percentile, hard maximum and failure behavior. A separate objective can permit cached authorization for reversible reads. There is no reason both classes must pay the same latency or accept the same risk.
Add an action-bound objective beside the time objective: “fewer than 100 privileged actions at risk at p99 after the decision.” If the agent’s concurrency or tool-call rate increases, this objective reveals that the old time target is no longer sufficient. The security budget moves automatically with machine speed.
Finally, distinguish normative sources from emerging proposals. RFC 7009, RFC 7662, RFC 9449, RFC 9700 and the final OpenID CAEP specification are standards or finalized specifications that can anchor production design. Attenuating agent tokens and heartbeat-bound hierarchical credentials are drafts or research. They are useful signals about where agent identity is heading, not controls you can claim as standardized today.
A 200 response is an intention, not proof
Sender-constraining is worth deploying. Short TTLs are worth deploying. Introspection, deny state and continuous-access events are worth deploying. The mistake is treating any one of them as the end of the lifecycle.
The security property is not that the authorization server recorded revoked. The property is that every relevant execution path stopped honoring the authority inside its risk budget, including descendants and work already in motion.
That is why token revocation is not an endpoint. It is a distributed convergence guarantee — and AI agents make every unmeasured second more expensive.
Key takeaways
- DPoP makes a stolen token alone far less useful; it does not protect a compromised proof key, client process, device or active session.
- A self-contained JWT can be cryptographically valid while the authorization decision it carries is operationally stale.
- Revocation decision and revocation enforcement are different events; RFC 7009 explicitly acknowledges propagation delay between servers.
- Revocation Completion Time measures when the last covered execution path is confirmed unable to exercise the authority — not how fast the revoke endpoint responds.
- A revocation guarantee spans four planes: decision, distribution, enforcement and effects.
- Revocation is a graph problem: token families, delegated credentials, subagents, queues, long-lived sessions and created credentials all need explicit shutdown semantics.
- Revocation Exposure converts delay into risk: completion time × accepted action rate × average irreversible impact.
- For AI agents, token TTL should be expressed in irreversible actions at risk, not only minutes.
- Failure behavior belongs in policy: high-risk actions should not silently fall back to stale local validation when revocation state is unknown.
- The correct production test is authorization chaos engineering across every region and failure mode, reporting p50, p95, p99 and maximum revocation time.
The old white-hat instinct still applies: do not trust the control because the dashboard says it exists. Make it fail, measure the last path that stays open, and prove the emergency brake works before an agent is moving at machine speed. That is the security side of owning the agent harness.
Top comments (0)