blog.dopana

Back

JSON Web Token (JWT) is one of the most widely adopted standards for authentication in modern web development. From Single Page Applications (React, Vue, Svelte) to Microservices and Mobile APIs, JWT is ubiquitous.

However, implementing JWT correctly and securely remains a significant challenge for many engineers: Where should tokens be stored? How should session expiration and token refresh be handled? How can we defend against both XSS and CSRF?

Let’s break down the complete architecture of Web Authentication with JWT from the ground up.

1. Explain Like I’m 10 (ELI5): Locker Tickets vs. VIP Wristbands#

To understand why JWT was invented, compare it to traditional session-based authentication using the analogy of a theme park:

Traditional Approach (Session-based): “Locker Keys and Ledger”#

  • When you enter the park, the front desk gives you a numbered locker ticket (Session ID).
  • All your personal details (Name, VIP status, entry timestamp) are written in a master ledger book behind the front desk (Database or Redis).
  • Every time you want to ride a roller coaster, the operator must call the front desk: “Can you check the ledger to see if ticket #102 has VIP access?”.
  • The Bottleneck: When 100,000 visitors ride 50 attractions simultaneously, the front desk phone lines crash under the weight of continuous lookups.

Modern Approach (JWT-based): “Digitally Signed VIP Wristbands”#

  • When you enter the park, the front desk hands you a smart electronic wristband (JWT).
  • Printed on the wristband itself is: Name: Alice, Tier: VIP, Expires: 18:00.
  • The management applies a tamper-proof digital seal (Cryptographic Signature) to the wristband.
  • Whenever you enter a ride, the operator simply verifies the digital seal on the spot and reads your VIP status immediately — no central database lookup required.
flowchart TD
    subgraph Traditional["1. Traditional Stateful Sessions"]
        Client1["Client"] -->|Sends Session ID| Server1["App Server"]
        Server1 -->|DB Lookup Every Request| DB["Database / Redis<br/>Session Store"]
    end

    subgraph JWTWay["2. Stateless JWT Authentication"]
        Client2["Client"] -->|Sends JWT Token| Server2["App Server"]
        Server2 -->|Validates Signature Locally| Server2
    end

2. The Three Parts of a JSON Web Token#

A JWT is a compact, URL-safe string made of three Base64URL-encoded parts separated by dots (.):

Header.Payload.Signature\text{Header} . \text{Payload} . \text{Signature}

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiQWxpY2UiLCJyb2xlIjoiYWRtaW4iLCJleHAiOjE3MDAwMDAwMDB9.4a5b6c...
text
  1. Header: Defines the token type (JWT) and cryptographic algorithm (e.g., HS256 or RS256).
  2. Payload (Claims): Contains the identity claims being transmitted, such as sub (User ID), role (Permissions), exp (Expiration timestamp), and iat (Issued-at timestamp).
  3. Signature: Generated by taking the encoded Header and Payload and signing them with a private server secret or asymmetric private key.

[!WARNING] Payload is NOT encrypted! It is only Base64URL encoded. Anyone can decode and inspect its contents. Never place passwords, credit card numbers, or secret API keys in a JWT payload!

3. Production Architecture: The Dual-Token Pattern#

Using a single JWT with a long lifetime is dangerous if intercepted. Conversely, a very short token forces users to log in repeatedly.

The industry-standard solution is the Dual-Token Pattern:

  • Access Token: Very short-lived (10 - 15 minutes), attached to incoming API requests for fast authorization.
  • Refresh Token: Longer-lived (7 - 30 days), stored securely and used exclusively to request new Access Tokens without user disruption.
flowchart TD
    User["Client Browser"]
    AuthServer["Auth API Server"]
    ResourceServer["Protected Resource Server"]

    User -->|1. POST /login - Credentials| AuthServer
    AuthServer -->|2. Returns Access Token and Refresh Cookie| User
    
    User -->|3. API Request with Bearer Token| ResourceServer
    ResourceServer -->|4. Authorized Response 200 OK| User

    User -->|5. Token Expired 401 Unauthorized| ResourceServer
    User -->|6. POST /refresh with HttpOnly Cookie| AuthServer
    AuthServer -->|7. Issues Fresh Access Token - Rotation| User

4. Where Should You Store Tokens on the Client? (XSS vs. CSRF)#

Choosing where to persist tokens on the client is critical to security:

Storage LocationXSS RiskCSRF RiskEvaluation
localStorage / sessionStorage❌ Critical (Accessible by any malicious injected JS script)✅ Immune (Not automatically transmitted by browsers)Not recommended for long-lived tokens
Standard Cookie (No HttpOnly)❌ Critical (Readable via document.cookie)❌ High (Automatically attached across origins)High risk
HttpOnly + Secure + SameSite Cookie✅ Protected (JavaScript cannot read the cookie)✅ Protected (SameSite=Lax/Strict blocks cross-site triggers)Recommended for Refresh Tokens
In-Memory Variable (JS State)✅ Protected (Isolated in memory, wiped on tab close)✅ Protected (Browser does not auto-attach)Recommended for Access Tokens

Best Practice Architecture#

  1. Access Token: Held strictly in In-Memory JavaScript State (e.g., React Context, Pinia, Svelte Store).
  2. Refresh Token: Persisted in an HttpOnly Cookie with Secure and SameSite=Lax (or Strict).
  3. When the user opens or refreshes a tab, the client performs a Silent Refresh (POST /auth/refresh) to populate the in-memory Access Token.

5. Practical Implementation with Node.js & TypeScript#

5.1. Authentication Controller (Login & Issuing Tokens)#

5.2. Protected Route Middleware#

5.3. Refresh Token Endpoint (Token Rotation)#

6. Common JWT Security Pitfalls#

[!TIP] JWT Security Checklist:

  1. Strong Secrets: Use at least 256-bit cryptographically random secrets or asymmetric RSA (RS256) / ECDSA (ES256) keypairs.
  2. Reject "alg": "none": Always configure your validator to explicitly whitelist trusted algorithms.
  3. Validate Audience (aud) and Issuer (iss): Ensure tokens were issued by your identity provider specifically for your API.
  4. Implement Token Rotation: Invalidate old refresh tokens upon renewal to detect and mitigate replay attacks.

7. Summary#

Using JWT for Web Authentication provides unmatched scalability and flexibility for modern distributed systems. By adopting the Dual-Token Pattern, storing Access Tokens in Memory, and securing Refresh Tokens in HttpOnly SameSite Cookies, you achieve the optimal balance of user experience and enterprise-grade security.

References#