<?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: Anish Hajare</title>
    <description>The latest articles on DEV Community by Anish Hajare (@anishhajare).</description>
    <link>https://dev.to/anishhajare</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F407950%2F834b8c3e-08d8-4212-97ba-d8fe2c753839.jpg</url>
      <title>DEV Community: Anish Hajare</title>
      <link>https://dev.to/anishhajare</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anishhajare"/>
    <language>en</language>
    <item>
      <title>The Security Problems Most Auth Tutorials Skip</title>
      <dc:creator>Anish Hajare</dc:creator>
      <pubDate>Mon, 23 Mar 2026 08:12:07 +0000</pubDate>
      <link>https://dev.to/anishhajare/the-security-problems-most-auth-tutorials-skip-44a6</link>
      <guid>https://dev.to/anishhajare/the-security-problems-most-auth-tutorials-skip-44a6</guid>
      <description>&lt;p&gt;A lot of authentication tutorials are useful.&lt;/p&gt;

&lt;p&gt;They help you get started, explain hashing, and show how login works.&lt;/p&gt;

&lt;p&gt;That's great.&lt;/p&gt;

&lt;p&gt;But many of them stop right before authentication gets really interesting.&lt;/p&gt;

&lt;p&gt;They cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Signup&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Login&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Password hashing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JWT basics&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And skip things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;User enumeration&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OTP abuse&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session invalidation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refresh token misuse&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Basic abuse protection&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What happens when things go wrong&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While building my auth system, I realized those "extra details" are actually where most of the security thinking lives.&lt;/p&gt;

&lt;p&gt;So this post is about the security problems many auth tutorials don't spend enough time on.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. User Enumeration
&lt;/h2&gt;

&lt;p&gt;This was one of the first issues that started bothering me.&lt;/p&gt;

&lt;p&gt;A lot of systems return messages like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Email not found&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Password incorrect&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Account not verified&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds helpful.&lt;/p&gt;

&lt;p&gt;But it also gives attackers information.&lt;/p&gt;

&lt;p&gt;If someone can test emails against your login flow and get different responses, they can start learning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Which accounts exist&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which accounts are registered&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which ones may still be inactive&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is called &lt;strong&gt;user enumeration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So in my login flow, I kept the response generic:&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid email or password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The email doesn't exist&lt;/li&gt;
&lt;li&gt;The account is unverified&lt;/li&gt;
&lt;li&gt;The password is wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API responds the same way.&lt;/p&gt;

&lt;p&gt;That small choice helps reduce information leakage.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Refresh Tokens Are Often Treated Too Casually
&lt;/h2&gt;

&lt;p&gt;Another thing I noticed is that many tutorials introduce refresh tokens, but don't really manage them.&lt;/p&gt;

&lt;p&gt;They often get treated like long-lived backup keys.&lt;/p&gt;

&lt;p&gt;That creates problems.&lt;/p&gt;

&lt;p&gt;If the same refresh token stays valid for a long time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A stolen token may keep working&lt;/li&gt;
&lt;li&gt;Revocation becomes harder&lt;/li&gt;
&lt;li&gt;Session control becomes weaker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why I used refresh token rotation and tied refresh tokens to server-side session records.&lt;/p&gt;

&lt;p&gt;This made it possible to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace old refresh tokens&lt;/li&gt;
&lt;li&gt;Track active sessions&lt;/li&gt;
&lt;li&gt;Revoke specific sessions&lt;/li&gt;
&lt;li&gt;Support logout-all behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It made the whole system feel much more realistic.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Logout Is Often Only Half Real
&lt;/h2&gt;

&lt;p&gt;A lot of basic auth flows treat logout as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delete token from client&lt;/li&gt;
&lt;li&gt;Clear cookie&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But if the backend still trusts that session, the logout is only partial.&lt;/p&gt;

&lt;p&gt;This became especially important when I implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logout from one device&lt;/li&gt;
&lt;li&gt;Logout from all devices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those features forced me to think in terms of session invalidation, not just token removal.&lt;/p&gt;

&lt;p&gt;Because real logout is not just "the frontend forgot the token."&lt;/p&gt;

&lt;p&gt;Real logout means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The backend no longer trusts that session.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was a major mindset shift for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. OTP Verification Has Its Own Attack Surface
&lt;/h2&gt;

&lt;p&gt;Before building this project, I thought of OTP verification as a simple utility step.&lt;/p&gt;

&lt;p&gt;Now I think of it as a tiny security system.&lt;/p&gt;

&lt;p&gt;Because OTP flows can be abused too.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repeated guessing&lt;/li&gt;
&lt;li&gt;Resend spam&lt;/li&gt;
&lt;li&gt;Using old codes&lt;/li&gt;
&lt;li&gt;Trying many wrong codes without consequences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OTP expiry&lt;/li&gt;
&lt;li&gt;Attempt counting&lt;/li&gt;
&lt;li&gt;Temporary lockout&lt;/li&gt;
&lt;li&gt;Resend cooldowns&lt;/li&gt;
&lt;li&gt;Fresh OTP creation on resend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That turned out to be one of the best examples of a broader lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Every auth feature creates a second security problem you also need to solve.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Basic Abuse Protection Still Matters
&lt;/h2&gt;

&lt;p&gt;I also wanted some protection against repeated request abuse on auth-sensitive routes.&lt;/p&gt;

