Build a Membership System with Express.js Session

Introduction

Based on previous tutorials, we can create basic CRUD programs, but due to HTTP being stateless, users need to re-validate on each login action, which is a poor experience.

One solution is for the server to create a session (a piece of data stored on the server) upon request, and the user will receive a session ID in the response, typically stored in a cookie. The server uses this ID to identify and locate the specific user’s session data, allowing it to remember the corresponding data for individual users.

Session Storage

Specifically, express-session🔗 is a commonly used package that helps us implement server sessions. It is recommended to read the official documentation for further setup, such as:

  • name: Customizable cookie name, avoid using the default connect.sid
  • cookie.httpOnly: Defaults to true, but it’s recommended to specify it explicitly
  • cookie.sameSite: Recommended to set to strict for increased security

Minimal Example

Based on Creating a Simple CRUD Todo List with Express.js🔗, create a simple backend server and install express-session to help us build sessions that remember users. By default, it will be stored in memory, but it can actually be linked to a file system, database, or distributed storage; there is no rule for session storage.

Terminal window
npm install express express-session
index.js
import express from 'express';
import session from 'express-session';
const app = express();
app.use(
session({
secret: 'secret-key',
resave: false,
saveUninitialized: true,
cookie: { secure: false },
}),
);
app.get('/', (req, res) => {
if (!req.session.views) {
req.session.views = 1;
} else {
req.session.views++;
}
res.send(`You visited ${req.session.views} time(s)`);
});
app.listen(3000, () => console.log('Server is running on http://localhost:3000'));

The above is a minimal example. When a user visits the homepage, it will count the number of visits and store the count in the session. If the user visits again, the count will increase.

Login System

  1. Registration page /register: Users enter their username and password, and the server verifies if the account already exists. If not, registration is successful.
  2. Login page /login: Users enter their username and password.
  3. Login processing /login: The server verifies the credentials, and if correct, sets the isLoggedIn status and username in the session.
  4. Profile page /profile: Determines whether to display user data or redirect to the login page based on login status.
  5. Logout /logout: Destroys the session and clears the cookie.
index.js
import express from 'express';
import session from 'express-session';
import bodyParser from 'body-parser';
const app = express();
const PORT = 3000;
// Use body-parser to parse POST request data
app.use(bodyParser.urlencoded({ extended: true }));
app.use(
session({
secret: 'my-secret-key',
resave: false,
saveUninitialized: true,
cookie: { secure: false },
}),
);
const users = [];
app.get('/register', (req, res) => {
res.send(`
<h1>Register</h1>
<form method="POST" action="/register">
<label>username: <input type="text" name="username" required /></label><br/>
<label>password: <input type="password" name="password" required /></label><br/>
<button type="submit">Submit</button>
</form>
`);
});
app.post('/register', (req, res) => {
const { username, password } = req.body;
const existingUser = users.find((user) => user.username === username);
if (existingUser) {
return res.send(
'<h1>Registration Failed: Username is already exists</h1><a href="/register">Go Registration page</a>',
);
}
users.push({ username, password });
res.send('<h1>Registration Success!</h1><a href="/login">Go Login Page</a>');
});
app.get('/login', (req, res) => {
res.send(`
<h1>Login</h1>
<form method="POST" action="/login">
<label>username: <input type="text" name="username" required /></label><br/>
<label>password: <input type="password" name="password" required /></label><br/>
<button type="submit">Login</button>
</form>
`);
});
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find((u) => u.username === username && u.password === password);
if (user) {
req.session.isLoggedIn = true;
req.session.username = username;
res.send(`<h1>Login Success! welcome, ${username}!</h1><a href="/profile">Go Profile Page</a>`);
} else {
res.send('<h1>Login Failed!</h1><a href="/login">Go Login Page</a>');
}
});
app.get('/profile', (req, res) => {
if (req.session.isLoggedIn) {
res.send(`<h1>Profile</h1><p>Welcome, ${req.session.username}!</p><a href="/logout">Logout</a>`);
} else {
res.redirect('/login');
}
});
app.get('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.send('Logout Failed');
}
res.clearCookie('connect.sid');
res.send('<h1>Logout Success!</h1><a href="/login">Go Login Page</a>');
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

I put the project on GitHub - express-in-memory-session🔗, you can download it and play with it.

Security Best Practices

  • Use HTTPS🔗: Encrypt the session ID during transmission to prevent man-in-the-middle attacks.
  • Set HttpOnly🔗 and Secure Cookie🔗: Prevent JavaScript from reading the session ID and ensure it is transmitted only over HTTPS.
  • Session expiration time: Set a reasonable session expiration time, such as expiring after 15 minutes of inactivity.
  • Regenerate Session ID: Regenerate the session ID upon login and logout to prevent Session Fixation🔗 attacks.

Password Encryption

Passwords should be stored in an encrypted form (e.g., using bcrypt🔗), converting the password into irreversible data through a hashing algorithm. During future verification, the user-input password will also be hashed before comparison. This way, even if the password database is leaked, user passwords will not be directly exposed.

Store
const hashedPassword = await bcrypt.hash(password, 10);
users.push({ username, password: hashedPassword });
Verify
const targetUser = users.find((user) => user.username === username);
const isValid = await bcrypt.compare(password, targetUser.password);

To protect the uniqueness of the hash value, bcrypt generates a random “salt” by default and appends it to the password. This means that even if two users have the same password, the hash values will be different, increasing the difficulty of cracking.

Further Reading