<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: polasamy-eng</title>
    <description>The latest articles on DEV Community by polasamy-eng (@polasamyeng).</description>
    <link>https://dev.to/polasamyeng</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4115634%2Fd69db6bc-9b94-43a0-9461-908cb64e43cc.png</url>
      <title>DEV Community: polasamy-eng</title>
      <link>https://dev.to/polasamyeng</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/polasamyeng"/>
    <language>en</language>
    <item>
      <title>Detecting Refresh Token Reuse with Redis (Working Code Included)</title>
      <dc:creator>polasamy-eng</dc:creator>
      <pubDate>Tue, 08 Sep 2026 12:10:32 +0000</pubDate>
      <link>https://dev.to/polasamyeng/detecting-refresh-token-reuse-with-redis-working-code-included-fhd</link>
      <guid>https://dev.to/polasamyeng/detecting-refresh-token-reuse-with-redis-working-code-included-fhd</guid>
      <description>&lt;h2&gt;
  
  
  tags: security, redis, nodejs, jwt
&lt;/h2&gt;

&lt;p&gt;Refresh token rotation is easy to describe and annoying to get right. The idea: every time a refresh token is used, kill it and hand back a new one. Simple.&lt;/p&gt;

&lt;p&gt;The part most write-ups skip: &lt;strong&gt;what do you do when someone uses a refresh token that's already been rotated away?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's not an edge case you can ignore. If your client already exchanged token A for token B, and a request shows up later using token A, one of two things happened:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A retried request or race condition on your own client (rare, but happens)&lt;/li&gt;
&lt;li&gt;Someone else has a copy of token A — the legitimate session already moved on, so this is theft&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't tell which from the request alone. So the safe move is to treat it as theft: kill the entire session lineage, not just the one bad token.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: token families
&lt;/h2&gt;

&lt;p&gt;Instead of tracking individual refresh tokens, track &lt;strong&gt;families&lt;/strong&gt;. A family is created at login. Every rotation within that family updates a single pointer in Redis: "this is the currently valid token ID for this family."&lt;/p&gt;

&lt;p&gt;When a refresh request comes in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the presented token ID matches the family's current pointer → legitimate rotation. Issue a new token ID, update the pointer.&lt;/li&gt;
&lt;li&gt;If it doesn't match → this token was already superseded. Reuse detected. Delete the family outright.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second branch is the whole point. One Redis key, one comparison, and you get theft detection almost for free.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;familyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;familyId&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tokenId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;presentedTokenId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// presented token isn't the current one — it was already rotated away&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;familyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;familyId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Refresh token reuse detected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Redis footprint stays constant per session regardless of how many times it refreshes — you're storing "what's valid right now," not a growing history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Redis and not just a DB flag
&lt;/h2&gt;

&lt;p&gt;You could do this in Postgres with a &lt;code&gt;revoked_at&lt;/code&gt; column. Redis just makes the TTL bookkeeping free: set the family key's expiry to match the refresh token's own lifetime, and stale families clean themselves up. No cron job, no orphaned rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full write-up + runnable code
&lt;/h2&gt;

&lt;p&gt;The full breakdown — including the login flow, what happens on legitimate logout, and why family-level revocation beats token-level revocation for this specific attack — is here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://devsaas.dev/blog/detect-refresh-token-reuse-redis" rel="noopener noreferrer"&gt;Detecting Refresh Token Reuse with Redis →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the actual runnable example (Express + ioredis + Docker Compose, clone and &lt;code&gt;curl&lt;/code&gt; it yourself):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/polasamy-eng/devsaas-devops-examples" rel="noopener noreferrer"&gt;github.com/polasamy-eng/devsaas-devops-examples&lt;/a&gt;&lt;/strong&gt; — see &lt;code&gt;refresh-token-reuse-detection/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you're implementing this and hit a case that doesn't fit — multi-device logout, mobile clients that retry on flaky networks, whatever — I'd genuinely like to hear it. Most of what's written about this pattern glosses over exactly those cases.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>backend</category>
      <category>security</category>
    </item>
  </channel>
</rss>
