Express.js & Node.js Course for Beginners - Full Tutorial
# Building a Secure Authentication System with Role-Based Access Control in Express.js
## Introduction
In this tutorial, we will guide you through the process of creating a robust authentication system for your Express.js application. This system includes user registration, login, logout, form validation, and role-based access control using Passport.js. By the end of this article, you will have a secure application where only authenticated users can access certain routes, and administrators have additional privileges.
## Prerequisites
Before starting, ensure you have the following:
- Node.js installed on your system.
- Basic knowledge of Express.js and JavaScript.
- Familiarity with database operations using Sequelize or another ORM tool.
## Setting Up Authentication Routes
### Step 1: Installing Necessary Packages
Begin by installing the required packages for authentication:
```bash
npm install passport passport-local express-validator lodash bcryptjs connect-flash
```
### Step 2: Configuring Passport.js
Create a new file `passportSetup.js` in your root directory and add the following code to set up Passport.js:
```javascript
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('./models/User');
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findOne({ where: { id } });
if (!user) return done(new Error('Invalid user ID'));
done(null, user);
} catch (error) {
done(error);
}
});
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
}, async (email, password, done) => {
try {
const user = await User.findOne({ where: { email } });
if (!user) return done(null, false, { message: 'Incorrect email.' });
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) return done(null, false, { message: 'Incorrect password.' });
done(null, user);
} catch (error) {
done(error);
}
}));
```
### Step 3: Defining Routes
In your `routes/index.js`, define the necessary routes:
```javascript
const express = require('express');
const router = express.Router();
const { signup, login, logout } = require('./controllers/auth');
router.get('/signup', signup.show);
router.post('/signup', validateSignupInput, signup.create);
router.get('/login', login.show);
router.post('/login', validateLoginInput, login.authenticate);
router.post('/logout', login.logout);
module.exports = router;
```
## Implementing Signup and Login Handlers
### Step 4: Creating Controllers
In `controllers/auth.js`, implement the signup and login logic:
```javascript
const passport = require('passport');
const User = require('../models/User');
exports.signup = {
show(req, res) {
res.render('signup', { title: 'Sign Up' });
},
create(req, res, next) => {
const { email, password } = req.body;
User.create({
email,
password: await bcrypt.hash(password, 8),
isAdmin: process.env.ADMIN === 'true'
})
.then(user => res.redirect('/'))
.catch(error => next(error));
}
};
exports.login = {
show(req, res) {
res.render('login', { title: 'Login' });
},
authenticate(req, res, next) => {
passport.authenticate('local', (error, user) => {
if (error || !user) {
req.flash('error', error.message);
res.redirect('/login');
} else {
req.login(user, () => {
res.redirect('/');
});
}
})(req, res, next);
},
logout(req, res) {
req.logout();
res.redirect('/');
}
};
```
## Validating Input
### Step 5: Implementing Validation
Create a validation middleware in `middleware/validate.js`:
```javascript
const validator = require('validator');
const _ = require('lodash');
exports.validateSignupInput = (req, res, next) => {
const errors = {};
if (!validator.isEmail(req.body.email)) {
errors.email = 'Please use a valid email address.';
}
if (!validator.isAlphanumeric(req.body.password)) {
errors.password = 'Invalid characters in password.';
} else if (req.body.password.length < 8) {
errors.password = 'Password must be at least 8 characters.';
}
return new Promise((resolve, reject) => {
User.findOne({ where: { email: req.body.email } })
.then(user => {
if (user && user.id) {
errors.email = 'Email is already in use.';
}
if (_.isEmpty(errors)) {
resolve();
} else {
req.flash('errors', errors);
res.locals.formData = req.body;
next();
}
})
.catch(error => reject(error));
});
};
```
## Securing Routes with Middleware
### Step 6: Creating Access Control Middleware
Add a middleware file `middleware/auth.js`:
```javascript
const _ = require('lodash');
exports.isAuth = (req, res, next) => {
if (!req.user) {
req.flash('error', 'Please login first.');
return res.redirect('/');
}
next();
};
exports.isAdmin = (req, res, next) => {
if (!req.user || !req.user.isAdmin) {
req.flash('error', 'Unauthorized access.');
return res.redirect('/');
}
next();
};
```
### Step 7: Securing Leads Routes
Modify your routes to include authentication:
```javascript
const auth = require('../middleware/auth');
router.get('/leads', auth.isAuth, showLeads);
router.get('/leads/:id/edit', auth.isAuth, editLead);
// ... other protected routes
```
## Implementing Role-Based Access Control
### Step 8: Adding isAdmin Field to Users Model
Update your `models/User.js`:
```javascript
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
sequelize.define('User', {
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
},
isAdmin: {
type: DataTypes.BOOLEAN,
defaultValue: false
}
});
};
```
## Conclusion
By following this comprehensive guide, you have successfully implemented a secure authentication system with role-based access control in your Express.js application. This setup ensures that only authenticated users can access certain routes, and administrators have additional privileges to manage content.