60-sec Short AI Security 50 sec Failure mode + fix

Refresh Token Replay

Someone else is using your AI agent's keys.

Video premiering soon on @AI.JoinDev Subscribe to get it first

Why it works

A refresh token is a bearer secret. Without rotation, a copy works just like the original until it expires.

The fix

Rotate on every refresh. If an old token comes back, revoke the whole family. Bind tokens to the client with DPoP.

Context

AI agents and LLM apps act on a user's behalf through OAuth-connected tools (Gmail, GitHub, Slack, Drive). To keep working without asking the user to log in again, the agent stores a long-lived refresh token and exchanges it at the authorization server for short-lived access tokens. Refresh tokens often live for weeks or months, and agent platforms hold many of them: one per user, per connector.

Architecture

ComponentRoleNotes
AI Agent (OAuth client)Calls tools on the user's behalfStores refresh tokens in a DB / secret store
Refresh tokenLong-lived credential to mint access tokensBearer by default: whoever holds it can use it
Auth serverExchanges refresh token → access tokenValidates signature/expiry, not who presents it
Resource APIsAccept access tokensShort-lived (minutes–1 h)
AttackerObtains a copy of the refresh tokenLogs, backups, debug dumps, compromised plugin, XSS on a web client

Request flow

  1. User connects a tool; the agent receives an access token + refresh token.
  2. When the access token expires, the agent sends the refresh token to the auth server.
  3. The auth server returns a new access token (and, without rotation, the same refresh token stays valid).
  4. The agent keeps calling the tool API.

The failure mode

The refresh token leaks — a request log that captured the token response, a database backup, a tool plugin with too-broad access. The attacker sends it to the token endpoint. Because refresh tokens are bearer credentials and the server doesn't rotate them, the request is indistinguishable from the agent's: it gets valid access tokens. Both the agent and the attacker keep refreshing in parallel, and nothing alerts until the token expires or someone notices data access. Revoking a single access token doesn't help — the attacker simply refreshes again.

Why it happens

A long-lived bearer secret with no rotation and no binding to the client that was issued it.

The fix

  • Refresh token rotation with reuse detection: every refresh returns a new refresh token and invalidates the old one. Tokens from one login form a family. If an already-used token is ever presented again, someone has a copy, so revoke the whole family and force a re-login.
  • Sender-constrained tokens: bind tokens to a key the client holds, using DPoP (RFC 9449) or mutual-TLS (RFC 8705), so a copied token is useless without the private key.
  • Hygiene: never log token responses; encrypt refresh tokens at rest; scope them narrowly; cap absolute lifetime; alert on refreshes from new IPs or ASNs.

Trade-offs

  • Rotation needs care with concurrent refreshes (two workers refreshing at once look like reuse). Allow a short grace window or serialise refreshes per token family.
  • DPoP adds a signed proof to each request and key management on the client.
  • Reuse detection logs the legitimate user out too. That is the point, but it is a UX cost.

Numbers worth knowing

  • The OAuth 2.0 Security Best Current Practice (RFC 9700, Jan 2025) requires refresh tokens for public clients to be either sender-constrained or rotated.
  • DPoP is standardised as RFC 9449 (2023).
  • Token lifetimes in the video are illustrative ("months"); they vary by provider.

Coming next in the series: Token Rotation done right

Found this useful?

Subscribe for the next episode, or share it with the person who owns this part of your stack.

Keep going