<?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: Parveen Kumari</title>
    <description>The latest articles on DEV Community by Parveen Kumari (@pku_bd13f856f0).</description>
    <link>https://dev.to/pku_bd13f856f0</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%2F3934879%2Fef41f426-097b-4578-b558-0bf4377be89b.jpg</url>
      <title>DEV Community: Parveen Kumari</title>
      <link>https://dev.to/pku_bd13f856f0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pku_bd13f856f0"/>
    <language>en</language>
    <item>
      <title>JWT Authentication, Explained by Actually Running One (No Setup)</title>
      <dc:creator>Parveen Kumari</dc:creator>
      <pubDate>Sat, 16 May 2026 13:08:14 +0000</pubDate>
      <link>https://dev.to/pku_bd13f856f0/jwt-authentication-explained-by-actually-running-one-no-setup-3l70</link>
      <guid>https://dev.to/pku_bd13f856f0/jwt-authentication-explained-by-actually-running-one-no-setup-3l70</guid>
      <description>&lt;p&gt;Decode a real JWT, exploit alg:none in 30 seconds, and learn exactly what to test in your own auth — all in your browser against a live sandbox&lt;/p&gt;

&lt;p&gt;Most JWT tutorials show you a diagram and call it a day. This one is different: every example runs against a real sandbox API in your browser, so you can decode tokens, exploit &lt;code&gt;alg: none&lt;/code&gt;, and watch a server actually reject (or accept) what you throw at it.&lt;/p&gt;

&lt;p&gt;If you've ever signed off on a JWT implementation without being 100% sure what the library is doing under the hood — this is for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a JWT actually is&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A JWT (JSON Web Token, pronounced "jot") is a signed, self-contained string that carries claims about a user, used to authenticate requests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Self-contained&lt;/em&gt; is the magic word. The server doesn't need to look up a session — the token itself has the user info plus a signature proving it hasn't been tampered with. The server just verifies the signature and trusts the claims.&lt;/p&gt;

&lt;p&gt;** The three parts&lt;/p&gt;

&lt;p&gt;A JWT is three Base64URL-encoded segments separated by dots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWxpY2UifQ.sig_bytes_here
↑ header              ↑ payload                              ↑ signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decode the first two and you get JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Header:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&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="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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Payload (claims):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&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;"user-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&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="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1712345678&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1712349278&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;p&gt;&lt;strong&gt;Signature:&lt;/strong&gt; &lt;code&gt;HMAC-SHA256(header.payload, secret)&lt;/code&gt; — or an RSA/ECDSA signature for asymmetric algorithms.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Anyone can decode a JWT. The payload is &lt;em&gt;signed&lt;/em&gt;, not &lt;em&gt;encrypted&lt;/em&gt;. Never put secrets (passwords, credit-card numbers, anything you'd be sad to see in a log) in a JWT.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Standard claims (RFC 7519)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Issuer — who created the token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Subject — who the token is about (user ID)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Audience — who the token is for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Expiration — Unix timestamp after which the token is invalid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nbf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not Before — Unix timestamp before which it's invalid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Issued At — when it was created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jti&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT ID — unique identifier for revocation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus any custom claims your app needs: &lt;code&gt;role&lt;/code&gt;, &lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;permissions&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a login and get a real token
&lt;/h2&gt;

&lt;p&gt;Here's the login request — &lt;code&gt;POST /auth/login&lt;/code&gt; against the public sandbox:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST https://demo.totalshiftleft.ai/auth/login
Content-Type: application/json