&lt;p&gt;So I added a simple route-level rate limiter.&lt;/p&gt;

&lt;p&gt;This is worth describing honestly: it is a basic in-memory per-IP rate limiter, not a distributed or production-grade rate limiting system.&lt;/p&gt;

&lt;p&gt;Still, even a simple version helps demonstrate an important idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth endpoints should not be left completely unguarded&lt;/li&gt;
&lt;li&gt;Abuse protection should be part of the design&lt;/li&gt;
&lt;li&gt;Simple safeguards are better than none&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And in this project, that route limiter works alongside the more specific OTP protections.&lt;/p&gt;

&lt;p&gt;So I'd describe the system like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It includes basic route-level per-IP rate limiting, plus OTP-specific retry limits and temporary lockout.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That feels more accurate than calling it full brute-force protection.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Logging Matters, But It Should Be Framed Carefully
&lt;/h2&gt;

&lt;p&gt;I also added structured auth event logging.&lt;/p&gt;

&lt;p&gt;That means auth-related actions are logged with useful metadata while sensitive values like passwords, OTPs, and tokens are excluded.&lt;/p&gt;

&lt;p&gt;That was important to me because auth systems should give you some visibility into what happened.&lt;/p&gt;

&lt;p&gt;At the same time, I think it's important not to oversell this.&lt;/p&gt;

&lt;p&gt;This is best described as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured auth event logging&lt;/li&gt;
&lt;li&gt;Sensitive-field sanitization&lt;/li&gt;
&lt;li&gt;Better visibility into auth flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not a fancy enterprise audit platform, and that's okay.&lt;/p&gt;

&lt;p&gt;For a learning project, this was a really useful layer to add.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Security and UX Constantly Fight Each Other
&lt;/h2&gt;

&lt;p&gt;This was probably the most human lesson in the whole project.&lt;/p&gt;

&lt;p&gt;Many auth decisions are not purely technical.&lt;/p&gt;

&lt;p&gt;They're tradeoffs.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generic login errors improve security, but give less feedback&lt;/li&gt;
&lt;li&gt;OTP lockouts reduce abuse, but can frustrate legitimate users&lt;/li&gt;
&lt;li&gt;Cooldowns limit resend spam, but can feel annoying&lt;/li&gt;
&lt;li&gt;Short token lifetimes improve security, but may hurt convenience&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used to think security features were mostly about adding more protections.&lt;/p&gt;

&lt;p&gt;Now I think they are often about choosing the least painful compromise.&lt;/p&gt;

&lt;p&gt;That feels much closer to real backend design.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. If You Don't Test Edge Cases, Auth Will Fool You
&lt;/h2&gt;

&lt;p&gt;Auth features often look correct in the happy path.&lt;/p&gt;

&lt;p&gt;The problems show up around the edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple devices&lt;/li&gt;
&lt;li&gt;Expired OTPs&lt;/li&gt;
&lt;li&gt;Revoked sessions&lt;/li&gt;
&lt;li&gt;Repeated failed attempts&lt;/li&gt;
&lt;li&gt;Refresh token reuse&lt;/li&gt;
&lt;li&gt;Rate-limited flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why I added integration tests around the auth lifecycle instead of only testing isolated pieces.&lt;/p&gt;

&lt;p&gt;Because auth is one of those systems where "it worked once" doesn't mean much.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 What This Project Taught Me
&lt;/h2&gt;

&lt;p&gt;The biggest lesson was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Working auth is not the same as safe auth.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that gap is where most of the interesting engineering lives.&lt;/p&gt;

&lt;p&gt;This project made me think more carefully about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What trust means&lt;/li&gt;
&lt;li&gt;How trust is renewed&lt;/li&gt;
&lt;li&gt;How trust is revoked&lt;/li&gt;
&lt;li&gt;How information leaks happen&lt;/li&gt;
&lt;li&gt;How abuse shows up in small features&lt;/li&gt;
&lt;li&gt;How architecture affects security&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That changed how I look at authentication completely.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Auth tutorials are great for getting started.&lt;/p&gt;

&lt;p&gt;But once you move past the basics, the important questions change.&lt;/p&gt;

&lt;p&gt;It's no longer just:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do I log a user in?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do I verify ownership?&lt;/li&gt;
&lt;li&gt;How do I control sessions?&lt;/li&gt;
&lt;li&gt;How do I reduce information leaks?&lt;/li&gt;
&lt;li&gt;How do I limit abuse?&lt;/li&gt;
&lt;li&gt;How do I make logout actually mean something?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the part of authentication I wanted to learn through this project.&lt;/p&gt;

&lt;p&gt;And honestly, it's the part I now find most interesting.&lt;/p&gt;




&lt;p&gt;Have you noticed security gaps in common auth tutorials too? Drop a comment below and ask questions if you have any. I'd love to hear your take. 💬&lt;/p&gt;

</description>
      <category>backend</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Safer Email OTP Verification in Node.js: Expiry, Retries, and Lockouts</title>
      <dc:creator>Anish Hajare</dc:creator>
      <pubDate>Mon, 23 Mar 2026 07:53:31 +0000</pubDate>
      <link>https://dev.to/anishhajare/building-safer-email-otp-verification-in-nodejs-expiry-retries-and-lockouts-558n</link>
      <guid>https://dev.to/anishhajare/building-safer-email-otp-verification-in-nodejs-expiry-retries-and-lockouts-558n</guid>
      <description>&lt;p&gt;Email verification sounds simple.&lt;/p&gt;

&lt;p&gt;And at first, it kind of is. 😄&lt;/p&gt;

&lt;p&gt;You generate a code, send it to the user, and compare it when they type it back in.&lt;/p&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;Except... not really.&lt;/p&gt;

&lt;p&gt;The moment I tried to make OTP verification behave more like something a real app could rely on, the hidden problems showed up fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What if someone brute-forces the OTP?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What if they keep requesting new codes?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What if the OTP expires?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What if email delivery fails?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How do you balance security with usability?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So in this project, I built an email verification flow with &lt;strong&gt;OTP expiry&lt;/strong&gt;, &lt;strong&gt;failed-attempt tracking&lt;/strong&gt;, &lt;strong&gt;temporary lockouts&lt;/strong&gt;, and &lt;strong&gt;resend protection&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And honestly, OTP ended up being more interesting than I expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Why Basic OTP Flows Are Not Enough
&lt;/h2&gt;

&lt;p&gt;A simple OTP flow usually looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Generate a code&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Store it&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Send it&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compare it later&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That works for demos.&lt;/p&gt;

&lt;p&gt;But if you stop there, you leave a lot exposed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Users can guess the code repeatedly&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They can spam resend&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expired codes may still linger&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failed delivery can create broken account states&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The OTP itself becomes a small attack surface&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why I wanted the system to do more than just "send a 6-digit code."&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 What My OTP Flow Does
&lt;/h2&gt;

&lt;p&gt;When a user registers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A 6-digit OTP is generated&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The OTP is hashed before storage&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An expiry time is set&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The code is emailed to the user&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The account stays unverified until the OTP is confirmed&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And on top of that, the system also includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Attempt counting&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Temporary lockout after too many failures&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A resend cooldown&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fresh OTP creation on resend&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleanup of expired records over time&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔐 Why I Hash the OTP
&lt;/h2&gt;

&lt;p&gt;This was a simple but important decision.&lt;/p&gt;

&lt;p&gt;Instead of storing the OTP in plain text, I store a hash of it.&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;function&lt;/span&gt; &lt;span class="nf"&gt;hashValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&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;That means even if someone gained database access, the raw OTP would not just be sitting there in readable form.&lt;/p&gt;

&lt;p&gt;It follows the same basic principle as password hashing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sensitive secrets should not be stored in plain text&lt;/li&gt;
&lt;li&gt;The database should not become an easy source of abuse&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⏳ OTP Expiry Matters
&lt;/h2&gt;

&lt;p&gt;A verification code should not live forever.&lt;/p&gt;

&lt;p&gt;So each OTP gets an expiration time.&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;expiresAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OTP_EXPIRY_MINUTES&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That helps in a few ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Limits how long an OTP is useful&lt;/li&gt;
&lt;li&gt;Prevents old codes from hanging around forever&lt;/li&gt;
&lt;li&gt;Makes the verification process more predictable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also added a TTL index on the OTP model.&lt;/p&gt;

&lt;p&gt;That means expired OTP records become eligible for automatic cleanup by MongoDB in the background.&lt;/p&gt;

&lt;p&gt;That detail is worth phrasing carefully: TTL cleanup is helpful, but it is not guaranteed to happen at the exact second the OTP expires.&lt;/p&gt;

&lt;p&gt;So the app still checks expiry directly when verifying the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚫 Failed Attempts and Temporary Lockout
&lt;/h2&gt;

&lt;p&gt;This was one of the most important protections in the flow.&lt;/p&gt;

&lt;p&gt;If someone enters the wrong OTP repeatedly, the system tracks the number of failed attempts.&lt;/p&gt;

&lt;p&gt;After enough bad attempts, the OTP is temporarily locked.&lt;/p&gt;

&lt;p&gt;Here's the core idea:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;otpDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;otpHash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;hashValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;otp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;otpDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attemptCount&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&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;otpDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attemptCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OTP_MAX_ATTEMPTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;otpDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lockedUntil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OTP_LOCKOUT_MINUTES&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="p"&gt;);&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;otpDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid OTP&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;This helps reduce OTP guessing.&lt;/p&gt;

&lt;p&gt;Because without limits, a 6-digit code field becomes a lot easier to abuse.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔁 Resend Protection Was Surprisingly Important
&lt;/h2&gt;

&lt;p&gt;At first, resend felt like a tiny feature.&lt;/p&gt;

&lt;p&gt;Then I thought about it a little longer and realized it needed rules too.&lt;/p&gt;

&lt;p&gt;Without controls, users or attackers could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spam the resend endpoint&lt;/li&gt;
&lt;li&gt;Flood email delivery&lt;/li&gt;
&lt;li&gt;Reset the verification flow repeatedly&lt;/li&gt;
&lt;li&gt;Turn a useful feature into an abuse point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I added a resend cooldown in the OTP flow.&lt;/p&gt;

&lt;p&gt;That means a new OTP cannot be sent immediately over and over again.&lt;/p&gt;

&lt;p&gt;And when a new OTP is created, the previous OTP record for that user is deleted first, so only the latest code remains valid.&lt;/p&gt;

&lt;p&gt;That also means resend creates a fresh OTP record, which naturally resets previous attempt and lock state.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛡️ OTP Resend Protection vs Rate Limiting
&lt;/h2&gt;

