Skip to content

ExpressJS - Authentication

Authentication is the process of verifying a user’s identity. In web applications, this typically involves a username/email and a password. This tutorial demonstrates a modern, secure approach to building an authentication system using sessions.

Security Warning: This example uses an in-memory array to store users for simplicity. In a real-world application, you must use a persistent database (like PostgreSQL or MongoDB) to store user data.

Start with a new Express project (see the Environment Setup chapter) and install the necessary packages for handling sessions and hashing passwords:

# For session management
npm install express-session
# For securely hashing passwords
npm install bcrypt
# A template engine for rendering views
npm install pug

A good structure is key. Create the following files and directories:

.
├── src/
│ ├── views/ # Pug templates
│ │ ├── login.pug
│ │ ├── signup.pug
│ │ └── protected.pug
│ └── index.js # Main application file
├── .env # Environment variables
├── package.json
└── ...

Create a .env file in your root directory to store your session secret. This should never be committed to git.

.env
SESSION_SECRET='a-very-strong-and-long-random-secret-key'

Step 3: Main Application Logic (src/index.js)

Section titled “Step 3: Main Application Logic (src/index.js)”

This file will contain all our logic. In a larger app, you would split this into routes, controllers, and services.

import express from 'express';
import session from 'express-session';
import bcrypt from 'bcrypt';
import 'dotenv/config'; // Loads .env file
const app = express();
const PORT = 3000;
// In-memory user store (FOR DEMO PURPOSES ONLY!)
const users = [];
// --- Middleware Setup ---
app.set('view engine', 'pug');
app.set('views', './src/views');
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: false } // In production, set to true with HTTPS
}));
// --- Custom Middleware for Protecting Routes ---
const checkAuth = (req, res, next) => {
if (req.session.user) {
next(); // User is authenticated, proceed
} else {
res.redirect('/login'); // Not authenticated, redirect to login
}
};
// --- Routes ---
app.get('/', (req, res) => {
res.redirect('/login');
});
app.get('/signup', (req, res) => {
res.render('signup');
});
app.post('/signup', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).render('signup', { message: 'Username and password are required.' });
}
if (users.find(user => user.username === username)) {
return res.status(400).render('signup', { message: 'Username already exists.' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10); // Hash the password
const newUser = { username, password: hashedPassword };
users.push(newUser);
console.log('New user registered:', newUser);
res.redirect('/login');
} catch (error) {
console.error(error);
res.status(500).send('Server error');
}
});
app.get('/login', (req, res) => {
res.render('login');
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user) {
return res.status(401).render('login', { message: 'Invalid credentials.' });
}
try {
const isMatch = await bcrypt.compare(password, user.password);
if (isMatch) {
req.session.user = { username: user.username }; // Store user in session
res.redirect('/protected');
} else {
res.status(401).render('login', { message: 'Invalid credentials.' });
}
} catch (error) {
console.error(error);
res.status(500).send('Server error');
}
});
app.get('/protected', checkAuth, (req, res) => {
res.render('protected', { username: req.session.user.username });
});
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).send('Could not log out.');
}
res.redirect('/login');
});
});
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));

Step 4: Creating the Views (Pug Templates)

Section titled “Step 4: Creating the Views (Pug Templates)”

Create the following three .pug files in your src/views directory.

html
head
title Signup
body
h1 Create an Account
if message
p(style={color: 'red'}) #{message}
form(action="/signup" method="POST")
div
label(for="username") Username:
input(name="username" type="text" required)
div
label(for="password") Password:
input(name="password" type="password" required)
button(type="submit") Sign Up
p Already have an account? <a href="/login">Login</a>
html
head
title Login
body
h1 Login
if message
p(style={color: 'red'}) #{message}
form(action="/login" method="POST")
div
label(for="username") Username:
input(name="username" type="text" required)
div
label(for="password") Password:
input(name="password" type="password" required)
button(type="submit") Log In
p Don't have an account? <a href="/signup">Sign up</a>
html
head
title Protected Area
body
h1 Welcome, #{username}!
p This is a protected page. You can only see it if you are logged in.
form(action="/logout" method="POST")
button(type="submit") Log Out

Run npm run dev and navigate to http://localhost:3000. You’ll be redirected to the login page. Try to access /protected directly; you will be sent back to login. Now, sign up for a new account, then log in with those credentials to access the protected page.

While sessions are great, another popular method for authentication is using JWTs. This is a stateless approach, meaning the server doesn’t need to store session data. The flow is:

  1. User logs in.
  2. Server validates credentials and returns a signed JWT to the client.
  3. Client stores the JWT (e.g., in an HttpOnly cookie) and sends it in the Authorization header of subsequent requests.
  4. Server verifies the JWT on each request to identify the user. This method is especially useful for Single Page Applications (SPAs) and mobile apps.