Gate guardian of the Empire! Architect Vitruvius shows you an alternative method of controlling access to the fort. So far you have learned JWT - passes that the legionary carries with them. Now you will learn Session Management - a system where the fort keeps a registry of who is currently inside!
In the world of legions, we have two approaches to access control:
1// Comparison of JWT vs Session
2// JWT: token stored on the client side
3const jwtApproach = {
4 storage: 'client (localStorage/cookie)',
5 serverState: 'none - stateless',
6 scalability: 'easy scaling - any server verifies the token',
7 revocation: 'difficult - token valid until expiration',
8};
9
10// Session: data stored on the server
11const sessionApproach = {
12 storage: 'server (memory/Redis/MongoDB)',
13 serverState: 'session in storage',
14 scalability: 'requires shared storage',
15 revocation: 'easy - delete session from database',
16};1npm install express-session
2npm install -D @types/express-session
3# Optionally - session store for production:
4npm install connect-redis ioredis
5npm install connect-mongo1// main.ts
2import { NestFactory } from '@nestjs/core';
3import { AppModule } from './app.module';
4import * as session from 'express-session';
5
6async function bootstrap() {
7 const app = await NestFactory.create(AppModule);
8
9 // Session configuration
10 app.use(
11 session({
12 secret: process.env.SESSION_SECRET || 'roma-aeterna-secret',
13 resave: false, // Don't save session if not modified
14 saveUninitialized: false, // Don't create empty sessions
15 cookie: {
16 httpOnly: true, // Not accessible from JavaScript
17 secure: process.env.NODE_ENV === 'production', // HTTPS in production
18 maxAge: 24 * 60 * 60 * 1000, // 24 hours
19 sameSite: 'strict', // CSRF protection
20 },
21 name: 'legion.sid', // Cookie name
22 }),
23 );
24
25 await app.listen(3000);
26}
27bootstrap();NestJS provides the `@Session()` decorator for reading and writing session data:
1// auth/session-auth.controller.ts
2import { Controller, Post, Get, Body, Session, HttpCode } from '@nestjs/common';
3import { AuthService } from './auth.service';
4
5@Controller('auth')
6export class SessionAuthController {
7 constructor(private authService: AuthService) {}
8
9 @Post('login')
10 @HttpCode(200)
11 async login(
12 @Body() loginDto: { username: string; password: string },
13 @Session() session: Record<string, any>,
14 ) {
15 const user = await this.authService.validateUser(
16 loginDto.username,
17 loginDto.password,
18 );
19
20 // Save user data in session
21 session.userId = user.id;
22 session.username = user.username;
23 session.role = user.role;
24 session.loginAt = new Date().toISOString();
25
26 return {
27 message: \\`Welcome to the fort, \\${user.username}!\\`,
28 user: { id: user.id, username: user.username, role: user.role },
29 };
30 }
31
32 @Get('profile')
33 async getProfile(@Session() session: Record<string, any>) {
34 if (!session.userId) {
35 throw new UnauthorizedException('No active session!');
36 }
37
38 return {
39 userId: session.userId,
40 username: session.username,
41 role: session.role,
42 loginAt: session.loginAt,
43 };
44 }
45
46 @Post('logout')
47 @HttpCode(200)
48 async logout(@Session() session: Record<string, any>) {
49 const username = session.username;
50
51 // Destroy session
52 session.destroy((err) => {
53 if (err) console.error('Error destroying session:', err);
54 });
55
56 return { message: \\`See you later, \\${username}!\\` };
57 }
58}In production, you cannot store sessions in memory (lost on restarts). Popular options:
1// main.ts with Redis
2import * as session from 'express-session';
3import RedisStore from 'connect-redis';
4import { Redis } from 'ioredis';
5
6const redisClient = new Redis({
7 host: process.env.REDIS_HOST || 'localhost',
8 port: parseInt(process.env.REDIS_PORT) || 6379,
9});
10
11app.use(
12 session({
13 store: new RedisStore({ client: redisClient }),
14 secret: process.env.SESSION_SECRET,
15 resave: false,
16 saveUninitialized: false,
17 cookie: {
18 httpOnly: true,
19 secure: process.env.NODE_ENV === 'production',
20 maxAge: 24 * 60 * 60 * 1000,
21 },
22 }),
23);1// main.ts with MongoDB
2import * as session from 'express-session';
3import MongoStore from 'connect-mongo';
4
5app.use(
6 session({
7 store: MongoStore.create({
8 mongoUrl: process.env.DATABASE,
9 collectionName: 'sessions',
10 ttl: 24 * 60 * 60, // 24 hours
11 }),
12 secret: process.env.SESSION_SECRET,
13 resave: false,
14 saveUninitialized: false,
15 }),
16);1// auth/guards/session.guard.ts
2import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
3
4@Injectable()
5export class SessionGuard implements CanActivate {
6 canActivate(context: ExecutionContext): boolean {
7 const request = context.switchToHttp().getRequest();
8 const session = request.session;
9
10 if (!session || !session.userId) {
11 throw new UnauthorizedException(
12 'Session expired or does not exist - log in again!'
13 );
14 }
15
16 // Add user data to request
17 request.user = {
18 id: session.userId,
19 username: session.username,
20 role: session.role,
21 };
22
23 return true;
24 }
25}| Aspect | JWT | Session | |--------|-----|---------| | Scaling | Easy (stateless) | Requires shared store | | Revocation | Difficult | Easy (delete from database) | | Size | Larger cookie | Small Session ID | | Microservices | Ideal | More difficult | | SPA + API | Ideal | Possible | | SSR (Next.js) | Possible | Natural | | Mobile apps | Ideal | Less convenient |
Each approach has its advantages - in the Roman Empire, it's worth knowing both ways of guarding access to the fort!