&lt;p&gt;This is a subtle distinction, but I think it matters.&lt;/p&gt;

&lt;p&gt;The resend flow is protected in two different ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A resend cooldown specific to OTP behavior&lt;/li&gt;
&lt;li&gt;A generic route-level rate limiter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are related, but they are not the same thing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cooldown is part of the OTP business logic.&lt;/li&gt;
&lt;li&gt;The route limiter is a broader per-IP request limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I liked having both, because they protect the flow from slightly different angles.&lt;/p&gt;




&lt;h2&gt;
  
  
  😅 One Subtle Problem: Failed Email Delivery
&lt;/h2&gt;

&lt;p&gt;Another detail I cared about was what happens if registration succeeds but the verification email fails.&lt;/p&gt;

&lt;p&gt;That can create a frustrating state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user exists&lt;/li&gt;
&lt;li&gt;The account is unverified&lt;/li&gt;
&lt;li&gt;The OTP never arrived&lt;/li&gt;
&lt;li&gt;The signup feels broken&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I chose to roll back registration if sending the verification email fails.&lt;/p&gt;

&lt;p&gt;That way the system avoids leaving behind incomplete account states.&lt;/p&gt;

&lt;p&gt;It's one of those small details that makes the flow feel much more intentional.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚖️ Security vs Usability
&lt;/h2&gt;

&lt;p&gt;This was the hardest part of OTP design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security says:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expire quickly&lt;/li&gt;
&lt;li&gt;Lock after repeated failures&lt;/li&gt;
&lt;li&gt;Limit resends&lt;/li&gt;
&lt;li&gt;Be strict&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Usability says:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't punish honest mistakes too harshly&lt;/li&gt;
&lt;li&gt;Give the user time to check email&lt;/li&gt;
&lt;li&gt;Allow recovery when delivery is delayed&lt;/li&gt;
&lt;li&gt;Keep the process understandable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real challenge was not just adding protections.&lt;/p&gt;

&lt;p&gt;It was choosing protections that still felt reasonable for a real user.&lt;/p&gt;

&lt;p&gt;That balance is where most of the design thinking happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 What I Learned
&lt;/h2&gt;

&lt;p&gt;Building OTP verification taught me a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OTP flows are small, but not simple&lt;/li&gt;
&lt;li&gt;Every verification feature creates its own abuse risks&lt;/li&gt;
&lt;li&gt;Hashing OTPs is worth doing&lt;/li&gt;
&lt;li&gt;Retry limits and lockouts matter&lt;/li&gt;
&lt;li&gt;Resend behavior needs guardrails too&lt;/li&gt;
&lt;li&gt;Security and recovery need to be balanced together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest takeaway?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Email verification is not just a checkbox feature. It's part of your security design.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And once I started thinking about it that way, the implementation got much better.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Before this project, I thought of OTP verification as a small supporting feature.&lt;/p&gt;

&lt;p&gt;After building it, I started seeing it as a tiny system of its own with its own rules, tradeoffs, and security concerns.&lt;/p&gt;

&lt;p&gt;That was actually one of my favorite lessons from this auth project.&lt;/p&gt;

&lt;p&gt;If you're adding email OTP verification to your app, don't stop at "generate and compare code."&lt;/p&gt;

&lt;p&gt;Think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expiry&lt;/li&gt;
&lt;li&gt;Retry limits&lt;/li&gt;
&lt;li&gt;Lockouts&lt;/li&gt;
&lt;li&gt;Resend protection&lt;/li&gt;
&lt;li&gt;Failure handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because those are the parts that turn a working OTP flow into a safer one.&lt;/p&gt;




&lt;p&gt;Have you built OTP verification before? Drop a comment below and ask questions if you have any. I'd love to hear how you handled the edge cases. 💬&lt;/p&gt;

</description>
      <category>backend</category>
      <category>javascript</category>
      <category>node</category>
      <category>security</category>
    </item>
    <item>
      <title>How I Implemented Logout From One Device and All Devices in My Auth System</title>
      <dc:creator>Anish Hajare</dc:creator>
      <pubDate>Mon, 23 Mar 2026 07:37:55 +0000</pubDate>
      <link>https://dev.to/anishhajare/how-i-implemented-logout-from-one-device-and-all-devices-in-my-auth-system-4dhn</link>
      <guid>https://dev.to/anishhajare/how-i-implemented-logout-from-one-device-and-all-devices-in-my-auth-system-4dhn</guid>
      <description>&lt;p&gt;"Logout" sounds like one of the easiest features in authentication.&lt;/p&gt;

&lt;p&gt;It's just a button, right? 😄&lt;/p&gt;

&lt;p&gt;That's what I thought too.&lt;/p&gt;

&lt;p&gt;But once I started building a more realistic auth system, I realized logout is only simple if you don't ask too many questions.&lt;/p&gt;

&lt;p&gt;Questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Does logout affect only this device?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What happens to other active sessions?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How do I support logout from all devices?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How do I make revoked sessions stop working right away on protected routes?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's when I learned something important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Real logout is not just about clearing a cookie. It's about telling the backend to stop trusting a session.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So in this project, I built both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Logout from the current device&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logout from all devices&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And it taught me a lot about session design.&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Why Logout Is Harder Than It Looks
&lt;/h2&gt;

