Sessions & Cookies
Secure session management with cookies, express-session, and Redis.
6 min read
Sessions vs Tokens#
| Aspect | Sessions | JWT Tokens |
|---|---|---|
| Storage | Server-side | Client-side |
| Stateful | Yes | No |
| Revocation | Easy | Requires blocklist |
| Scalability | Need shared store | Stateless |
| Size | Small cookie | Larger payload |
Cookie Basics#
javascript
// Setting cookies
res.cookie('sessionId', 'abc123', {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 86400000, // 24 hours in ms
path: '/', // Available on all routes
domain: '.example.com', // Include subdomains
});
// Reading cookies
import cookieParser from 'cookie-parser';
app.use(cookieParser());
app.get('/profile', (req, res) => {
const sessionId = req.cookies.sessionId;
});
// Clearing cookies
res.clearCookie('sessionId');
Cookie Security Options#
javascript
res.cookie('session', value, {
// REQUIRED for security
httpOnly: true, // Prevents XSS access
secure: true, // Only send over HTTPS
sameSite: 'strict', // Prevents CSRF
// Recommended
maxAge: 3600000, // Expire in 1 hour
path: '/', // Scope to entire site
// Optional
domain: '.example.com', // Share across subdomains
signed: true, // Verify integrity (requires cookieParser(secret))
});
SameSite Explained#
javascript
// Strict - Never sent cross-site
sameSite: 'strict' // Best for session cookies
// Lax - Sent on top-level navigation (links)
sameSite: 'lax' // Good for most cases
// None - Always sent (requires secure: true)
sameSite: 'none' // For cross-site APIs (embed scenarios)
Express Sessions#
Basic Setup#
javascript
import express from 'express';
import session from 'express-session';
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET,
name: 'sid', // Custom cookie name (not default 'connect.sid')
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}));
// Use session
app.post('/login', async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
req.session.userId = user.id;
req.session.role = user.role;
res.json({ success: true });
});
app.get('/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
res.json({ userId: req.session.userId });
});
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
res.clearCookie('sid');
res.json({ success: true });
});
});
With Redis (Production)#
bash
npm install connect-redis ioredis
javascript
import session from 'express-session';
import RedisStore from 'connect-redis';
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
app.use(session({
store: new RedisStore({
client: redis,
prefix: 'sess:', // Key prefix in Redis
}),
secret: process.env.SESSION_SECRET,
name: 'sid',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000,
},
}));
With PostgreSQL#
bash
npm install connect-pg-simple
javascript
import session from 'express-session';
import pgSession from 'connect-pg-simple';
import pg from 'pg';
const pgPool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
});
const PgStore = pgSession(session);
app.use(session({
store: new PgStore({
pool: pgPool,
tableName: 'sessions',
createTableIfMissing: true,
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000,
},
}));
Session Security#
Regenerate on Login#
Prevent session fixation attacks:
javascript
app.post('/login', async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
// Regenerate session ID after login
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.role = user.role;
req.session.loginTime = Date.now();
res.json({ success: true });
});
});
Session Timeout#
javascript
// Check session age on each request
function sessionTimeout(maxAge = 30 * 60 * 1000) { // 30 minutes
return (req, res, next) => {
if (req.session.lastAccess) {
const elapsed = Date.now() - req.session.lastAccess;
if (elapsed > maxAge) {
return req.session.destroy(() => {
res.status(401).json({ error: 'Session expired' });
});
}
}
req.session.lastAccess = Date.now();
next();
};
}
app.use(sessionTimeout(30 * 60 * 1000)); // 30 min inactivity timeout
Concurrent Session Control#
javascript
// Only allow one session per user
app.post('/login', async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
// Invalidate existing sessions
await redis.del(`user_session:${user.id}`);
req.session.regenerate(async (err) => {
req.session.userId = user.id;
// Track this session
await redis.set(`user_session:${user.id}`, req.sessionID);
res.json({ success: true });
});
});
// Middleware to check session validity
async function validateSession(req, res, next) {
if (!req.session.userId) return next();
const validSessionId = await redis.get(`user_session:${req.session.userId}`);
if (validSessionId !== req.sessionID) {
return req.session.destroy(() => {
res.status(401).json({ error: 'Session invalidated' });
});
}
next();
}
app.use(validateSession);
Remember Me#
javascript
import crypto from 'crypto';
// Generate remember token
function generateRememberToken() {
return crypto.randomBytes(32).toString('hex');
}
app.post('/login', async (req, res) => {
const { email, password, rememberMe } = req.body;
const user = await authenticate(email, password);
req.session.regenerate(async (err) => {
req.session.userId = user.id;
if (rememberMe) {
const token = generateRememberToken();
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
await prisma.rememberToken.create({
data: {
userId: user.id,
token: hashedToken,
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
},
});
res.cookie('remember', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
});
}
res.json({ success: true });
});
});
// Auto-login with remember token
async function autoLogin(req, res, next) {
if (req.session.userId) return next();
const token = req.cookies.remember;
if (!token) return next();
const hashedToken = crypto.createHash('sha256').update(token).digest('hex');
const stored = await prisma.rememberToken.findFirst({
where: {
token: hashedToken,
expiresAt: { gt: new Date() },
},
include: { user: true },
});
if (stored) {
req.session.regenerate((err) => {
req.session.userId = stored.user.id;
next();
});
} else {
res.clearCookie('remember');
next();
}
}
app.use(autoLogin);
CSRF Protection#
javascript
import csrf from 'csurf';
// Setup CSRF protection
const csrfProtection = csrf({
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
},
});
// Apply to state-changing routes
app.use('/api', csrfProtection);
// Send token to client
app.get('/api/csrf-token', (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Client sends token in header
// fetch('/api/update', {
// method: 'POST',
// headers: { 'X-CSRF-Token': csrfToken },
// })
Key Takeaways#
- Use httpOnly cookies - Prevent XSS access to session
- Set secure flag - HTTPS only in production
- Use sameSite - Prevent CSRF attacks
- Regenerate on login - Prevent session fixation
- Use Redis in production - Share sessions across servers
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.