Skip to main content
Authentication Systems Tutorial
CHAPTER 17 Intermediate

Building a Complete Authentication Project

Updated: May 14, 2026
45 min read

# CHAPTER 17

Building a Complete Authentication Project

1. Introduction

You have studied the theory of hashing, the mechanics of JWTs, the architecture of Role-Based Access Control, and the necessity of rate limiting. To transition from a student to a professional backend engineer, you must synthesize these isolated concepts into a unified, secure system. In this chapter, we will architect a complete, production-grade Authentication API service. This serves as a blueprint you can deploy in your own portfolio projects.

2. Learning Objectives

By the end of this chapter, you will be able to:
  • Architect the folder structure for a secure REST API.
  • Synthesize Registration, Login, and Password Reset workflows.
  • Implement the Access/Refresh token pattern.
  • Apply Route Guards and Role-Based Access Control (RBAC).

3. Project Overview: The Secure Auth API

Requirements:
  • Stack: Node.js, Express, MongoDB (Mongoose), bcrypt, jsonwebtoken.
  • Features:
  • Secure Registration (Password hashing).
  • Login (Returns short-lived Access Token & long-lived Refresh Token).
  • Protected User Profile route.
  • Protected Admin-Only route (RBAC).
  • Rate Limiting on auth endpoints.

4. Step 1: The Project Architecture

Professional APIs separate the routing layer from the business logic layer.
text
123456789101112
auth_api/
    server.js           <-- The main entry point
    .env                <-- Secret Keys (Git Ignored)
    models/
        User.js         <-- Database Schema
    controllers/
        authController.js <-- Login/Register logic
    routes/
        authRoutes.js   <-- Endpoint mapping
    middleware/
        authGuard.js    <-- JWT and Role verification
        rateLimiter.js  <-- Brute force protection

5. Step 2: The Database Schema (MongoDB/Mongoose)

Open models/User.js. We define the user and store the Refresh Token directly on the user record for revocation purposes.
javascript
12345678910
const mongoose = require(&#039;mongoose&#039;);

const userSchema = new mongoose.Schema({
    email: { type: String, required: true, unique: true, lowercase: true },
    password: { type: String, required: true }, // Will be hashed!
    role: { type: String, enum: [&#039;user&#039;, &#039;admin&#039;], default: &#039;user&#039; },
    refreshToken: { type: String } // Stores the long-lived token
});

module.exports = mongoose.model(&#039;User&#039;, userSchema);

6. Step 3: The Authentication Controller

Open controllers/authController.js. This is the core logic.
javascript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
const bcrypt = require(&#039;bcrypt&#039;);
const jwt = require(&#039;jsonwebtoken&#039;);
const User = require(&#039;../models/User&#039;);

// The Registration Logic
exports.register = async (req, res) => {
    try {
        const { email, password } = req.body;
        
        // 1. Hash password
        const hashedPassword = await bcrypt.hash(password, 10);
        
        // 2. Save user
        const newUser = new User({ email, password: hashedPassword });
        await newUser.save();
        
        res.status(201).json({ message: "User registered successfully" });
    } catch (error) {
        res.status(500).json({ error: "Registration failed" });
    }
};

// The Login Logic (The Dual-Token System)
exports.login = async (req, res) => {
    const { email, password } = req.body;
    
    // 1. Find User
    const user = await User.findOne({ email });
    if (!user) return res.status(401).json({ error: "Invalid credentials" });
    
    // 2. Verify Password
    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) return res.status(401).json({ error: "Invalid credentials" });
    
    // 3. Generate Tokens
    const accessToken = jwt.sign({ userId: user._id, role: user.role }, process.env.JWT_SECRET, { expiresIn: &#039;15m&#039; });
    const refreshToken = jwt.sign({ userId: user._id }, process.env.REFRESH_SECRET, { expiresIn: &#039;7d&#039; });
    
    // 4. Save Refresh Token to DB
    user.refreshToken = refreshToken;
    await user.save();
    
    // 5. Send Response
    // Best Practice: Send Access Token in JSON, send Refresh Token in an HttpOnly Cookie!
    res.cookie(&#039;jwt_refresh&#039;, refreshToken, { httpOnly: true, secure: true, maxAge: 7 * 24 * 60 * 60 * 1000 });
    res.json({ accessToken });
};

7. Step 4: The Middleware Guards

Open middleware/authGuard.js.
javascript
123456789101112131415161718192021
const jwt = require(&#039;jsonwebtoken&#039;);

// 1. AuthN Guard
exports.verifyToken = (req, res, next) => {
    const token = req.headers.authorization?.split(&#039; &#039;)[1];
    if (!token) return res.status(401).json({ error: "Access Denied" });

    jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
        if (err) return res.status(403).json({ error: "Token Expired or Invalid" });
        req.user = decoded;
        next();
    });
};

// 2. AuthZ Guard (RBAC)
exports.requireAdmin = (req, res, next) => {
    if (req.user.role !== &#039;admin&#039;) {
        return res.status(403).json({ error: "Forbidden: Admins Only" });
    }
    next();
};

8. Step 5: Assembling the Routes

Open routes/authRoutes.js. We apply the rate limiter to the login route and chain our middlewares to the protected routes.
javascript
12345678910111213141516171819202122
const express = require(&#039;express&#039;);
const router = express.Router();
const authController = require(&#039;../controllers/authController&#039;);
const { verifyToken, requireAdmin } = require(&#039;../middleware/authGuard&#039;);
const { loginLimiter } = require(&#039;../middleware/rateLimiter&#039;);

// Public Routes
router.post(&#039;/register&#039;, authController.register);
router.post(&#039;/login&#039;, loginLimiter, authController.login);

// Protected Routes (Chaining the Middleware!)
// Standard users can view their profile
router.get(&#039;/profile&#039;, verifyToken, (req, res) => {
    res.json({ message: "Profile Data", userId: req.user.userId });
});

// ONLY Admins can view the system settings
router.get(&#039;/admin/settings&#039;, verifyToken, requireAdmin, (req, res) => {
    res.json({ message: "Sensitive Admin Settings" });
});

module.exports = router;

9. Step 6: The Final Assembly

Open server.js.
javascript
123456789101112131415
require(&#039;dotenv&#039;).config();
const express = require(&#039;express&#039;);
const mongoose = require(&#039;mongoose&#039;);
const authRoutes = require(&#039;./routes/authRoutes&#039;);

const app = express();
app.use(express.json());

// Mount the router
app.use(&#039;/api&#039;, authRoutes);

mongoose.connect(process.env.MONGO_URI).then(() => {
    console.log("Connected to Database");
    app.listen(3000, () => console.log("Server running on port 3000"));
});

10. Summary

You have just architected a professional authentication service! Look at the elegance of the routing layer: router.get('/admin', verifyToken, requireAdmin, ...) reads like plain English. The complexity of hashing, token generation, and role verification is neatly abstracted into dedicated controllers and middleware files. By implementing the Dual-Token system (Access + Refresh), managing roles (RBAC), and utilizing rate-limiting, this blueprint is ready to power a highly secure, scalable production application.

11. Next Chapter Recommendation

Before deploying this code, how do we prove it works without manually typing emails into a frontend form? Proceed to Chapter 18: Authentication Testing and Debugging.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·