&lt;p&gt;In simple auth demos, logout often means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Delete token from local storage&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear cookie&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redirect to login&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That works on the client side.&lt;/p&gt;

&lt;p&gt;But from the backend's point of view, that is not always enough.&lt;/p&gt;

&lt;p&gt;If the server still trusts the session behind that token, then logout is only partially real.&lt;/p&gt;

&lt;p&gt;That's the gap I wanted to close.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 The Key Idea: Sessions Live on the Server
&lt;/h2&gt;

&lt;p&gt;To make logout meaningful, I tracked sessions in the database.&lt;/p&gt;

&lt;p&gt;Each session stores things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Which user it belongs to&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A hashed refresh token&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP address&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User agent&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Whether the session is revoked&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When it was revoked&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes logout a backend-controlled action instead of just a frontend cleanup step.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚪 Logout From the Current Device
&lt;/h2&gt;

&lt;p&gt;For current-device logout, the goal is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Invalidate only the session tied to the current refresh token&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leave the user's other devices alone&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the server looks up the session connected to the refresh token, marks it as revoked, and stops trusting it.&lt;/p&gt;

&lt;p&gt;Here's the main logic:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;revokeCurrentSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sessionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;refreshTokenHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hashValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid refresh token&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;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revoked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revokedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;That gives logout real meaning.&lt;/p&gt;

&lt;p&gt;It's not just "remove the cookie."&lt;br&gt;
It's "this session should no longer be accepted."&lt;/p&gt;


&lt;h2&gt;
  
  
  🌍 Logout From All Devices
&lt;/h2&gt;

&lt;p&gt;This feature was even more interesting.&lt;/p&gt;

&lt;p&gt;A user might want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign out everywhere after suspicious activity&lt;/li&gt;
&lt;li&gt;Clear old sessions across devices&lt;/li&gt;
&lt;li&gt;End all active logins at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I added a logout-all flow.&lt;/p&gt;

&lt;p&gt;In this implementation, the backend uses the refresh token cookie to identify the user, then revokes all active sessions for that account.&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;revokeAllSessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&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;sessionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;revokedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&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;That means every active session for that user becomes revoked.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 What About Existing Access Tokens?
&lt;/h2&gt;

&lt;p&gt;This was the part that made the design much more interesting.&lt;/p&gt;

&lt;p&gt;Revoking refresh tokens alone is not enough.&lt;/p&gt;

&lt;p&gt;A user may still have an access token that has not expired yet.&lt;/p&gt;

&lt;p&gt;So I tied access tokens to a &lt;code&gt;sessionId&lt;/code&gt; and had protected routes check whether the related session is still active.&lt;/p&gt;

&lt;p&gt;That means protected routes do not only verify the JWT itself.&lt;/p&gt;

&lt;p&gt;They also check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the session still exist?&lt;/li&gt;
&lt;li&gt;Does it belong to this user?&lt;/li&gt;
&lt;li&gt;Has it been revoked?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So it's more accurate to say this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Existing access tokens stop working on protected routes because the server checks whether their session is still active.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's what makes session revocation effective in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Testing This Was Super Important
&lt;/h2&gt;

&lt;p&gt;Logout is one of those features that feels correct until you actually test it.&lt;/p&gt;

&lt;p&gt;So I added tests for behaviors like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logout should revoke only one session&lt;/li&gt;
&lt;li&gt;Logout-all should revoke every active session&lt;/li&gt;
&lt;li&gt;Protected routes should reject access tied to revoked sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last part matters a lot.&lt;/p&gt;

&lt;p&gt;Because logout-all is only meaningful if revoked sessions are actually rejected later.&lt;/p&gt;

&lt;p&gt;Testing helped confirm that the behavior matched the promise.&lt;/p&gt;




&lt;h2&gt;
  
  
  😅 What Was Challenging
&lt;/h2&gt;

&lt;p&gt;The tricky part wasn't writing the endpoint itself.&lt;/p&gt;

&lt;p&gt;It was making the whole system agree on what logout means.&lt;/p&gt;

&lt;p&gt;That included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refresh token lookup&lt;/li&gt;
&lt;li&gt;Session revocation&lt;/li&gt;
&lt;li&gt;Protected route validation&lt;/li&gt;
&lt;li&gt;Multi-device behavior&lt;/li&gt;
&lt;li&gt;Making sure one-device logout doesn't affect others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was one of those features that sounded small in product terms, but touched a lot of auth architecture underneath.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 What I Learned
&lt;/h2&gt;

&lt;p&gt;A few things became really clear while building this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clearing a cookie is not the same as invalidating a session&lt;/li&gt;
&lt;li&gt;Server-side session tracking makes logout much stronger&lt;/li&gt;
&lt;li&gt;Logout-all is really a session revocation problem&lt;/li&gt;
&lt;li&gt;Protected routes need session awareness if you want revoked sessions to stop working&lt;/li&gt;
&lt;li&gt;Multi-device auth adds complexity very quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My biggest takeaway was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If your backend cannot tell whether a session is still trusted, logout is only partially real.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That idea changed the way I think about authentication.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This feature taught me that logout is not boring at all.&lt;/p&gt;

&lt;p&gt;It's actually one of the most revealing parts of an auth system, because it forces you to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who is authenticated right now?&lt;/li&gt;
&lt;li&gt;Which session is still trusted?&lt;/li&gt;
&lt;li&gt;How does trust get revoked?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building your own auth system, I highly recommend thinking beyond "clear the cookie."&lt;/p&gt;

