<?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: Pallavi Kumari</title>
    <description>The latest articles on DEV Community by Pallavi Kumari (@auberzean).</description>
    <link>https://dev.to/auberzean</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%2F3853648%2F6c68dc53-c660-4f13-bef4-4fc23dc3f486.jpeg</url>
      <title>DEV Community: Pallavi Kumari</title>
      <link>https://dev.to/auberzean</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/auberzean"/>
    <language>en</language>
    <item>
      <title>JWT auth is one of those things that’s easy to get working… and just as easy to get subtly wrong.
I tried to break down the full flow and the trade-offs a bit more clearly here.

Would love feedback / how others are doing it 👇</title>
      <dc:creator>Pallavi Kumari</dc:creator>
      <pubDate>Fri, 24 Apr 2026 02:26:32 +0000</pubDate>
      <link>https://dev.to/auberzean/jwt-auth-is-one-of-those-things-thats-easy-to-get-working-and-just-as-easy-to-get-subtly-wrong-3hh4</link>
      <guid>https://dev.to/auberzean/jwt-auth-is-one-of-those-things-thats-easy-to-get-working-and-just-as-easy-to-get-subtly-wrong-3hh4</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/auberzean/designing-jwt-auth-the-right-way-e60" class="crayons-story__hidden-navigation-link"&gt;Designing JWT Auth the Right Way&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/auberzean" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3853648%2F6c68dc53-c660-4f13-bef4-4fc23dc3f486.jpeg" alt="auberzean profile" class="crayons-avatar__image" width="800" height="1422"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/auberzean" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Pallavi Kumari
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Pallavi Kumari
                
              
              &lt;div id="story-author-preview-content-3543683" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/auberzean" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3853648%2F6c68dc53-c660-4f13-bef4-4fc23dc3f486.jpeg" class="crayons-avatar__image" alt="" width="800" height="1422"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Pallavi Kumari&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/auberzean/designing-jwt-auth-the-right-way-e60" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 24&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/auberzean/designing-jwt-auth-the-right-way-e60" id="article-link-3543683"&gt;
          Designing JWT Auth the Right Way
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/jwt"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;jwt&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/security"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;security&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/authentication"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;authentication&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/auberzean/designing-jwt-auth-the-right-way-e60" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt; reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/auberzean/designing-jwt-auth-the-right-way-e60#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>architecture</category>
      <category>backend</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Designing JWT Auth the Right Way</title>
      <dc:creator>Pallavi Kumari</dc:creator>
      <pubDate>Fri, 24 Apr 2026 02:21:16 +0000</pubDate>
      <link>https://dev.to/auberzean/designing-jwt-auth-the-right-way-e60</link>
      <guid>https://dev.to/auberzean/designing-jwt-auth-the-right-way-e60</guid>
      <description>&lt;p&gt;You log in to an app.&lt;br&gt;&lt;br&gt;
Close the tab. Come back tomorrow.&lt;/p&gt;

&lt;p&gt;You're still logged in.&lt;br&gt;
Convenient? Yes.&lt;/p&gt;

&lt;p&gt;Also the exact place where most JWT implementations quietly go wrong.&lt;/p&gt;

&lt;p&gt;HTTP forgets everything between requests. Every call is a clean slate.&lt;br&gt;&lt;br&gt;
So something, somewhere, is remembering you.&lt;/p&gt;

&lt;p&gt;That "something" is your auth system.&lt;/p&gt;

&lt;p&gt;And JWT-based auth isn't just a technique — it's a series of deliberate trade-offs. Speed vs control. Statelessness vs revocability. Simplicity vs security edge cases. Nobody explains this part. This is the mental model I wish I had from day one.&lt;/p&gt;




&lt;h3&gt;
  
  
  Before we start
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What is JWT ?&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;JWT (JSON Web Token) is an open standard for securely transferring information between two parties as a compact, signed JSON object. The server issues it, the client stores it, and sends it with every request. The server verifies it without ever checking a database — that is what makes it stateless. It is digitally signed using a shared secret via HMAC, or a key pair via RSA or ECDSA. JWTs are fast and scalable, but you cannot revoke a stolen token before it expires. That is the core tradeoff.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;What is bcrypt ?&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;bcrypt is a password hashing function. Unlike regular hashing, it is intentionally slow, which makes brute-force attacks expensive. It also automatically generates and embeds a salt into every hash, so two identical passwords never produce the same hash.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;What is an access token ?&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;A short-lived JWT the client sends with every request to prove identity. The server validates it by recomputing the signature. No database lookup needed. Expires in 15 minutes.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;What is a refresh token ?&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;A long-lived token used only to get a new access token when the old one expires. It is stored server-side as a hash and sent to the client as an&lt;/em&gt; &lt;code&gt;httpOnly&lt;/code&gt; &lt;em&gt;cookie. This is where actual session control lives.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  What JWT Actually Is
&lt;/h2&gt;

&lt;p&gt;A JWT has three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Header&lt;/li&gt;
&lt;li&gt;Payload&lt;/li&gt;
&lt;li&gt;Signature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The server creates it and signs it.&lt;br&gt;&lt;br&gt;
The client stores it.&lt;br&gt;&lt;br&gt;
And on every request, the client sends it back.&lt;/p&gt;

