Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
Windows Tipps & SecurityGrafikkarte vor Überhitzung schützen: So geht’s(25.09.2026 um 08:00 Uhr)
••••••••••
Windows Tipps & SecurityGrafikkarte vor Überhitzung schützen: So geht’s(25.09.2026 um 08:00 Uhr)
••••••••••
Intelligence View
⚡ tsecurity.de Intelligence

Building Secure Session Management in NestJS — Refresh Tokens, Device Tracking & Session Revocation(PART 1)

1. The Hook — Why "Just Use JWT" Isn't Enough If you've built auth before, you've probably done this: User logs in → you generate a JWT → you send it back → done. And it works! For a weekend project. But let me ask you a few questio…

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






1. The Hook — Why "Just Use JWT" Isn't Enough



If you've built auth before, you've probably done this:




  1. User logs in → you generate a JWT → you send it back → done.
    And it works! For a weekend project. But let me ask you a few questions:
    What happens if a user's token gets stolen? Can you invalidate it?
    If a user changes their password, are their other logged-in devices kicked out?
    Can your users see where they're logged in — phone, laptop, browser — and revoke specific sessions?
    Do you even know how many active sessions exist right now?
    If you answered "no" to any of these, this article is for you.



In this post I'll walk through the entire thing — from the problem, to the architecture, to the actual code. By the end, you'll understand exactly how to implement production-grade session management in NestJS.






2. The Problem — What Breaks Without Proper Session Management



Let's start with a quick primer on how JWT auth normally works.



What is a JWT?

A JWT (JSON Web Token) is a string that encodes information — like a user's ID and role — and is cryptographically signed. It looks like this:




eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTEyMyIsImVtYWlsIjoiam9lQGV4YW1wbGUuY29tIn0.abc123






Three parts, separated by dots: header, payload, signature. You don't need to memorize this — just know that:




  • The payload contains data you encoded (user ID, role, expiry time)


  • The signature proves it wasn't tampered with


  • Anyone with a valid token can use it until it expires — you can't cancel it early




That last point is the key problem.



The "Hotel Key Card" Analogy

Think of a JWT like a hotel key card. When you check in, the front desk gives you a card that opens your room. It works for the duration of your stay.



Now imagine someone steals your key card. The hotel can't remotely deactivate that specific card — once it's issued, it opens the door until it expires.



Standard JWT auth has this exact problem:



No revocation. Once issued, a token is valid until expiry. If it's stolen, you can't stop it.

No visibility. You have no idea how many "copies" of a session exist or where they're being used from.

Password changes don't log out other devices. A user changes their password thinking they're secure — but any stolen tokens still work.

No real logout. Traditional JWT "logout" just deletes the token from localStorage. The token itself is still perfectly valid on the server.



What we had before

Here's a basic auth service:




// The "naive" version — functional but not production-safe
async login(email: string, password: string) {
const user = await this.validateUser(email, password);

if (!user) { throw new UnauthorizedException('Invalid credentials');}

const payload = { email: user.email, sub: user.id, role: user.role };

// Access token — valid for 15 minutes
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' });

// Refresh token — valid for 7 days (more on this below)
const refreshToken = this.jwtService.sign(payload,{
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
expiresIn: '7d',
});

return { access_token: accessToken, refresh_token: refreshToken };
}






This works, but there's no database record of this session. No way to revoke it. No device info. No IP. If someone grabs that refresh_token,they can silently keep refreshing access for 7 days.



Let's fix that.






3. The Architecture — JWT + Database-Backed Sessions



First: What's the difference between an access token and a refresh token?

This trips up a lot of beginners, so let's nail it before going further.



When a user logs in, we issue two tokens:




  • Access token — Short-lived (15 minutes). The client sends this with every API request. It's like your boarding pass: valid, fast to check, but expires quickly.


  • Refresh token — Long-lived (7 days). The client only uses this to request a new access token when the old one expires. It's like the ID you showed at check-in to get the boarding pass.




Why two tokens instead of one long-lived one? Because access tokens are sent constantly — if one leaks, the damage window is small (15 minutes max). The refresh token is only used occasionally and can be stored more securely.



The solution: hybrid JWT + DB sessions

We combine stateless JWTs with stateful database sessions. You get the performance benefits of JWTs for most requests, plus the control of server-side sessions when you need it.



Here's the high-level flow:




User logs in
│
▼
Validate credentials
│
▼
Create a Session record in the DB
(stores: refresh token hash, device info, IP, timestamps)
│
▼
Issue access token (JWT, 15 min) + refresh token (JWT, 7 days)
│
▼
User makes API requests using the short-lived access token
│
▼
Access token expires → client sends refresh token to /auth/refresh
│
▼
Look up session in DB → if found & valid → issue new access token
│
▼
On logout → delete session from DB (refresh token is now worthless)






The key insight: the refresh token is only as good as the session record in your database. Even if someone steals the refresh token, if you delete the session record, the token is dead.






4. The Session Entity — What Gets Stored Per Session



Let's start with the data model using typeOrm as our ORM(Object-Relational Mapper). A session represents one login event — one device, one browser, one app install.




