Node.js Intermediate

Node.js Authentication and Authorization: Securing Your Applications

CodingerWeb
CodingerWeb
18 views 90 min read

Authentication vs Authorization

Authentication verifies who a user is, while Authorization determines what an authenticated user can do. This lesson covers implementing both in Node.js applications.

Password Hashing with bcrypt

Never store passwords in plain text. Use bcrypt to hash passwords securely:

# Install bcrypt
npm install bcrypt
const bcrypt = require("bcrypt");

class PasswordManager {
    static async hashPassword(plainPassword) {
        try {
            const saltRounds = 12;
            const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
            return hashedPassword;
        } catch (err) {
            throw new Error("Error hashing password");
        }
    }
    
    static async comparePassword(plainPassword, hashedPassword) {
        try {
            const isMatch = await bcrypt.compare(plainPassword, hashedPassword);
            return isMatch;
        } catch (err) {
            throw new Error("Error comparing passwords");
        }
    }
}

// Usage example
async function demonstratePasswordHashing() {
    const password = "mySecretPassword123";
    
    // Hash password
    const hashed = await PasswordManager.hashPassword(password);
    console.log("Hashed password:", hashed);
    
    // Verify password
    const isValid = await PasswordManager.comparePassword(password, hashed);
    console.log("Password is valid:", isValid);
    
    // Wrong password
    const isInvalid = await PasswordManager.comparePassword("wrongPassword", hashed);
    console.log("Wrong password is valid:", isInvalid);
}

demonstratePasswordHashing();

JWT (JSON Web Tokens) Authentication

JWTs are a popular way to handle authentication in modern web applications:

# Install jsonwebtoken
npm install jsonwebtoken
const jwt = require("jsonwebtoken");

const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
const JWT_EXPIRES_IN = "7d";

class TokenManager {
    static generateToken(payload) {
        return jwt.sign(payload, JWT_SECRET, {
            expiresIn: JWT_EXPIRES_IN
        });
    }
    
    static verifyToken(token) {
        try {
            return jwt.verify(token, JWT_SECRET);
        } catch (err) {
            throw new Error("Invalid or expired token");
        }
    }
    
    static decodeToken(token) {
        return jwt.decode(token);
    }
}

// Middleware for protecting routes
const authenticateToken = (req, res, next) => {
    const authHeader = req.headers["authorization"];
    const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN
    
    if (!token) {
        return res.status(401).json({
            error: "Access token required"
        });
    }
    
    try {
        const decoded = TokenManager.verifyToken(token);
        req.user = decoded;
        next();
    } catch (err) {
        return res.status(403).json({
            error: "Invalid or expired token"
        });
    }
};

module.exports = { TokenManager, authenticateToken };

Complete Authentication System

const express = require("express");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const mysql = require("mysql2/promise");

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

// Database connection
const pool = mysql.createPool({
    host: "localhost",
    user: "root",
    password: "password",
    database: "auth_demo"
});

const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";

// User registration
app.post("/api/register", async (req, res) => {
    try {
        const { username, email, password } = req.body;
        
        // Validation
        if (!username || !email || !password) {
            return res.status(400).json({
                error: "Username, email, and password are required"
            });
        }
        
        if (password.length < 6) {
            return res.status(400).json({
                error: "Password must be at least 6 characters long"
            });
        }
        
        // Check if user already exists
        const [existingUsers] = await pool.execute(
            "SELECT id FROM users WHERE email = ? OR username = ?",
            [email, username]
        );
        
        if (existingUsers.length > 0) {
            return res.status(400).json({
                error: "User with this email or username already exists"
            });
        }
        
        // Hash password
        const hashedPassword = await bcrypt.hash(password, 12);
        
        // Create user
        const [result] = await pool.execute(
            "INSERT INTO users (username, email, password, created_at) VALUES (?, ?, ?, NOW())",
            [username, email, hashedPassword]
        );
        
        // Generate JWT token
        const token = jwt.sign(
            { userId: result.insertId, username, email },
            JWT_SECRET,
            { expiresIn: "7d" }
        );
        
        res.status(201).json({
            message: "User registered successfully",
            token,
            user: {
                id: result.insertId,
                username,
                email
            }
        });
        
    } catch (err) {
        console.error("Registration error:", err);
        res.status(500).json({ error: "Internal server error" });
    }
});

