Adding Authentication and Authorization to Your Node.js API
This tutorial builds on our MongoDB-powered API by implementing JWT (JSON Web Token) authentication and role-based authorization. We'll create a complete authentication system with user registration, login, and protected routes.
Prerequisites
- Completed Parts 1 and 2 of the tutorial
- Understanding of JWT and basic security concepts
Project Setup
First, install the required dependencies:
npm install jsonwebtoken bcryptjs validator
Updated Project Structure
Add these new files to your project:
books-api/
├── src/
│ ├── config/
│ │ ├── database.js
│ │ └── auth.js
│ ├── models/
│ │ ├── Book.js
│ │ └── User.js
│ ├── routes/
│ │ ├── books.js
│ │ └── auth.js
│ ├── middleware/
│ │ ├── errorHandler.js
│ │ ├── auth.js
│ │ └── roleCheck.js
│ └── server.js
├── package.json
└── .env
Environment Setup
Update your .env
file:
MONGODB_URI=mongodb://localhost:27017/books_api
NODE_ENV=development
PORT=3000
JWT_SECRET=your_jwt_secret_key_here
JWT_EXPIRE=24h
Auth Configuration
Create src/config/auth.js
:
module.exports = {
roles: {
ADMIN: 'admin',
EDITOR: 'editor',
USER: 'user'
},
permissions: {
books: {
create: ['admin', 'editor'],
update: ['admin', 'editor'],
delete: ['admin']
}
}
};
User Model
Create src/models/User.js
:
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const validator = require('validator');
const jwt = require('jsonwebtoken');
const { roles } = require('../config/auth');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Please provide a name'],
trim: true,
maxlength: [50, 'Name cannot be more than 50 characters']
},
email: {
type: String,
required: [true, 'Please provide an email'],
unique: true,
lowercase: true,
validate: [validator.isEmail, 'Please provide a valid email']
},
password: {
type: String,
required: [true, 'Please provide a password'],
minlength: [8, 'Password must be at least 8 characters'],
select: false
},
role: {
type: String,
enum: Object.values(roles),
default: roles.USER
},
passwordResetToken: String,
passwordResetExpires: Date,
active: {
type: Boolean,
default: true,
select: false
}
}, {
timestamps: true
});
// Encrypt password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Sign JWT token
userSchema.methods.getSignedJwtToken = function() {
return jwt.sign(
{ id: this._id, role: this.role },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
};
// Match password
userSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
Authentication Middleware
Create src/middleware/auth.js
:
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const protect = async (req, res, next) => {
try {
let token;
if (req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({
message: 'Not authorized to access this route'
});
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get user from token
req.user = await User.findById(decoded.id);
if (!req.user) {
return res.status(401).json({
message: 'User no longer exists'
});
}
next();
} catch (error) {
return res.status(401).json({
message: 'Not authorized to access this route'
});
}
};
module.exports = protect;
Role-based Authorization Middleware
Create src/middleware/roleCheck.js
:
const { permissions } = require('../config/auth');
const checkPermission = (action) => {
return (req, res, next) => {
const userRole = req.user.role;
if (!permissions.books[action].includes(userRole)) {
return res.status(403).json({
message: 'Not authorized to perform this action'
});
}
next();
};
};
module.exports = checkPermission;
Authentication Routes
Create src/routes/auth.js
:
const express = require('express');
const router = express.Router();
const User = require('../models/User');
const protect = require('../middleware/auth');
// Register user
router.post('/register', async (req, res, next) => {
try {
const { name, email, password } = req.body;
const user = await User.create({
name,
email,
password
});
sendTokenResponse(user, 201, res);
} catch (error) {
next(error);
}
});
// Login user
router.post('/login', async (req, res, next) => {
try {
const { email, password } = req.body;
// Validate email & password
if (!email || !password) {
return res.status(400).json({
message: 'Please provide an email and password'
});
}
// Check for user
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({
message: 'Invalid credentials'
});
}
// Check if password matches
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return res.status(401).json({
message: 'Invalid credentials'
});
}
sendTokenResponse(user, 200, res);
} catch (error) {
next(error);
}
});
// Get current logged in user
router.get('/me', protect, async (req, res) => {
const user = await User.findById(req.user.id);
res.json(user);
});
// Logout user
router.get('/logout', (req, res) => {
res.cookie('token', 'none', {
expires: new Date(Date.now() + 10 * 1000),
httpOnly: true
});
res.json({
message: 'User logged out successfully'
});
});
// Helper function to get token from model, create cookie and send response
const sendTokenResponse = (user, statusCode, res) => {
const token = user.getSignedJwtToken();
const options = {
expires: new Date(
Date.now() + process.env.JWT_COOKIE_EXPIRE * 24 * 60 * 60 * 1000
),
httpOnly: true
};
if (process.env.NODE_ENV === 'production') {
options.secure = true;
}
res.status(statusCode)
.cookie('token', token, options)
.json({
token
});
};
module.exports = router;
Updated Books Routes
Update src/routes/books.js
to include authentication and authorization:
const express = require('express');
const router = express.Router();
const Book = require('../models/Book');
const protect = require('../middleware/auth');
const checkPermission = require('../middleware/roleCheck');
// Public routes
router.get('/', async (req, res, next) => {
// ... existing get all books code ...
});
router.get('/:id', async (req, res, next) => {
// ... existing get single book code ...
});
// Protected routes
router.post('/',
protect,
checkPermission('create'),
async (req, res, next) => {
try {
const book = new Book({
...req.body,
createdBy: req.user.id
});
const savedBook = await book.save();
res.status(201).json(savedBook);
} catch (error) {
next(error);
}
}
);
router.put('/:id',
protect,
checkPermission('update'),
async (req, res, next) => {
try {
const book = await Book.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true }
);
if (!book) {
return res.status(404).json({
message: 'Book not found'
});
}
res.json(book);
} catch (error) {
next(error);
}
}
);
router.delete('/:id',
protect,
checkPermission('delete'),
async (req, res, next) => {
try {
const book = await Book.findByIdAndDelete(req.params.id);
if (!book) {
return res.status(404).json({
message: 'Book not found'
});
}
res.status(204).send();
} catch (error) {
next(error);
}
}
);
module.exports = router;
Updated Server File
Update src/server.js
:
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const connectDB = require('./config/database');
const errorHandler = require('./middleware/errorHandler');
const booksRouter = require('./routes/books');
const authRouter = require('./routes/auth');
// Initialize express app
const app = express();
const PORT = process.env.PORT || 3000;
// Connect to MongoDB
connectDB();
// Middleware
app.use(bodyParser.json());
// Routes
app.use('/api/auth', authRouter);
app.use('/api/books', booksRouter);
// Error handling middleware
app.use(errorHandler);
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Testing Authentication and Authorization
Test the new authentication endpoints:
# Register a new user
curl -X POST -H "Content-Type: application/json" \
-d '{
"name": "Test User",
"email": "test@example.com",
"password": "password123"
}' \
http://localhost:3000/api/auth/register
# Login
curl -X POST -H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123"
}' \
http://localhost:3000/api/auth/login
# Use the token for protected routes
curl -H "Authorization: Bearer YOUR_TOKEN_HERE" \
http://localhost:3000/api/auth/me
# Create a book (requires authentication)
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{
"title": "New Book",
"author": "Author Name"
}' \
http://localhost:3000/api/books
Security Best Practices Implemented
Password Security
- Passwords are hashed using bcrypt
- Minimum password length enforcement
- Password field excluded from queries by default
JWT Implementation
- Secure token generation
- Token expiration
- HTTP-only cookies option
Role-based Access Control
- Different user roles (admin, editor, user)
- Permission-based actions
- Granular access control
Input Validation
- Email validation
- Required fields checking
- Data sanitization
Error Handling
- Secure error messages
- Different messages for development/production
Next Steps
To further enhance your API:
- Implementing rate limiting
- Adding API documentation using Swagger
- Setting up automated testing
- Adding pagination for GET requests
- Implementing proper logging
Security Considerations
Environment Variables
- Keep all secrets in environment variables
- Use different secrets for development and production
- Regularly rotate secrets
Headers
- Use helmet middleware for security headers
- Implement CORS properly
- Set secure cookies in production
Input Validation
- Validate all input data
- Sanitize user input
- Use parameterized queries
Rate Limiting
- Implement rate limiting for auth routes
- Set up IP-based blocking
- Monitor for suspicious activity
Conclusion
You now have a secure API with authentication and authorization! This implementation includes:
- User registration and login
- JWT-based authentication
- Role-based authorization
- Protected routes
- Security best practices
Remember to:
- Regularly update dependencies
- Monitor for security vulnerabilities
- Keep secrets secure
- Implement proper logging
- Regular security audits
The next tutorial will cover implementing rate limiting for your API.
Related Posts
6 min read
The Model Context Protocol (MCP) has emerged as one of the most significant developments in AI technology in 2025. Launched by Anthropic in November 2024, MCP is an open standard designed to bridge AI...
5 min read
APIs (Application Programming Interfaces) are the backbone of modern digital applications. They allow different software systems to communicate, exchange data, and collaborate seamlessly. As businesse...