Express Authentication Basics
Authentication is a crucial aspect of web development that ensures users are who they claim to be. In this guide, we'll explore how to implement authentication in Express.js applications, covering core concepts and providing practical examples for beginners.
What is Authentication?
Authentication is the process of verifying the identity of users attempting to access your application. It answers the question: "Are you who you say you are?" This differs from authorization, which determines what authenticated users are allowed to do.
In web applications, authentication typically involves:
- User registration - Creating new user accounts
- User login - Verifying credentials against stored data
- Session management - Maintaining user state across requests
- Logout functionality - Removing authentication state
Authentication Flow in Express
Here's how a typical authentication flow works in an Express application:
- User submits credentials (username/email and password)
- Server validates credentials against stored user data
- If valid, server creates a session or token
- Server sends the session ID or token to the client
- Client stores and sends this identifier with subsequent requests
- Server verifies the identifier to maintain authenticated state
Setting Up Dependencies
To implement authentication in Express, we'll need several packages:
npm install express express-session bcryptjs mongoose cookie-parser
express-session
: For managing user sessionsbcryptjs
: For password hashingmongoose
: For MongoDB interactions (our database)cookie-parser
: For parsing cookies
Basic Express Setup with Session Support
Let's start with a basic Express setup that includes session support:
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const mongoose = require('mongoose');
// Create Express app
const app = express();
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/auth_demo', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('MongoDB connection error:', err));
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
// Session configuration
app.use(session({
secret: 'your-secret-key', // Use a strong secret in production!
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production', // Use secure cookies in production
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Server start
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Creating a User Model
We'll need a user model to store authentication data:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
// Hash password before saving
userSchema.pre('save', async function(next) {
// Only hash password if it's modified (or new)
if (!this.isModified('password')) return next();
try {
// Generate salt
const salt = await bcrypt.genSalt(10);
// Hash the password along with the new salt
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// Method to validate password
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
const User = mongoose.model('User', userSchema);
module.exports = User;
Implementing User Registration
Now let's implement a user registration route:
const User = require('./models/User'); // Adjust path as needed
// Registration route
app.post('/register', async (req, res) => {
try {
// Extract user data from request
const { username, email, password } = req.body;
// Check if user already exists
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(400).json({
message: 'User already exists with that email or username'
});
}
// Create new user
const newUser = new User({
username,
email,
password // Will be hashed by our pre-save hook
});
// Save user to database
await newUser.save();
// Return success (don't send password)
res.status(201).json({
message: 'User registered successfully',
user: {
id: newUser._id,
username: newUser.username,
email: newUser.email
}
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ message: 'Server error during registration' });
}
});
Implementing User Login
Next, let's implement user login with session management:
// Login route
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user by email
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Check password
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Create session
req.session.userId = user._id;
req.session.username = user.username;
res.json({
message: 'Login successful',
user: {
id: user._id,
username: user.username,
email: user.email
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: 'Server error during login' });
}
});
Authentication Middleware
To protect routes that require authentication, we'll create middleware:
// Authentication middleware
const isAuthenticated = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ message: 'Authentication required' });
}
next();
};
// Example of a protected route
app.get('/profile', isAuthenticated, async (req, res) => {
try {
// Get user data from database (excluding password)
const user = await User.findById(req.session.userId).select('-password');
res.json({ user });
} catch (error) {
console.error('Profile fetch error:', error);
res.status(500).json({ message: 'Server error fetching profile' });
}
});
Implementing Logout
Let's add logout functionality to destroy the session:
// Logout route
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ message: 'Logout failed' });
}
res.clearCookie('connect.sid'); // Clear session cookie
res.json({ message: 'Logged out successfully' });
});
});
Session vs. Token-Based Authentication
Express supports two main authentication approaches:
Session-Based Authentication (what we've implemented)
- Server stores session data
- Client gets a session ID in a cookie
- Stateful (server must store session information)
- Better suited for traditional web applications
Token-Based Authentication (JWT)
- Server generates signed tokens with claims
- Client stores and sends token with each request
- Stateless (server doesn't store session info)
- Better suited for APIs, mobile apps, SPAs
Practical Example: Complete Authentication System
Let's see how all these components work together in a complete, minimal authentication system:
const express = require('express');
const session = require('express-session');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const cookieParser = require('cookie-parser');
const app = express();
// MongoDB connection
mongoose.connect('mongodb://localhost:27017/auth_demo')
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('MongoDB connection error:', err));
// User model
const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 10);
next();
});
const User = mongoose.model('User', userSchema);
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 24 * 60 * 60 * 1000 }
}));
// Authentication middleware
const isAuthenticated = (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ message: 'Authentication required' });
}
next();
};
// Routes
app.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
const user = new User({ username, email, password });
await user.save();
res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
res.status(400).json({ message: 'Registration failed', error: error.message });
}
});
app.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) return res.status(400).json({ message: 'Invalid credentials' });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(400).json({ message: 'Invalid credentials' });
req.session.userId = user._id;
res.json({ message: 'Login successful' });
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
app.get('/profile', isAuthenticated, async (req, res) => {
try {
const user = await User.findById(req.session.userId).select('-password');
res.json({ user });
} catch (error) {
res.status(500).json({ message: 'Error fetching profile' });
}
});
app.post('/logout', (req, res) => {
req.session.destroy(() => {
res.clearCookie('connect.sid');
res.json({ message: 'Logged out successfully' });
});
});
// Start server
app.listen(3000, () => console.log('Server running on port 3000'));
Security Best Practices
When implementing authentication, follow these security best practices:
- Never store plain-text passwords - Always hash passwords using bcrypt or Argon2
- Use HTTPS in production - Protect data in transit
- Implement rate limiting - Prevent brute-force attacks
- Set secure and HttpOnly cookies - Prevent XSS attacks
- Use environment variables for secrets - Don't hardcode secrets
- Implement CSRF protection - Prevent cross-site request forgery
- Validate and sanitize all inputs - Prevent injection attacks
- Implement account lockouts - After multiple failed login attempts
Summary
In this guide, we've covered the basics of authentication in Express applications:
- Setting up Express with session support
- Creating a user model with secure password handling
- Implementing user registration and login functionality
- Protecting routes with authentication middleware
- Implementing logout functionality
- Understanding session vs. token-based authentication
- Security best practices
Authentication is a critical foundation for most web applications. This guide provides the basic building blocks, but remember that production systems often require additional features like password reset, email verification, and more robust security measures.
Additional Resources
- Express.js Official Documentation
- express-session Documentation
- bcrypt.js Documentation
- OWASP Authentication Best Practices
- JWT Authentication
Exercises
- Extend the user model with additional fields like first name, last name, and profile picture.
- Implement password reset functionality using email.
- Add email verification during registration.
- Implement OAuth authentication with a provider like Google or GitHub.
- Create a role-based authorization system with admin and regular user roles.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)