7 min read
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
• 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...
• 4 min read
In today’s interconnected digital world, APIs (Application Programming Interfaces) are the backbone of communication between different software applications. From mobile apps to cloud services, APIs e...
• 5 min read
In the modern digital ecosystem, APIs (Application Programming Interfaces) serve as the backbone of connectivity. Whether you're building microservices, enabling integrations, or crafting data pipelin...