---
title: "Rotate refresh tokens and catch their · Agent Wranglers"
url: https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/refresh-token-rotation
id: ACC-80
level: Required
applies: accounts
risk: takeover
updated: 2026-10-27
build: a2a6bdfed830
tokens: 1253
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Rotate refresh tokens and catch their reuse

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Sessions and security activity](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity.md).

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

- ID: ACC-80
- Level: Required
- Chapter: [Sessions and security activity](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity.md)
- Rule: Issue a new refresh token on every refresh and cancel the old one. When a cancelled refresh token comes back, revoke its whole family and make the person sign in again. A client that cannot rotate binds its refresh tokens to a key with DPoP or mutual TLS.
- Check: A test refreshes once and gets a new token. It then sends the first token again and gets `invalid_grant`. The second token is now refused too, and the security log holds one reuse event. A DPoP-bound token sent without a proof, or with a proof from another key, is refused.
- Risk: An account or system taken over
- Applies when: The project has user accounts.
- How: RFC 9700 says refresh tokens for public clients, such as browser and mobile apps, must be sender-constrained or rotated. Store each refresh token as a hash, with a family ID, the client, the user and the scopes. Mark the old token used in the same transaction that stores its successor. A used token that comes back means two parties hold it, and the server cannot tell which one is the thief. So revoke every token in the family and write an event to the [security log](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/security-activity-log.md). DPoP, from RFC 9449, binds a token to a key pair the client keeps, and every request carries a signed proof. Expire a family after 30 days without use. End it at sign-out, as [let people sign out of one device or all of them](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/revoke-sessions.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
export async function refresh(db, presentedToken) {
  return db.transaction(async tx => {
    const stored = await tx.refreshTokens.findByHash(sha256(presentedToken));
    if (!stored) throw new InvalidGrant();
    if (stored.usedAt) {
      await tx.refreshTokens.revokeFamily(stored.familyId);
      await tx.securityLog.add(stored.userId, 'refresh_token_reuse');
      throw new InvalidGrant();
    }
    await tx.refreshTokens.markUsed(stored.id);
    return issueTokens(tx, stored.userId, stored.familyId, stored.scopes);
  });
}
```

### Python example

```python
def refresh(db, presented_token: str):
    with db.transaction() as tx:
        stored = tx.refresh_tokens.find_by_hash(sha256(presented_token))
        if stored is None:
            raise InvalidGrant()
        if stored.used_at is not None:
            tx.refresh_tokens.revoke_family(stored.family_id)
            tx.security_log.add(stored.user_id, "refresh_token_reuse")
            raise InvalidGrant()
        tx.refresh_tokens.mark_used(stored.id)
        return issue_tokens(tx, stored.user_id, stored.family_id, stored.scopes)
```

## Sources

- [RFC 9700 Best Current Practice for OAuth 2.0 Security, section 4.14](https://www.rfc-editor.org/rfc/rfc9700#section-4.14)
- [RFC 9449 OAuth 2.0 Demonstrating Proof of Possession (DPoP)](https://www.rfc-editor.org/rfc/rfc9449)

## Related rules

- ACC-28 [Let people sign out of one device or all of them](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/revoke-sessions.md) in Accounts and identity
- ACC-31 [Show people their own security log](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/security-activity-log.md) in Accounts and identity
- APS-12 [Verify every token](https://agentwranglers.se/playbook/application-security/authentication-and-sessions/token-checks.md) in Application security

**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/accounts/sessions-and-security-activity/refresh-token-rotation">Agent Wranglers accounts and identity playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [End sessions when access changes](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/end-sessions-on-change.md)
- Next: [List connected apps and personal tokens](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/connected-apps.md)
- [Sessions and security activity](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity.md)