&lt;p&gt;Because once users have multiple devices, logout becomes much more than a frontend action.&lt;/p&gt;




&lt;p&gt;Have you implemented logout-all in your own auth flow? Drop a comment below and ask questions if you have any. I'd love to talk about it. 💬&lt;/p&gt;

</description>
      <category>backend</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Finally Understood JWT Auth - After Building Refresh Token Rotation From Scratch</title>
      <dc:creator>Anish Hajare</dc:creator>
      <pubDate>Sun, 22 Mar 2026 14:50:59 +0000</pubDate>
      <link>https://dev.to/anishhajare/i-finally-understood-jwt-auth-after-building-refresh-token-rotation-from-scratch-fd4</link>
      <guid>https://dev.to/anishhajare/i-finally-understood-jwt-auth-after-building-refresh-token-rotation-from-scratch-fd4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;JWT tutorials only teach the easy part. Here's what happens after.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Most auth tutorials end at "user logs in, gets a token, done." And for a while, that felt fine to me too.&lt;/p&gt;

&lt;p&gt;Then the uncomfortable questions showed up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the refresh token is stolen? How do you actually revoke a session? How do you know which device is logged in?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the point where I realized I needed to build something real to understand auth properly. So I built &lt;strong&gt;refresh token rotation backed by server-side session tracking&lt;/strong&gt; - and it changed the way I think about authentication entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  😅 The Problem With "Basic" JWT Auth
&lt;/h2&gt;

&lt;p&gt;A lot of beginner tutorials go like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ Create a token when the user logs in&lt;/li&gt;
&lt;li&gt;✅ Send it to the client&lt;/li&gt;
&lt;li&gt;✅ Verify it on protected routes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That works. Until it doesn't.&lt;/p&gt;

&lt;p&gt;Fully stateless JWT auth makes some critical things hard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ &lt;strong&gt;You can't easily revoke sessions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;You can't safely manage multiple devices&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;A stolen refresh token stays valid until it expires&lt;/strong&gt; (which could be days or weeks)&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;"Logout" becomes a client-side illusion, not a server-side guarantee&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧠 The Core Concept: Controlled Token Lifecycle
&lt;/h2&gt;

&lt;p&gt;Instead of treating refresh tokens like permanent keys, I made them part of a &lt;strong&gt;controlled session lifecycle&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's the high-level approach:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token&lt;/th&gt;
&lt;th&gt;Lifetime&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Access Token&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Short-lived&lt;/td&gt;
&lt;td&gt;Used for protected requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Refresh Token&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Longer-lived&lt;/td&gt;
&lt;td&gt;Stored in &lt;code&gt;httpOnly&lt;/code&gt; cookie, used to get a new access token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Think of it like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🪪 &lt;strong&gt;Access token&lt;/strong&gt; = visitor pass&lt;br&gt;&lt;br&gt;
🔁 &lt;strong&gt;Refresh token&lt;/strong&gt; = a controlled way to ask for a new pass&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The access token should be disposable. The refresh token needs stricter handling.&lt;/p&gt;




&lt;h2&gt;
  
  
  🗺️ The Full Auth Flow
&lt;/h2&gt;

&lt;p&gt;Here's the big picture of how everything fits together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────┐
│                        AUTH FLOW OVERVIEW                        │
└─────────────────────────────────────────────────────────────────┘

                         ┌──────────┐
                         │  Client  │
                         └────┬─────┘
                              │ POST /login
                              ▼
                    ┌─────────────────┐
                    │   Auth Server   │
                    │  ─────────────  │
                    │  Verifies creds │
                    │  Creates session│
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
      ┌──────────────┐  ┌─────────┐  ┌───────────────┐
      │ Access Token │  │ Refresh │  │  Session Row  │
      │  (short TTL) │  │  Token  │  │  in Database  │
      └──────────────┘  │(httpOnly│  │  userId, ip,  │
                        │ cookie) │  │  userAgent,   │
                        └─────────┘  │  tokenHash    │
                                     └───────────────┘

      ┌──────────────────────────────────────────────────┐
      │              PROTECTED REQUEST                   │
      │                                                  │
      │  Client ──── Access Token ────► Protected Route  │
      │                                    │             │
      │                          Token valid? ──► ✅ OK  │
      │                          Token expired? ──► 401  │
      └──────────────────────────────────────────────────┘

      ┌──────────────────────────────────────────────────┐
      │              TOKEN REFRESH                       │
      │                                                  │
      │  Client ──── Refresh Token ──► /refresh          │
      │                                    │             │
      │                          Hash match in DB?       │
      │                          Session revoked?        │
      │                          Already used? ──► THEFT │
      │                                    │             │
      │                          ✅ Issue new tokens     │
      │                          ❌ Reject + revoke all  │
      └──────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ♻️ What Refresh Token Rotation Actually Means
&lt;/h2&gt;

&lt;p&gt;When the client asks for a new access token, the server does &lt;strong&gt;not&lt;/strong&gt; keep trusting the same refresh token forever.&lt;/p&gt;