// User login
app.post("/api/login", async (req, res) => {
    try {
        const { email, password } = req.body;
        
        if (!email || !password) {
            return res.status(400).json({
                error: "Email and password are required"
            });
        }
        
        // Find user
        const [users] = await pool.execute(
            "SELECT id, username, email, password FROM users WHERE email = ?",
            [email]
        );
        
        if (users.length === 0) {
            return res.status(401).json({
                error: "Invalid email or password"
            });
        }
        
        const user = users[0];
        
        // Verify password
        const isPasswordValid = await bcrypt.compare(password, user.password);
        
        if (!isPasswordValid) {
            return res.status(401).json({
                error: "Invalid email or password"
            });
        }
        
        // Generate JWT token
        const token = jwt.sign(
            { userId: user.id, username: user.username, email: user.email },
            JWT_SECRET,
            { expiresIn: "7d" }
        );
        
        res.json({
            message: "Login successful",
            token,
            user: {
                id: user.id,
                username: user.username,
                email: user.email
            }
        });
        
    } catch (err) {
        console.error("Login error:", err);
        res.status(500).json({ error: "Internal server error" });
    }
});

// Authentication middleware
const authenticateToken = (req, res, next) => {
    const authHeader = req.headers["authorization"];
    const token = authHeader && authHeader.split(" ")[1];
    
    if (!token) {
        return res.status(401).json({
            error: "Access token required"
        });
    }
    
    try {
        const decoded = jwt.verify(token, JWT_SECRET);
        req.user = decoded;
        next();
    } catch (err) {
        return res.status(403).json({
            error: "Invalid or expired token"
        });
    }
};

// Protected route
app.get("/api/profile", authenticateToken, async (req, res) => {
    try {
        const [users] = await pool.execute(
            "SELECT id, username, email, created_at FROM users WHERE id = ?",
            [req.user.userId]
        );
        
        if (users.length === 0) {
            return res.status(404).json({ error: "User not found" });
        }
        
        res.json({ user: users[0] });
    } catch (err) {
        res.status(500).json({ error: "Internal server error" });
    }
});

app.listen(3000, () => {
    console.log("Auth server running on http://localhost:3000");
});

Role-Based Authorization

// Authorization middleware
const authorize = (roles = []) => {
    return async (req, res, next) => {
        try {
            // Get user role from database
            const [users] = await pool.execute(
                "SELECT role FROM users WHERE id = ?",
                [req.user.userId]
            );
            
            if (users.length === 0) {
                return res.status(404).json({ error: "User not found" });
            }
            
            const userRole = users[0].role;
            
            // Check if user has required role
            if (roles.length && !roles.includes(userRole)) {
                return res.status(403).json({
                    error: "Insufficient permissions"
                });
            }
            
            req.user.role = userRole;
            next();
        } catch (err) {
            res.status(500).json({ error: "Authorization error" });
        }
    };
};

// Admin-only route
app.get("/api/admin/users", 
    authenticateToken, 
    authorize(["admin"]), 
    async (req, res) => {
        try {
            const [users] = await pool.execute(
                "SELECT id, username, email, role, created_at FROM users"
            );
            res.json({ users });
        } catch (err) {
            res.status(500).json({ error: "Internal server error" });
        }
    }
);

// Moderator or Admin route
app.delete("/api/posts/:id", 
    authenticateToken, 
    authorize(["admin", "moderator"]), 
    async (req, res) => {
        // Delete post logic
        res.json({ message: "Post deleted" });
    }
);

Session-Based Authentication

const express = require("express");
const session = require("express-session");
const MySQLStore = require("express-mysql-session")(session);

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

// Session store configuration
const sessionStore = new MySQLStore({
    host: "localhost",
    port: 3306,
    user: "root",
    password: "password",
    database: "auth_demo"
});

// Session middleware
app.use(session({
    key: "session_cookie_name",
    secret: "session_cookie_secret",
    store: sessionStore,
    resave: false,
    saveUninitialized: false,
    cookie: {
        maxAge: 1000 * 60 * 60 * 24 // 24 hours
    }
}));

// Login route
app.post("/api/login", async (req, res) => {
    // ... authentication logic ...
    
    if (isPasswordValid) {
        // Store user info in session
        req.session.userId = user.id;
        req.session.username = user.username;
        req.session.isAuthenticated = true;
        
        res.json({
            message: "Login successful",
            user: {
                id: user.id,
                username: user.username,
                email: user.email
            }
        });
    }
});

// Session-based authentication middleware
const requireAuth = (req, res, next) => {
    if (req.session && req.session.isAuthenticated) {
        next();
    } else {
        res.status(401).json({ error: "Authentication required" });
    }
};

// Protected route
app.get("/api/dashboard", requireAuth, (req, res) => {
    res.json({
        message: "Welcome to dashboard",
        user: {
            id: req.session.userId,
            username: req.session.username
        }
    });
});

// Logout route
app.post("/api/logout", (req, res) => {
    req.session.destroy((err) => {
        if (err) {
            return res.status(500).json({ error: "Could not log out" });
        }
        res.json({ message: "Logged out successfully" });
    });
});

Exercise

Build a complete authentication system that includes:

  1. User registration with email verification
  2. Login with JWT tokens
  3. Password reset functionality
  4. Role-based access control
  5. Rate limiting for authentication endpoints

What's Next?

In the next lesson, we'll explore testing Node.js applications and implementing proper error handling.