Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
YouTube Security VideosGoogle Cloud Tech: Vibe coding in the pit lane 🏁(23.09.2026 um 01:00 Uhr)
Sichere ProgrammierungBuild an Explainable Vendor-Risk Gate in Node.js(23.09.2026 um 00:27 Uhr)
Sichere ProgrammierungFrom p=none to Enforcement: A Working Sequence for DMARC Rollout(23.09.2026 um 00:40 Uhr)
Sichere ProgrammierungWhen OPA's Bundle Loader Runs Past a `.manifest` Typo(23.09.2026 um 00:53 Uhr)
Sichere ProgrammierungGovernance Attack Surface Review: Bybit(23.09.2026 um 01:00 Uhr)
Linux Tipps & HardeningOpenShot video editor is now available as a snap(23.09.2026 um 00:09 Uhr)
KI & AI VideosAI Revolution: AI Robots Are Beating Humans Now(23.09.2026 um 00:32 Uhr)
YouTube Security VideosGoogle Cloud Tech: Vibe coding in the pit lane 🏁(23.09.2026 um 01:00 Uhr)
Sichere ProgrammierungBuild an Explainable Vendor-Risk Gate in Node.js(23.09.2026 um 00:27 Uhr)
Sichere ProgrammierungFrom p=none to Enforcement: A Working Sequence for DMARC Rollout(23.09.2026 um 00:40 Uhr)
Sichere ProgrammierungWhen OPA's Bundle Loader Runs Past a `.manifest` Typo(23.09.2026 um 00:53 Uhr)
Sichere ProgrammierungGovernance Attack Surface Review: Bybit(23.09.2026 um 01:00 Uhr)
Linux Tipps & HardeningOpenShot video editor is now available as a snap(23.09.2026 um 00:09 Uhr)
KI & AI VideosAI Revolution: AI Robots Are Beating Humans Now(23.09.2026 um 00:32 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

🔐 I Finally Understood JWT Auth - After Building Refresh Token Rotation From Scratch

JWT tutorials only teach the easy part. Here's what happens after. Most auth tutorials end at "user logs in, gets a token, done." And for a while, that felt fine to me too. Then the uncomfortable questions showed up. What if the…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

JWT tutorials only teach the easy part. Here's what happens after.







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



Then the uncomfortable questions showed up.



What if the refresh token is stolen? How do you actually revoke a session? How do you know which device is logged in?



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









😅 The Problem With "Basic" JWT Auth



A lot of beginner tutorials go like this:




  1. ✅ Create a token when the user logs in

  2. ✅ Send it to the client

  3. ✅ Verify it on protected routes



That works. Until it doesn't.



Fully stateless JWT auth makes some critical things hard:




  • You can't easily revoke sessions

  • You can't safely manage multiple devices

  • A stolen refresh token stays valid until it expires (which could be days or weeks)

  • "Logout" becomes a client-side illusion, not a server-side guarantee









🧠 The Core Concept: Controlled Token Lifecycle



Instead of treating refresh tokens like permanent keys, I made them part of a controlled session lifecycle.



Here's the high-level approach:























Token Lifetime Purpose
Access Token Short-lived Used for protected requests
Refresh Token Longer-lived Stored in httpOnly cookie, used to get a new access token


Think of it like this:




🪪 Access token = visitor pass


🔁 Refresh token = a controlled way to ask for a new pass




The access token should be disposable. The refresh token needs stricter handling.









🗺️ The Full Auth Flow



Here's the big picture of how everything fits together:




┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└──────────────────────────────────────────────────┘












♻️ What Refresh Token Rotation Actually Means



When the client asks for a new access token, the server does not keep trusting the same refresh token forever.



Instead:




  1. 🔍 Verify the current refresh token's signature

  2. 🔎 Match its hash to an active, non-superseded session in the DB

  3. 🆕 Generate a new refresh token

  4. 🔄 Mark the old token as superseded (not just replace the hash)

  5. 🚫 Old refresh token stops working immediately




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 ✅






Here's the implementation with theft detection included:




export async function rotateRefreshToken(refreshToken) {
// 1. Verify the incoming token is cryptographically valid
const decoded = jwt.verify(refreshToken, config.JWT_SECRET);
const refreshTokenHash = hashValue(refreshToken);

// 2. Look it up in the DB - find ANY matching session (revoked or not)
const session = await sessionModel.findOne({ refreshTokenHash });

if (!session) {
// Token hash not found at all - truly invalid
throw new AppError(401, "Invalid refresh token");
}

// 3. If the token was already rotated (superseded), this signals THEFT
// Revoke the entire session to protect the legitimate user
if (session.superseded || session.revoked) {
await sessionModel.updateMany(
{ userId: session.userId },
{ revoked: true, revokedAt: new Date() }
);
throw new AppError(401, "Token reuse detected. All sessions revoked.");
}

// 4. Generate a brand new refresh token
const newRefreshToken = signRefreshToken({ id: decoded.id });

// 5. Mark the old token as superseded and store the new hash atomically
session.superseded = true;
session.refreshTokenHash = hashValue(newRefreshToken);
await session.save();

// 6. Return both new tokens to the client
return {
accessToken: signAccessToken({
id: decoded.id,
sessionId: session._id,
}),
refreshToken: newRefreshToken,
};
}







⚠️ Why theft detection matters: 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.










🔐 Why I Hash the Refresh Token (and You Should Too)



This was one of those "oh yeah, of course" moments.



If refresh tokens are powerful, storing them in plain text is risky.



So instead of storing the raw token, I store a SHA-256 hash of it:




import crypto from 'crypto';

function hashValue(value) {
return crypto
.createHash('sha256')
.update(value)
.digest('hex');
}






Why this matters:




























Scenario Without Hashing With Hashing
DB is compromised 💀 Attacker has live tokens ✅ Only useless hashes
Insider threat 💀 Employee can steal sessions ✅ Can't derive tokens from hashes
Data breach logged 💀 Tokens in log files are exploitable ✅ Hashes are worthless



🔑 SHA-256 vs bcrypt - important distinction:


You might wonder: why SHA-256 and not bcrypt like we use for passwords?



Passwords are low-entropy human-chosen strings - they need slow, salted hashing (bcrypt, Argon2) to resist brute force.



Refresh tokens are cryptographically random, high-entropy values (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.










📱 Why Session Tracking Is the Real Upgrade



This is where the system moves from "auth demo" to "actual auth system."



Each login creates a session record in the database:




// Session schema
{
userId: ObjectId, // which user
refreshTokenHash: String, // hashed token (NOT the raw token)
ip: String, // where they logged in from
userAgent: String, // which browser / device
revoked: Boolean, // has this been explicitly killed?
revokedAt: Date, // when was it killed?
superseded: Boolean, // has this token already been rotated?
createdAt: Date,
}






With session tracking, the backend can now answer real questions:




  • ✅ Which sessions are currently active?

  • ✅ Which device or browser logged in?

  • ✅ Has this session been revoked?

  • ✅ Was this token already rotated (possible theft)?

  • ✅ Should this refresh token still be trusted?




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. ✅






Without this, JWT auth is like sending signed permission slips into the wild and hoping they behave.









🤔 Why Not Just Use Stateless JWT Everywhere?



Because stateless JWT is great - until you need stateful behavior.



And auth almost always needs stateful behavior:











































Need Stateless JWT With Session Tracking
Logout one device ❌ Token lives until expiry ✅ Revoke that session
Logout all devices ❌ Impossible cleanly ✅ Revoke all sessions
Immediate invalidation ❌ Can't do it ✅ Flip revoked: true
Detect stolen tokens ❌ No awareness ✅ Reuse detection built-in
Track suspicious sessions ❌ No awareness ✅ IP + userAgent logged
Rotate tokens safely ❌ Risky ✅ Hash rotation + superseded flag



💡 The biggest insight:


JWT alone solves identity proof.


Session state solves lifecycle control.


In real apps, lifecycle control matters a lot.



⚖️ One honest tradeoff: Including sessionId 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.










😵 What Was Actually Hard to Build



The tricky part wasn't getting token generation to work. Generating tokens is easy.



The hard part was making the flow safe and consistent:




  • 🔄 Making rotation happen without breaking the client's flow

  • 🚫 Ensuring old refresh tokens immediately stop working

  • 🕵️ Building reuse detection that protects users when tokens are stolen

  • 🏗️ Designing sessions so they can be revoked individually or all at once

  • 🧪 Keeping the logic clean enough to actually test



This is the gap between a "tutorial auth system" and something production-worthy.









📚 Key Lessons Learned



1. Refresh tokens shouldn't be treated casually


They're long-lived and powerful. Hash them. Rotate them. Track them. Detect reuse.



2. Session tracking gives your backend real control


Without it, you're flying blind. You can't revoke what you can't see.



3. Stateless auth is great - for the right problems


Short-lived access tokens? Stateless is perfect. Long-lived refresh tokens? You need server state.



4. Revocation becomes easy when sessions exist


Want to kill a session? Set revoked: true. Done. No waiting for token expiry.



5. Reuse detection is what makes rotation actually secure


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.



6. Multiple devices change everything


The moment you support multiple devices, you need per-device session tracking. There's no clean alternative.









🎯 Final Thoughts



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.



These two ideas changed the way I think about auth systems entirely.



The biggest lesson?




JWT gives you identity. Sessions give you control. You need both.







If you've built auth before, I'd love to know how you handled refresh tokens and sessions. Drop your approach in the comments 💬






Tags: #node #security #webdev #javascript

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten 🔐 I Finally Understood JWT Auth - After Building Refresh Token Rotation From Scratch

Thematisch verwandte Begriffe: Finally, Understood, Auth, After · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-58268 | SIPGO is a library for writing SIP services in the GO language. Prior to…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick