Sessions and Authentication β Express Session
After completing this topic
You will understand why sessions are necessary, be able to implement login/logout using express-session, and know the concept of session stores and security settings.
Why are sessions necessary?
In the Cookies topic, we learned that "HTTP is stateless." While cookies can be used to maintain state, they have critical limitations.
// Bad β Storing user information directly in a cookie
res.cookie('user', JSON.stringify({ id: 1, role: 'admin' }));The client (browser) can freely modify cookies. What if a user changes role: 'admin' to role: 'superadmin' and sends it? The server cannot distinguish this.
Sessions solve this problem. Sensitive data is stored on the server, and only an identifier key (session ID) is sent to the client.
Cookie method: Stores "name=Hoon, role=admin" on the client (vulnerable to tampering)
Session method: Stores only "sessionID=abc123" on the client β Server retrieves user information using abc123How sessions work
1. Login request
Client β Server: POST /login {username: "Hoon", password: "****"}
2. Server creates a session
Server: Stores {userId: 1, role: "admin"} in the session store
Server: Generates session ID = "abc123"
Server β Client: Set-Cookie: connect.sid=abc123
3. Subsequent requests
Client β Server: GET /dashboard (Cookie: connect.sid=abc123)
Server: Retrieves session using abc123 β Verifies {userId: 1, role: "admin"}
Server β Client: Dashboard dataThe client only knows the session ID, and the actual data (userId, role) is stored in the server's memory or database. Even if the client forges the session ID, authentication will fail if the corresponding ID does not exist in the server's session store.
Installing and configuring express-session
npm install express-sessionconst express = require('express');
const session = require('express-session');
const app = express();
app.use(express.json());
app.use(session({
secret: 'my-secret-key-change-this',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: false, // true in production
maxAge: 3600000 // 1 hour
}
}));| Option | Description |
|---|---|
secret | A key to encrypt the session ID. Should be long and random |
resave | Whether to save the session on each request, even if there are no changes. false is recommended |
saveUninitialized | Whether to save empty sessions. Setting to false prevents cookies from being issued before login |
cookie | Session cookie options (httpOnly, secure, maxAge, etc.) |
secret should never be hardcoded into the code. It should be managed as an environment variable:
secret: process.env.SESSION_SECRET || 'fallback-dev-only'Implementing login/logout
const users = [
{ id: 1, username: 'alice', password: 'pass123', role: 'admin' },
{ id: 2, username: 'bob', password: 'pass456', role: 'user' }
];
// Login
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Store user information in the session
req.session.userId = user.id;
req.session.role = user.role;
res.json({ message: `Welcome, ${user.username}` });
});
// Logout
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});req.session is an object. You can freely store any properties in it. req.session.destroy() deletes the session data on the server.
Authentication middleware
When accessing protected routes, you need to check "Is the user logged in?" every time. Creating this as middleware allows you to reuse it in all routes.
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Login required' });
}
next();
}
function requireAdmin(req, res, next) {
if (req.session.role !== 'admin') {
return res.status(403).json({ error: 'Admin access required' });
}
next();
}
// Usage
app.get('/profile', requireAuth, (req, res) => {
res.json({ userId: req.session.userId, role: req.session.role });
});
app.get('/admin/dashboard', requireAuth, requireAdmin, (req, res) => {
res.json({ message: 'Admin dashboard' });
});You can chain middleware to create complex conditions like "login + admin".
Session store β Memory vs. external storage
By default, express-session stores sessions in server memory. This is sufficient for development, but it presents problems in production.
| Problem | Description |
|---|---|
| Server restart | Memory is initialized, so all users are logged out |
| Memory leak | As sessions accumulate, the server's memory becomes insufficient |
| Multiple servers | Server A's memory-stored session is not known to Server B |
In production, use external storage such as Redis, MongoDB, or PostgreSQL:
npm install connect-redis redisconst RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient();
redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));Storing sessions in Redis ensures that sessions are maintained even when the server restarts, and if multiple servers share the same Redis, they can share sessions.
Session vs. JWT β When to use what
| Session | JWT | |
|---|---|---|
| State storage | Server (stateful) | Client (stateless) |
| Scaling | Requires a shared store like Redis | No server-side sharing needed |
| Logout | Invalidated immediately with destroy() | Valid until token expiration (requires a blacklist) |
| Security | Server manages data | Difficult to handle token theft |
| Suitable cases | Traditional web apps, admin pages | API servers, mobile apps, microservices |
Traditional web app (SSR) β Sessions are more natural
SPA + API server β JWT is more convenient
Mobile app β JWT (Cookie management is complex)Key takeaways
| Concept | Summary |
|---|---|
| Session | Stores user data on the server and sends only an ID to the client |
| Session ID | An identifier key sent in a cookie (connect.sid) |
req.session | An object for reading and writing session data |
destroy() | Logout β Deletes session data |
| Session store | Use external storage like Redis in production |
The key to session-based authentication: Store sensitive data on the server, and send only the key (session ID) to the client. Understanding this principle will naturally lead to an understanding of the differences with token-based authentication (JWT).