{
  "email": "demo@totalshiftleft.ai",
  "password": "demo123"
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"access_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJhbGciOi..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"refresh_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJhbGciOi..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;900&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;p&gt;Want to actually run it and see the real token come back?&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://totalshiftleft.ai/learn/authentication/jwt-authentication" rel="noopener noreferrer"&gt;Run this live in your browser — no signup&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then call a protected endpoint with the token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://demo.totalshiftleft.ai/api/v1/me
Authorization: Bearer eyJhbGciOi...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop the &lt;code&gt;Authorization&lt;/code&gt; header and you get a 401. That's the whole flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the server verifies a token
&lt;/h2&gt;

&lt;p&gt;On every request, the server runs through this checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;Split the token into header, payload, signature.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check &lt;code&gt;alg&lt;/code&gt; matches the expected algorithm — reject &lt;code&gt;none&lt;/code&gt; explicitly.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recompute the signature&lt;/strong&gt; over &lt;code&gt;header.payload&lt;/code&gt; using the secret/public key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare&lt;/strong&gt; the computed signature to the provided one — in constant time.&lt;/li&gt;
&lt;li&gt;Parse the payload. Check &lt;code&gt;exp&lt;/code&gt; is in the future and &lt;code&gt;nbf&lt;/code&gt; is in the past.&lt;/li&gt;
&lt;li&gt;Optionally check &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt;, &lt;code&gt;jti&lt;/code&gt; against a revocation list.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Only now&lt;/em&gt; treat the claims as trusted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skipping step 3 or 4 is the classic vulnerability — and several major libraries have shipped it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seven JWT vulnerabilities you should be able to test for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;alg: none&lt;/code&gt;.&lt;/strong&gt; Attacker changes the header to &lt;code&gt;{"alg":"none"}&lt;/code&gt;, drops the signature, and forges any payload. Only works if the library accepts &lt;code&gt;none&lt;/code&gt;. Reject it explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;alg&lt;/code&gt; confusion (RS256 → HS256).&lt;/strong&gt; Attacker flips &lt;code&gt;alg&lt;/code&gt; from &lt;code&gt;RS256&lt;/code&gt; to &lt;code&gt;HS256&lt;/code&gt; and signs with the server's &lt;em&gt;public&lt;/em&gt; key as if it were the HMAC secret. Naive libraries verify it as HMAC against the public key (which is, well, public) and accept it. Always pin the algorithm server-side. Never trust the header's &lt;code&gt;alg&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Weak HMAC secrets.&lt;/strong&gt; 8-character secrets can be brute-forced offline from a single valid token. Use ≥256 bits of entropy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Long expirations.&lt;/strong&gt; &lt;code&gt;exp&lt;/code&gt; 30 days out means a stolen token is usable for 30 days. Access tokens should live ~15 minutes; long-lived refresh tokens do the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. No revocation.&lt;/strong&gt; JWTs are stateless by design — which makes revocation hard. Mitigations: short expiry + refresh tokens, &lt;code&gt;jti&lt;/code&gt; blacklist, or per-user "tokens issued before X are invalid" timestamps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Storing JWTs in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/strong&gt; Vulnerable to XSS. Use &lt;code&gt;httpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite&lt;/code&gt; cookies for browser clients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Generous clock skew.&lt;/strong&gt; A 5-minute window is fine. A 24-hour window is a vulnerability.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually test
&lt;/h2&gt;

&lt;p&gt;If you're writing tests for a JWT-protected API — or QA-ing one — here's the checklist I actually use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Happy paths
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Login with valid creds → 200 with &lt;code&gt;access_token&lt;/code&gt;, &lt;code&gt;refresh_token&lt;/code&gt;, &lt;code&gt;expires_in&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Protected endpoint with valid token → 200.&lt;/li&gt;
&lt;li&gt;Token includes expected claims (&lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;role&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication negatives
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;Authorization&lt;/code&gt; header → 401.&lt;/li&gt;
&lt;li&gt;Wrong scheme (&lt;code&gt;Basic&lt;/code&gt; instead of &lt;code&gt;Bearer&lt;/code&gt;) → 401.&lt;/li&gt;
&lt;li&gt;Malformed token (missing a segment) → 401.&lt;/li&gt;
&lt;li&gt;Expired token (&lt;code&gt;exp&lt;/code&gt; in the past) → 401 with a distinguishable code (e.g., &lt;code&gt;TOKEN_EXPIRED&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Not-yet-valid token (&lt;code&gt;nbf&lt;/code&gt; in the future) → 401.&lt;/li&gt;
&lt;li&gt;Token signed with the wrong secret → 401.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;alg: none&lt;/code&gt; token → 401. &lt;strong&gt;This is a security-critical test — most JWT libraries have shipped this bug at some point.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Tampered payload (modify a claim, keep the old signature) → 401.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authorization negatives
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Valid token with &lt;code&gt;role: user&lt;/code&gt; hitting an admin endpoint → 403.&lt;/li&gt;
&lt;li&gt;Valid token but &lt;code&gt;tenant_id&lt;/code&gt; doesn't own the resource → 403 or 404 (document which — both are defensible).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Edge cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Token issued by a different &lt;code&gt;iss&lt;/code&gt; → 401.&lt;/li&gt;
&lt;li&gt;Token with &lt;code&gt;aud&lt;/code&gt; not matching this API → 401.&lt;/li&gt;
&lt;li&gt;Token with a huge payload (10 KB of custom claims) → should work or 413.&lt;/li&gt;
&lt;li&gt;Multiple &lt;code&gt;Authorization&lt;/code&gt; headers → document the behavior (most stacks take the first).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Refresh flow
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Valid refresh token → new access + refresh pair; old refresh token now invalid.&lt;/li&gt;
&lt;li&gt;Re-using an old refresh token → 401 (detects token theft).&lt;/li&gt;
&lt;li&gt;Refresh token after the user changes their password → 401. A password change should invalidate tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pen-tester checklist (steal this)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Copy the JWT, change &lt;code&gt;alg&lt;/code&gt; to &lt;code&gt;none&lt;/code&gt;, remove the signature — does it still work? If yes: critical bug.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;alg&lt;/code&gt; is &lt;code&gt;RS256&lt;/code&gt;, try resigning with &lt;code&gt;HS256&lt;/code&gt; using the server's public key — does it work? If yes: critical bug.&lt;/li&gt;
&lt;li&gt;Decode the payload. Anything that shouldn't be there (passwords, PII, internal IDs that leak architecture)? Report as data exposure.&lt;/li&gt;
&lt;li&gt;Does the token include a &lt;code&gt;jti&lt;/code&gt;? If not, revocation is likely not implemented.&lt;/li&gt;
&lt;li&gt;Set the system clock past &lt;code&gt;exp&lt;/code&gt; — does the token still work? Clock-skew vulnerability.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;Reading about JWTs is fine. Watching one fail validation in real time is better.&lt;/p&gt;

&lt;p&gt;The full lesson — with a runnable login, a real protected endpoint, and the &lt;code&gt;alg: none&lt;/code&gt; exploit live against a sandbox — is here, free, no signup:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://totalshiftleft.ai/learn/authentication/jwt-authentication" rel="noopener noreferrer"&gt;Learn JWT Authentication — runnable lesson&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's part of a free 32-lesson API testing course covering REST, GraphQL, SOAP, OAuth2, contract testing, and AI-assisted testing. Every lesson has a runnable example against a live sandbox: &lt;a href="https://totalshiftleft.ai/learn" rel="noopener noreferrer"&gt;totalshiftleft.ai/learn&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;What's your favorite JWT footgun in production? Drop it in the comments — I'm collecting them for the next post on refresh-token rotation patterns.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>ai</category>
      <category>api</category>
    </item>
  </channel>
</rss>
