Authentication and authorization are critical components of any web application. Authentication verifies the identity of a user, while authorization determines what an authenticated user is allowed to do. This chapter will cover these concepts in detail, using Node.js and Express.js to create a secure and robust authentication and authorization system. We'll go from the basics to advanced topics, including practical examples, code snippets, and detailed explanations.
Authentication is the process of verifying who a user is. Common methods include username and password, biometric verification, and multi-factor authentication (MFA).
Authorization determines what resources a user can access and what actions they can perform. It typically follows after authentication.
mkdir auth-example
cd auth-example
npm init -y
npm install express mongoose body-parser jsonwebtoken bcryptjs
auth-example/
├── models/
│ └── user.js
├── routes/
│ ├── auth.js
│ └── users.js
├── middlewares/
│ └── auth.js
├── app.js
├── index.js
├── package.json
JSON Web Tokens (JWT) are a compact, URL-safe means of representing claims between two parties. JWTs can be signed using a secret or a public/private key pair.
// models/user.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
});
userSchema.pre('save', async function (next) {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 10);
}
next();
});
module.exports = mongoose.model('User', userSchema);
// routes/auth.js
const express = require('express');
const router = express.Router();
const User = require('../models/user');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
// Registration
router.post('/register', async (req, res) => {
const { username, password } = req.body;
try {
const existingUser = await User.findOne({ username });
if (existingUser) {
return res.status(400).json({ message: 'Username already exists' });
}
const user = new User({ username, password });
await user.save();
const token = jwt.sign({ userId: user._id, role: user.role }, 'SECRET_KEY', { expiresIn: '1h' });
res.status(201).json({ token });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Login
router.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(400).json({ message: 'Invalid username or password' });
}
const token = jwt.sign({ userId: user._id, role: user.role }, 'SECRET_KEY', { expiresIn: '1h' });
res.status(200).json({ token });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
module.exports = router;
// middlewares/auth.js
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) return res.status(401).json({ message: 'Access Denied' });
jwt.verify(token, 'SECRET_KEY', (err, user) => {
if (err) return res.status(403).json({ message: 'Invalid Token' });
req.user = user;
next();
});
}
function authorizeRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ message: 'Access Denied' });
}
next();
};
}
module.exports = { authenticateToken, authorizeRole };
Role-Based Access Control (RBAC) restricts access to resources based on the user’s role.
// routes/users.js
const express = require('express');
const router = express.Router();
const { authenticateToken, authorizeRole } = require('../middlewares/auth');
const User = require('../models/user');
// Get all users (admin only)
router.get('/', authenticateToken, authorizeRole('admin'), async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
module.exports = router;
JWTs can expire, requiring the client to obtain a new token without re-authenticating.
Add a refresh token to the user schema:
// models/user.js
const userSchema = new mongoose.Schema({
// Other fields...
refreshToken: String
});
2.Create a refresh token route:
// routes/auth.js
router.post('/token', async (req, res) => {
const { token } = req.body;
if (!token) return res.status(401).json({ message: 'Access Denied' });
try {
const user = await User.findOne({ refreshToken: token });
if (!user) return res.status(403).json({ message: 'Invalid Token' });
jwt.verify(token, 'REFRESH_SECRET_KEY', (err, user) => {
if (err) return res.status(403).json({ message: 'Invalid Token' });
const accessToken = jwt.sign({ userId: user._id, role: user.role }, 'SECRET_KEY', { expiresIn: '1h' });
res.json({ accessToken });
});
} catch (err) {
res.status(500).json({ message: err.message });
}
});
3.Generate refresh token on login and registration:
// routes/auth.js
const refreshToken = jwt.sign({ userId: user._id, role: user.role }, 'REFRESH_SECRET_KEY');
user.refreshToken = refreshToken;
await user.save();
res.status(200).json({ accessToken: token, refreshToken });
Using bcrypt
to hash passwords before storing them in the database.
// models/user.js
userSchema.pre('save', async function (next) {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 10);
}
next();
});
1. Two-Factor Authentication (2FA): 2FA adds an additional layer of security. Popular methods include SMS, email, or authentication apps.
2. OAuth2: OAuth2 is a protocol for authorization. It allows third-party applications to access user resources without sharing credentials.
3. Password Reset: Implement a secure password reset mechanism using email tokens.
4. Rate Limiting: Prevent brute-force attacks by limiting the number of login attempts.
5. Secure Cookie Storage: Store tokens in secure, HTTP-only cookies to prevent XSS attacks.
Below is the complete code for a practical example of implementing authentication and authorization in a Node.js application.
auth-example/
├── models/
│ └── user.js
├── routes/
│ ├── auth.js
│ └── users.js
├── middlewares/
│ └── auth.js
├── app.js
├── index.js
├── package.json
index.js
const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const authRouter = require('./routes/auth');
const usersRouter = require('./routes/users');
const app = express();
const port = 3000;
mongoose.connect('mongodb://localhost/auth-example', { useNewUrlParser: true, useUnifiedTopology: true });
app.use(bodyParser.json());
app.use('/auth', authRouter);
app.use('/users', usersRouter);
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
models/user.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
refreshToken: String
});
userSchema.pre('save', async function (next) {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 10);
}
next();
});
module.exports = mongoose.model('User', userSchema);
routes/auth.js
const express = require('express');
const router = express.Router();
const User = require('../models/user');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
// Registration
router.post('/register', async (req, res) => {
const { username, password } = req.body;
try {
const existingUser = await User.findOne({ username });
if (existingUser) {
return res.status(400).json({ message: 'Username already exists' });
}
const user = new User({ username, password });
await user.save();
const accessToken = jwt.sign({ userId: user._id, role: user.role }, 'SECRET_KEY', { expiresIn: '1h' });
const refreshToken = jwt.sign({ userId: user._id, role: user.role }, 'REFRESH_SECRET_KEY');
user.refreshToken = refreshToken;
await user.save();
res.status(201).json({ accessToken, refreshToken });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Login
router.post('/login', async (req, res) => {
const { username, password } = req.body;
try {
const user = await User.findOne({ username });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(400).json({ message: 'Invalid username or password' });
}
const accessToken = jwt.sign({ userId: user._id, role: user.role }, 'SECRET_KEY', { expiresIn: '1h' });
const refreshToken = jwt.sign({ userId: user._id, role: user.role }, 'REFRESH_SECRET_KEY');
user.refreshToken = refreshToken;
await user.save();
res.status(200).json({ accessToken, refreshToken });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Refresh Token
router.post('/token', async (req, res) => {
const { token } = req.body;
if (!token) return res.status(401).json({ message: 'Access Denied' });
try {
const user = await User.findOne({ refreshToken: token });
if (!user) return res.status(403).json({ message: 'Invalid Token' });
jwt.verify(token, 'REFRESH_SECRET_KEY', (err, user) => {
if (err) return res.status(403).json({ message: 'Invalid Token' });
const accessToken = jwt.sign({ userId: user._id, role: user.role }, 'SECRET_KEY', { expiresIn: '1h' });
res.json({ accessToken });
});
} catch (err) {
res.status(500).json({ message: err.message });
}
});
module.exports = router;
routes/users.js
const express = require('express');
const router = express.Router();
const { authenticateToken, authorizeRole } = require('../middlewares/auth');
const User = require('../models/user');
// Get all users (admin only)
router.get('/', authenticateToken, authorizeRole('admin'), async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
module.exports = router;
middlewares/auth.js
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token == null) return res.status(401).json({ message: 'Access Denied' });
jwt.verify(token, 'SECRET_KEY', (err, user) => {
if (err) return res.status(403).json({ message: 'Invalid Token' });
req.user = user;
next();
});
}
function authorizeRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ message: 'Access Denied' });
}
next();
};
}
module.exports = { authenticateToken, authorizeRole };
In this chapter, we've covered the essential concepts of authentication and authorization in Node.js, from basic JWT-based authentication to advanced topics like role-based access control and refresh tokens. By following the principles and best practices outlined here, you can create secure and robust authentication systems for your Node.js applications. This comprehensive guide should provide all the information you need to implement authentication and authorization in your projects.Happy coding !❤️