&lt;p&gt;The server verifies the signature to ensure:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This data hasn't been tampered with."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's all a JWT really does.&lt;/p&gt;

&lt;p&gt;JWT is not a session system. Treating it like one is where everything breaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structure*:&lt;/strong&gt;* header.payload.signature (xxxxx.yyyyy.zzzzz)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header: JOSE Header&lt;/strong&gt; (JSON Object Signing and Encryption):
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;signing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;e.g.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;HMAC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;SHA&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;RSA&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;token&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Payload&lt;/strong&gt;: contains claims, key-value statements about an entity
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;registered&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;typically&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth.xyz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;registered&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;issuer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;token&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shared&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;across&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;systems&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"feature_flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;specific&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;application&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signature:&lt;/strong&gt; verifies the payload was not modified in transit
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nc"&gt;HMACSHA256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;base64UrlEncoded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;base64UrlEncoded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;secret&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claim Types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registered: Predefined and recommended. 3-char names to keep tokens compact.&lt;/li&gt;
&lt;li&gt;Public: Custom claims shared across systems. e.g. &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;permissions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Private: App-specific. Not registered or shared. Only meaningful within your own system. e.g. &lt;code&gt;feature_flag&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If signed with a private key, the sender's identity can be verified too, not just the payload integrity.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Flow
&lt;/h2&gt;

&lt;p&gt;A typical JWT flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User logs in&lt;/li&gt;
&lt;li&gt;Server verifies credentials&lt;/li&gt;
&lt;li&gt;Server generates a token&lt;/li&gt;
&lt;li&gt;Client stores the token&lt;/li&gt;
&lt;li&gt;Client sends it with every request&lt;/li&gt;
&lt;li&gt;Server validates and responds&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;But what you've actually done is given the client a self-contained proof of identity that works until it expires.&lt;/p&gt;

&lt;p&gt;No server memory. No built-in revocation.&lt;/p&gt;

&lt;p&gt;That's the trade-off.&lt;/p&gt;