// session.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';


import { User } from '../users/entities/user.entity';

@Entity('sessions')
export class Session {
@PrimaryGeneratedColumn('uuid')
id: string;

// Which user this session belongs to.
// onDelete: 'CASCADE' means if the user is deleted, their sessions are too.

@ManyToOne(() => User, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'userId' })
user: User;

@Column()
userId: string;

// We store a HASH of the refresh token, never the raw token.
// Same reason you hash passwords — if the DB leaks, tokens are useless.
// select: false means this field is excluded from queries unless explicitly requested.

@Column({ select: false })
refreshTokenHash: string;

// Device & browser info (captured from the User-Agent header on login)
@Column({ nullable: true })
deviceType: string; // e.g. 'mobile', 'desktop', 'tablet'

@Column({ nullable: true })
browser: string; // e.g. 'Chrome', 'Safari', 'Firefox'

@Column({ nullable: true })
os: string; // e.g. 'Windows', 'macOS', 'Android'

// Where the login came from
@Column({ nullable: true })
ipAddress: string;

// The raw User-Agent string, kept for debugging
@Column({ nullable: true })
userAgent: string;

// When does this session expire? (should match the refresh token's expiry)
@Column({ type: 'timestamp' })
expiresAt: Date;

// Last time this session was used to get a new access token
@Column({ type: 'timestamp', nullable: true })
lastUsedAt: Date;

@CreateDateColumn()
createdAt: Date;

// Soft-delete flag: we mark sessions inactive rather than deleting them immediately.

// Useful for audit logs and security investigations.

@Column({ default: true })
isActive: boolean;

}






A few important design decisions here:



We hash the refresh token before storing it, just like passwords. If your database is ever compromised, the raw tokens are never exposed.

isActive flag lets you soft-delete sessions for auditing, rather than hard-deleting immediately.

expiresAt lets you write a cleanup cron job to purge old sessions periodically.





5. DeviceInfoMiddleware — Capturing Device Info on Every Login



When a user logs in from their iPhone, we want to record "iPhone, Safari, iOS 17". When they log in from their laptop, "Chrome, macOS". This is how apps like Google show you "Signed in on 3 devices."



What is a middleware?

In NestJS (and Express), a middleware is a function that runs before your route handler. Think of it as a checkpoint — every request passes through it, and you can attach extra information to the request object.



We get device info from the User-Agent HTTP header — a string the browser automatically sends with every request. You can parse it with the ua-parser-js library:




// device-info.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as UAParser from 'ua-parser-js';

// Extend Express's Request type so TypeScript knows about our custom property.
// This is TypeScript "declaration merging" — we're adding to an existing type.

declare global {
namespace Express {
interface Request {
deviceInfo?: {
deviceType: string;
browser: string;
os: string;
userAgent: string;
ipAddress: string;
};
}}}

@Injectable()
export class DeviceInfoMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const userAgentString = req.headers['user-agent'] || '';

// ua-parser-js parses the User-Agent string into structured data
const parser = new UAParser(userAgentString);
const result = parser.getResult();

// Determine if this is mobile, tablet, or desktop.
// Note: ua-parser-js returns undefined for desktops, so we default to 'desktop'.

const deviceType = result.device.type || 'desktop';

// Get the real IP address.
// When you're behind a proxy or load balancer (like nginx or Cloudflare),
// the real client IP is in the 'x-forwarded-for' header, not req.ip.

const ipAddress =
(req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ||
req.ip ||'unknown';

// Attach device info to the request object.
// Our auth service will read it from here during login.

req.deviceInfo = {
deviceType,
browser: result.browser.name || 'Unknown Browser',
os: result.os.name || 'Unknown OS',
userAgent: userAgentString,
ipAddress,
};

// Call next() to pass control to the next middleware or the route handler
next();
}
}






Register the middleware only on the login route in your auth module:




// auth.module.ts
import {
MiddlewareConsumer,
Module,
NestModule,
RequestMethod,
} from '@nestjs/common';

import { DeviceInfoMiddleware } from './device-info.middleware';
@Module({
// ... imports, providers, exports
})

export class AuthModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(DeviceInfoMiddleware)

// Only apply to POST /auth/login
.forRoutes({ path: 'auth/login', method: RequestMethod.POST });
}}









6. The Login Flow — Creating a Session on Login



Now let's update the login method. After validating credentials, we create a session record in the database.




// auth.service.ts — updated login method
async login(
email: string,
password: string,
deviceInfo?: Request['deviceInfo'],
) {
// Step 1: Validate credentials (same as before)
const user = await this.validateUser(email, password);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}

// The JWT payload — this is what gets encoded inside the token
const payload = { email: user.email, sub: user.id, role: user.role };

// Step 2: Issue both tokens
const accessToken = this.jwtService.sign(payload, {
expiresIn: '15m', // Short-lived
});

const refreshToken = this.jwtService.sign(payload, {
secret: this.configService.get<string>('JWT_REFRESH_SECRET'),
expiresIn: '7d', // Long-lived
});

// Step 3: Hash the refresh token before storing it.
// We use bcrypt here (same library used for passwords).
const refreshTokenHash = await bcrypt.hash(refreshToken, 10);

