Skip to main content

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:

  1. User registration - Creating new user accounts
  2. User login - Verifying credentials against stored data
  3. Session management - Maintaining user state across requests
  4. Logout functionality - Removing authentication state

Authentication Flow in Express

Here's how a typical authentication flow works in an Express application:

  1. User submits credentials (username/email and password)
  2. Server validates credentials against stored user data
  3. If valid, server creates a session or token
  4. Server sends the session ID or token to the client
  5. Client stores and sends this identifier with subsequent requests
  6. Server verifies the identifier to maintain authenticated state

Setting Up Dependencies

To implement authentication in Express, we'll need several packages:

bash
npm install express express-session bcryptjs mongoose cookie-parser
  • express-session: For managing user sessions
  • bcryptjs: For password hashing
  • mongoose: 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:

javascript
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:

javascript
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:

javascript
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:

javascript
// 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:

javascript
// 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:

javascript
// 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:

javascript
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:

  1. Never store plain-text passwords - Always hash passwords using bcrypt or Argon2
  2. Use HTTPS in production - Protect data in transit
  3. Implement rate limiting - Prevent brute-force attacks
  4. Set secure and HttpOnly cookies - Prevent XSS attacks
  5. Use environment variables for secrets - Don't hardcode secrets
  6. Implement CSRF protection - Prevent cross-site request forgery
  7. Validate and sanitize all inputs - Prevent injection attacks
  8. 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

Exercises

  1. Extend the user model with additional fields like first name, last name, and profile picture.
  2. Implement password reset functionality using email.
  3. Add email verification during registration.
  4. Implement OAuth authentication with a provider like Google or GitHub.
  5. 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! :)