&lt;h3&gt;
  
  
  Sign-up
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /auth/sign-up&lt;/code&gt; with email and password.&lt;/li&gt;
&lt;li&gt;Server hashes the password with &lt;code&gt;bcrypt&lt;/code&gt;. The salt is auto-embedded into the hash, so it never needs to be stored separately.&lt;/li&gt;
&lt;li&gt;User record and password hash are saved to the database. Plain-text password never touches storage.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Login
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /auth/login&lt;/code&gt; with email and password.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bcrypt.compare(password, stored_hash)&lt;/code&gt; extracts the embedded salt, re-hashes the input, and compares.&lt;/li&gt;
&lt;li&gt;On match, server signs the encoded header and payload with a secret to generate the JWT access token.&lt;/li&gt;
&lt;li&gt;A refresh token is minted too. Plain value goes into an &lt;code&gt;httpOnly&lt;/code&gt; cookie. Its hash is stored in the database. Access token goes in the response body, stored in client memory.&lt;/li&gt;
&lt;li&gt;Access token expires in &lt;code&gt;15 min&lt;/code&gt;. Refresh token lasts &lt;code&gt;7 days&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  On every request
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Access token is sent in the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header with every request.&lt;/li&gt;
&lt;li&gt;Server splits the token into header, payload, and signature.&lt;/li&gt;
&lt;li&gt;Recomputes the signature using the secret, header, and payload. A match means the payload is untampered and the request is valid.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;exp&lt;/code&gt; claim is also validated on every request.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Token refresh
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Access token expired? Client sends &lt;code&gt;POST /auth/refresh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Server reads the refresh token from the cookie, hashes it, and compares against &lt;code&gt;refresh_token_hash&lt;/code&gt; in the database.&lt;/li&gt;
&lt;li&gt;If valid, new access token and refresh token are issued. New hash stored, old hash marked revoked, both tokens sent to client. The refresh token rotating on each use is what keeps the session sliding forward for active users.&lt;/li&gt;
&lt;li&gt;If the refresh token is expired too, server rejects and forces re-login.&lt;/li&gt;
&lt;li&gt;Reuse attack: a revoked refresh token being presented signals potential token theft. Server revokes all sessions for that user and forces re-login.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Mistakes (And Why They're Easy to Make)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake #1  : Long-Lived Tokens
&lt;/h3&gt;

&lt;p&gt;If your token lives for days (or worse, weeks), you've already lost control over it.&lt;/p&gt;

&lt;p&gt;Anyone who gets access to that token can act as the user until it expires.&lt;/p&gt;

&lt;p&gt;No kill switch. No recall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use short-lived access tokens (5–15 minutes) with refresh tokens to maintain sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake #2 : Storing Tokens Carelessly
&lt;/h3&gt;

&lt;p&gt;Where you store your token matters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;localStorage&lt;/code&gt; → vulnerable to XSS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sessionStorage&lt;/code&gt; → slightly better, still vulnerable&lt;/li&gt;
&lt;li&gt;cookies → safer &lt;em&gt;if configured properly&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Use &lt;code&gt;httpOnly&lt;/code&gt;, &lt;code&gt;secure&lt;/code&gt;, &lt;code&gt;sameSite&lt;/code&gt; cookies. Avoid exposing tokens to JavaScript when possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake #3: Trusting the Payload Too Much
&lt;/h3&gt;

&lt;p&gt;JWT payload is not encrypted. It's just base64 encoded. Anyone can decode it.&lt;/p&gt;

&lt;p&gt;If you're putting sensitive data inside — emails, roles, permissions — assume it's visible. And if your verification is weak, it can be tampered with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Keep payload minimal. Always verify signatures properly. Never trust client-sent claims blindly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mistake #4: Ignoring Revocation
&lt;/h3&gt;

&lt;p&gt;This is where most implementations fall apart.&lt;/p&gt;

&lt;p&gt;JWT is stateless — which means you can't "log out" a token unless you build extra systems. If a user logs out, changes password, or gets compromised: the token still works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix approaches:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use short-lived tokens&lt;/li&gt;
&lt;li&gt;Rotate refresh tokens&lt;/li&gt;
&lt;li&gt;Maintain a blacklist (if necessary)&lt;/li&gt;
&lt;li&gt;Add token versioning (invalidate on password change)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Design decisions and why
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Access token in memory, not localStorage&lt;/strong&gt; localStorage is readable by any JS on the page, making it an easy XSS target. Memory is scoped to the tab and never exposed to the DOM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh token as an httpOnly cookie&lt;/strong&gt; &lt;code&gt;httpOnly&lt;/code&gt; cookies are fully inaccessible to JavaScript. The browser attaches them automatically, but no client-side code can read the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storing the hash, not the plain refresh token&lt;/strong&gt; A compromised database gives the attacker hashes, not usable tokens. Never store a secret in a form that works as-is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15-minute expiry on the access token&lt;/strong&gt; Short expiry limits the damage if a token is stolen. 5 minutes causes unnecessary refresh churn. 1 hour widens the theft window considerably. 15 is the sweet spot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7-day expiry on the refresh token&lt;/strong&gt; Covers a typical weekly usage pattern. With sliding sessions, active users never hit this limit. 30 days is common but 7 is the more conservative default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sliding session via refresh token rotation&lt;/strong&gt; A fixed expiry abruptly logs out active users. Rotating on each use extends the session for active users and correctly expires idle ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revoke all sessions on refresh token reuse&lt;/strong&gt; A reused revoked token means a replay attack or a stolen token. You cannot tell which, so revoking everything is the only safe response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two tokens instead of one long-lived token&lt;/strong&gt; The access token gives you stateless speed. The refresh token gives you session control. A single long-lived JWT gives you neither safety nor revocability.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  When JWT Is the Wrong Choice
&lt;/h2&gt;

&lt;p&gt;JWT is great when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you need stateless scaling&lt;/li&gt;
&lt;li&gt;multiple services verify tokens&lt;/li&gt;
&lt;li&gt;you want to avoid session storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it's a poor choice when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you need strict session control&lt;/li&gt;
&lt;li&gt;you need instant revocation&lt;/li&gt;
&lt;li&gt;your system is simple and centralized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes, a session-based system is just better.&lt;/p&gt;

&lt;p&gt;JWT isn't the default. It's a trade-off.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Better Mental Model
&lt;/h2&gt;

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

&lt;blockquote&gt;
&lt;p&gt;A signed permission slip that anyone holding it can use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A controllable session stored on your server.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once issued, it's out there. Your job is to limit how long it lives, how much power it has, and how easily it can be abused.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Safer Baseline Setup
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Short-lived access token (5–15 minutes)&lt;/li&gt;
&lt;li&gt;Refresh token with rotation&lt;/li&gt;
&lt;li&gt;httpOnly cookies&lt;/li&gt;
&lt;li&gt;Signature verification on every request&lt;/li&gt;
&lt;li&gt;Minimal payload&lt;/li&gt;
&lt;li&gt;Optional revocation strategy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not perfect. But significantly harder to mess up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmcxtuelfxo88wol0hdt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frmcxtuelfxo88wol0hdt.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;JWT isn't insecure.&lt;br&gt;
But most implementations are.&lt;/p&gt;

&lt;p&gt;They look correct. They pass tests. They work in production.&lt;br&gt;
Until they don't.&lt;/p&gt;

&lt;p&gt;And when they fail, they fail quietly.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;Glossary&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jwt.io" rel="noopener noreferrer"&gt;JWT | JSON Web Token&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc2104" rel="noopener noreferrer"&gt;HMAC | Hash-based Message Authentication Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7515" rel="noopener noreferrer"&gt;JOSE | JSON Object Signing and Encryption&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc8017" rel="noopener noreferrer"&gt;RSA | Rivest-Shamir-Adleman&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6979" rel="noopener noreferrer"&gt;ECDSA | Elliptic Curve Digital Signature Algorithm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>jwt</category>
      <category>security</category>
      <category>webdev</category>
      <category>authentication</category>
    </item>
  </channel>
</rss>
