Building a RESTful API from Scratch with Node.js (Part 3)
APIs tutorialcreate an APIAPI development guideTutorial

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

  1. Password Security

    • Passwords are hashed using bcrypt
    • Minimum password length enforcement
    • Password field excluded from queries by default
  2. JWT Implementation

    • Secure token generation
    • Token expiration
    • HTTP-only cookies option
  3. Role-based Access Control

    • Different user roles (admin, editor, user)
    • Permission-based actions
    • Granular access control
  4. Input Validation

    • Email validation
    • Required fields checking
    • Data sanitization
  5. Error Handling

    • Secure error messages
    • Different messages for development/production

Next Steps

To further enhance your API:

  1. Implementing rate limiting
  2. Adding API documentation using Swagger
  3. Setting up automated testing
  4. Adding pagination for GET requests
  5. Implementing proper logging

Security Considerations

  1. Environment Variables

    • Keep all secrets in environment variables
    • Use different secrets for development and production
    • Regularly rotate secrets
  2. Headers

    • Use helmet middleware for security headers
    • Implement CORS properly
    • Set secure cookies in production
  3. Input Validation

    • Validate all input data
    • Sanitize user input
    • Use parameterized queries
  4. 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

API Management Tools: A Comprehensive Overview
APIs tutorialcreate an APIAPI development guideTutorial

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...

API Security: Best Practices for Protecting Your Application
APIs tutorialcreate an APIAPI development guideTutorial

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...

API Design Best Practices: Crafting Robust and Scalable APIs
APIs tutorialcreate an APIAPI development guideTutorial

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...