&lt;p&gt;Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;🔍 &lt;strong&gt;Verify&lt;/strong&gt; the current refresh token's signature&lt;/li&gt;
&lt;li&gt;🔎 &lt;strong&gt;Match&lt;/strong&gt; its hash to an active, non-superseded session in the DB&lt;/li&gt;
&lt;li&gt;🆕 &lt;strong&gt;Generate&lt;/strong&gt; a new refresh token&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Mark the old token as superseded&lt;/strong&gt; (not just replace the hash)&lt;/li&gt;
&lt;li&gt;🚫 &lt;strong&gt;Old refresh token&lt;/strong&gt; stops working immediately
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ROTATION CYCLE:

  Client                    Server                    Database
    │                          │                          │
    │── POST /refresh ─────►   │                          │
    │   {refreshToken}         │── findSession(hash) ──►  │
    │                          │◄── session found ───────  │
    │                          │                          │
    │                          │── generate newRefreshToken
    │                          │── mark old as superseded ►│
    │                          │── store new hash ────────►│
    │                          │                          │
    │◄── {accessToken,  ───────│                          │
    │     newRefreshToken}     │                          │
    │                          │                          │
    │  [old token is now dead] │                          │

TOKEN THEFT DETECTION:

  Attacker uses stolen (already-rotated) token
    │
    ▼
  Server looks up hash - finds it was already superseded
    │
    ▼
  ⚠️  REUSE DETECTED - revoke the ENTIRE session family
    │
    ▼
  Both the legitimate user and attacker are logged out
  The compromise is contained ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the implementation with theft detection included:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rotateRefreshToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Verify the incoming token is cryptographically valid&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshTokenHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Look it up in the DB - find ANY matching session (revoked or not)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sessionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;refreshTokenHash&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Token hash not found at all - truly invalid&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid refresh token&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;span class="c1"&gt;// 3. If the token was already rotated (superseded), this signals THEFT&lt;/span&gt;
  &lt;span class="c1"&gt;//    Revoke the entire session to protect the legitimate user&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;superseded&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;)&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;sessionModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;revokedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AppError&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Token reuse detected. All sessions revoked.&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;span class="c1"&gt;// 4. Generate a brand new refresh token&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newRefreshToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signRefreshToken&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Mark the old token as superseded and store the new hash atomically&lt;/span&gt;
  &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;superseded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshTokenHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newRefreshToken&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 6. Return both new tokens to the client&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;signAccessToken&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newRefreshToken&lt;/span&gt;&lt;span class="p"&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;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Why theft detection matters:&lt;/strong&gt; Without it, if an attacker steals and uses a refresh token before the legitimate user does, the server happily rotates it for the attacker. The real user's next request just silently fails with "invalid token." They have no idea they were compromised. With reuse detection, the moment a superseded token is presented, the entire session gets revoked - limiting the blast radius and signaling that something is wrong.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🔐 Why I Hash the Refresh Token (and You Should Too)
&lt;/h2&gt;

&lt;p&gt;This was one of those &lt;em&gt;"oh yeah, of course"&lt;/em&gt; moments.&lt;/p&gt;

&lt;p&gt;If refresh tokens are powerful, storing them in plain text is risky.&lt;/p&gt;

&lt;p&gt;So instead of storing the raw token, I store a &lt;strong&gt;SHA-256 hash&lt;/strong&gt; of it:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hashValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&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;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&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;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&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;&lt;strong&gt;Why this matters:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Without Hashing&lt;/th&gt;
&lt;th&gt;With Hashing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DB is compromised&lt;/td&gt;
&lt;td&gt;💀 Attacker has live tokens&lt;/td&gt;
&lt;td&gt;✅ Only useless hashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Insider threat&lt;/td&gt;
&lt;td&gt;💀 Employee can steal sessions&lt;/td&gt;
&lt;td&gt;✅ Can't derive tokens from hashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data breach logged&lt;/td&gt;
&lt;td&gt;💀 Tokens in log files are exploitable&lt;/td&gt;
&lt;td&gt;✅ Hashes are worthless&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;🔑 &lt;strong&gt;SHA-256 vs bcrypt - important distinction:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
You might wonder: why SHA-256 and not bcrypt like we use for passwords?  &lt;/p&gt;

&lt;p&gt;Passwords are &lt;strong&gt;low-entropy human-chosen strings&lt;/strong&gt; - they need slow, salted hashing (bcrypt, Argon2) to resist brute force.  &lt;/p&gt;

&lt;p&gt;Refresh tokens are &lt;strong&gt;cryptographically random, high-entropy values&lt;/strong&gt; (typically 256+ bits of CSPRNG output). Brute-forcing a random 256-bit value is computationally infeasible regardless of hash speed. SHA-256 is fast, collision-resistant, and perfectly appropriate here. Using bcrypt on a refresh token would add unnecessary latency with zero security benefit.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📱 Why Session Tracking Is the Real Upgrade
&lt;/h2&gt;

&lt;p&gt;This is where the system moves from "auth demo" to "actual auth system."&lt;/p&gt;

&lt;p&gt;Each login creates a &lt;strong&gt;session record&lt;/strong&gt; in the database:&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="c1"&gt;// Session schema&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="nx"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// which user&lt;/span&gt;
  &lt;span class="nx"&gt;refreshTokenHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// hashed token (NOT the raw token)&lt;/span&gt;
  &lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;               &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// where they logged in from&lt;/span&gt;
  &lt;span class="nx"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// which browser / device&lt;/span&gt;
  &lt;span class="nx"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// has this been explicitly killed?&lt;/span&gt;
  &lt;span class="nx"&gt;revokedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// when was it killed?&lt;/span&gt;
  &lt;span class="nx"&gt;superseded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// has this token already been rotated?&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nb"&gt;Date&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;With session tracking, the backend can now answer real questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Which sessions are currently active?&lt;/li&gt;