// Step 4: Calculate when this session should expire (7 days from now)
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);

// Step 5: Create the session record in the database

const session = this.sessionRepository.create({
userId: user.id,
refreshTokenHash, // store the hash, never the raw token
expiresAt,

// Device info from the middleware (falls back gracefully if not present)

deviceType: deviceInfo?.deviceType || 'unknown',
browser: deviceInfo?.browser || 'unknown',
os: deviceInfo?.os || 'unknown',
userAgent: deviceInfo?.userAgent || '',
ipAddress: deviceInfo?.ipAddress || 'unknown',

isActive: true,
});
await this.sessionRepository.save(session);

return {
access_token: accessToken,
refresh_token: refreshToken, // raw token sent to client; hash stays in DB

expires_in: 900, // 15 minutes in seconds

session_id: session.id, // the client uses this to revoke this specific session later

user: {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
},
};
}






Update the controller to pass device info through:




// auth.controller.ts
@Post('login')
@ApiOperation({ summary: 'Login user' })
async login(
@Body() body: { email: string; password: string },
@Req() req: Request,
) {
// req.deviceInfo was attached by DeviceInfoMiddleware before this handler ran

return this.authService.login(body.email, body.password, req.deviceInfo);

}






We’ve now laid the foundation: why plain JWTs fall short, how database‑backed sessions solve those gaps, and how to capture device info cleanly in NestJS.

By now, you should see that production‑grade authentication isn’t just about issuing tokens — it’s about visibility, control, and security.

We’ve set up the building blocks in this first part.



In Part 2, we’ll go deeper — covering refresh flows, logout mechanics, session revocation,and give users the power to manage their own sessions. Stay tuned!



Follow @bismark66 for more backend engineering deep dives.

1. Sofort-Triage & Abwehrmaßnahmen

SOC Incident Playbook: Vulnerability Remediation & Verification
Syntax validiert (0 Fehler)
title: Detect Exploitation - Building Secure Session Management in NestJS — Refresh Tokens, Device Tracking & Session Revocation(PART 1)
id: bbb9f24b-a87e-4259-97cc-19d7f712d61f
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-27
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
Syntax validiert (0 Fehler)
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-27"
        description = "YARA Signature for "
    strings:
        $str = "Building Secure Session Manage" ascii wide
    condition:
        any of them
}
Syntax validiert (0 Fehler)
index=security sourcetype IN ("cisco:asa", "pan:traffic", "zeek_conn", "suricata", "WinEventLog:Security")
("Building Secure Session Management in Ne")
| stats count earliest(_time) as first_seen latest(_time) as last_seen by src_ip, dest_ip, dest_host, signature
| eval first_seen=strftime(first_seen, "%Y-%m-%d %H:%M:%S"), last_seen=strftime(last_seen, "%Y-%m-%d %H:%M:%S")
| sort - count
Syntax validiert (0 Fehler)
message: "*Building Secure Session Management in Ne*"
Syntax validiert (0 Fehler)
CommonSecurityLog
| where Message has "Building Secure Session Management in Ne"
| summarize EventCount = count(), FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated) by SourceIP, DestinationIP, DestinationPort, Activity
| extend DetectionRule = "iShareStuff-CTI-Compiled"
| sort by EventCount desc

2. Cyber Threat Intelligence & Forensik

CTI Threat Relationship Graph2 Knoten / 1 Relationen
CVE / Incident Software MITRE ATT&CK CWE Weakness IoC
🎯
MITRE ATT&CK Matrix Navigator 14 Taktiken
Reconnaissance
-
Resource Development
-
Initial Access
Execution
Persistence
-
Privilege Escalation
Defense Evasion
Credential Access
-
Discovery
-
Lateral Movement
-
Collection
-
Command and Control
Exfiltration
-
Impact
tsecurity.de Cognitive Threat RAG
Fokus-Vektor:

Analyse für identifizierte Bedrohung auf Basis von Live-CTI (ENISA EUVD): CVSS 0.0 · EPSS 0.0% · CISA KEV: nein. Handlungsableitung aus den verlinkten Hersteller-Quellen.

🛡️ Angriffsfläche & Exposure

Netzwerk/Remote-Zugriff ohne Vorauthentifizierung möglich.

⚡ Empfohlene Sofortmaßnahmen
  • 1. Perimeter-Inspektion: Relevante Portfreigaben und exponierte Endpunkte unverzüglich scannen.
  • 2. Patch-Applikation: Hersteller-Hotfix einspielen oder betroffene Daemons in isolierte DMZ-Segmente überführen.
  • 3. Telemetrie & EDR-Alerts: Prozessaufrufe und Child-Processes auf anomale Shell-Spawns überwachen.
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building Secure Session Management in NestJS — Refresh Tokens, Device Tracking & Session Revocation(PART 1)

Thematisch verwandte Begriffe: Building, Secure, Session, Management · 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 ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-100620 | Capgo CLI (npm package @capgo/cli) through 7.98.2 is affected by an ove…
Advisory →
tsecurity.de Icon
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