&lt;li&gt;✅ Which device or browser logged in?&lt;/li&gt;
&lt;li&gt;✅ Has this session been revoked?&lt;/li&gt;
&lt;li&gt;✅ Was this token already rotated (possible theft)?&lt;/li&gt;
&lt;li&gt;✅ Should this refresh token still be trusted?
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SESSION REVOCATION FLOW:

  User clicks "Log out of all devices"
              │
              ▼
    ┌─────────────────────┐
    │  Mark ALL sessions  │
    │  revoked: true      │
    │  revokedAt: now()   │
    └─────────────────────┘
              │
              ▼
    Any future refresh attempt
    finds revoked: true ──► 401

    All devices logged out
    instantly. No waiting
    for token expiry. ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, JWT auth is like &lt;em&gt;sending signed permission slips into the wild and hoping they behave.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🤔 Why Not Just Use Stateless JWT Everywhere?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Because stateless JWT is great - until you need stateful behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And auth almost always needs stateful behavior:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Stateless JWT&lt;/th&gt;
&lt;th&gt;With Session Tracking&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Logout one device&lt;/td&gt;
&lt;td&gt;❌ Token lives until expiry&lt;/td&gt;
&lt;td&gt;✅ Revoke that session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logout all devices&lt;/td&gt;
&lt;td&gt;❌ Impossible cleanly&lt;/td&gt;
&lt;td&gt;✅ Revoke all sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immediate invalidation&lt;/td&gt;
&lt;td&gt;❌ Can't do it&lt;/td&gt;
&lt;td&gt;✅ Flip &lt;code&gt;revoked: true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detect stolen tokens&lt;/td&gt;
&lt;td&gt;❌ No awareness&lt;/td&gt;
&lt;td&gt;✅ Reuse detection built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Track suspicious sessions&lt;/td&gt;
&lt;td&gt;❌ No awareness&lt;/td&gt;
&lt;td&gt;✅ IP + userAgent logged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotate tokens safely&lt;/td&gt;
&lt;td&gt;❌ Risky&lt;/td&gt;
&lt;td&gt;✅ Hash rotation + superseded flag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The biggest insight:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
JWT alone solves &lt;strong&gt;identity proof&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Session state solves &lt;strong&gt;lifecycle control&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
In real apps, lifecycle control matters &lt;em&gt;a lot&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;⚖️ &lt;strong&gt;One honest tradeoff:&lt;/strong&gt; Including &lt;code&gt;sessionId&lt;/code&gt; in the access token payload (as shown in the code) is a deliberate design choice that makes the access token semi-stateful. It only adds value if you validate the session on every protected request - otherwise it's payload bloat. If you validate it server-side on every request, you're giving up some of the "stateless" benefit of JWT in exchange for tighter session control. Know the tradeoff before you make it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  😵 What Was Actually Hard to Build
&lt;/h2&gt;

&lt;p&gt;The tricky part wasn't getting token generation to work. Generating tokens is easy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hard part was making the flow safe and consistent:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔄 Making rotation happen without breaking the client's flow&lt;/li&gt;
&lt;li&gt;🚫 Ensuring old refresh tokens immediately stop working&lt;/li&gt;
&lt;li&gt;🕵️ Building reuse detection that protects users when tokens are stolen&lt;/li&gt;
&lt;li&gt;🏗️ Designing sessions so they can be revoked individually or all at once&lt;/li&gt;
&lt;li&gt;🧪 Keeping the logic clean enough to actually test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the gap between a "tutorial auth system" and something production-worthy.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Key Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Refresh tokens shouldn't be treated casually&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
They're long-lived and powerful. Hash them. Rotate them. Track them. Detect reuse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Session tracking gives your backend real control&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Without it, you're flying blind. You can't revoke what you can't see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Stateless auth is great - for the right problems&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Short-lived access tokens? Stateless is perfect. Long-lived refresh tokens? You need server state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Revocation becomes easy when sessions exist&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Want to kill a session? Set &lt;code&gt;revoked: true&lt;/code&gt;. Done. No waiting for token expiry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Reuse detection is what makes rotation actually secure&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Rotation without reuse detection is like changing your locks but leaving the old key working. If a rotated token is presented again, revoke everything immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Multiple devices change everything&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The moment you support multiple devices, you need per-device session tracking. There's no clean alternative.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you're building auth in Node.js and only using JWT on its own - I really encourage you to think about refresh token rotation and session tracking.&lt;/p&gt;

&lt;p&gt;These two ideas changed the way I think about auth systems entirely.&lt;/p&gt;

&lt;p&gt;The biggest lesson?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;JWT gives you identity. Sessions give you control. You need both.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;em&gt;If you've built auth before, I'd love to know how you handled refresh tokens and sessions. Drop your approach in the comments 💬&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;#node&lt;/code&gt; &lt;code&gt;#security&lt;/code&gt; &lt;code&gt;#webdev&lt;/code&gt; &lt;code&gt;#javascript&lt;/code&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>security</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
