Authentication & Authorization

Authentication & Authorization
  • Authentication and authorization are essential for securing Node.js applications

  • ensuring users are properly identified and granted appropriate permissions.


Authentication

  • Authentication verifies a user's identity using credentials such as a username and password.

  • It often involves checking if the credentials match stored data and issuing tokens or sessions upon successful verification.

Password Matching

  • To compare passwords securely, you can use the bcryptjs library, which hashes passwords and checks them against stored hashes.

Example of Password Matching:

import bcrypt from 'bcryptjs';

const matchPasswords = (password, hashedPassword) => {
    return bcrypt.compare(password, hashedPassword);
};

export default matchPasswords;

bcrypt.compare(password, hashedPassword): Compares a plain password with a hashed password and returns a promise that resolves to true if they match, otherwise false.

Authorization

Authorization controls access to resources based on user permissions or roles. It ensures that only authorized users can access certain routes or perform specific actions.

Token Verification

  • Use JSON Web Tokens (JWT) to manage user sessions and verify their authenticity.

  • Tokens are typically stored in cookies or local storage and are used to authenticate user requests.

Example of Token Verification Middleware:


import jwt from 'jsonwebtoken';

const verifyToken = (req, res, next) => {
    const token = req.cookies.token;
    if (token) {
        jwt.verify(token, process.env.TOKEN_ACCESS_SECRET, (err, data) => {
            if (err) {
                res.status(498).json({ message: 'Token is not valid' });
            } else {
                // Optionally attach user data to the request object
                req.user = data;
                next();
            }
        });
    } else {
        res.status(498).json({ message: 'Token is not valid' });
    }
};

export default verifyToken;
  • jwt.verify(token, process.env.TOKEN_ACCESS_SECRET, callback): Verifies the JWT token using a secret key.

    • If the token is valid, the callback receives the decoded data.

    • If not, an error is returned.

  • req.cookies.token: Retrieves the token from the request cookies.


Securing Authentication and Authorization

Hash Passwords

Ensure passwords are hashed using libraries like bcryptjs before storing them in the database.

Hashing Password Example:

import bcrypt from 'bcryptjs';

// Hash a password
const hashPassword = async (password) => {
    try {
        const salt = await bcrypt.genSalt(10); // Generate a salt
        const hashedPassword = await bcrypt.hash(password, salt); // Hash the password with the salt
        return hashedPassword;
    } catch (error) {
        console.error('Error hashing password:', error);
        throw new Error('Password hashing failed');
    }
};

// Compare a password with a hashed password
const comparePasswords = async (password, hashedPassword) => {
    try {
        const isMatch = await bcrypt.compare(password, hashedPassword);
        return isMatch;
    } catch (error) {
        console.error('Error comparing passwords:', error);
        throw new Error('Password comparison failed');
    }
};

export { hashPassword, comparePasswords };
  • bcrypt.genSalt(rounds): Generates a salt with the specified number of rounds.

  • bcrypt.hash(password, salt): Hashes the password using the salt.

  • bcrypt.compare(password, hashedPassword): Compares a plain password with a hashed password.

Secure Tokens

Use environment variables to keep secrets like TOKEN_ACCESS_SECRET secure and ensure tokens are sent over HTTPS.

import jwt from 'jsonwebtoken';

// Middleware to verify JWT tokens
const verifyToken = (req, res, next) => {
    const token = req.cookies.token;
    if (token) {
        jwt.verify(token, process.env.TOKEN_ACCESS_SECRET, (err, decoded) => {
            if (err) {
                return res.status(498).json({ message: 'Token is not valid' });
            }
            req.user = decoded; // Add the decoded user data to the request object
            next();
        });
    } else {
        res.status(401).json({ message: 'No token provided' });
    }
};

export default verifyToken;
  • jwt.verify(token, secret, callback): Verifies the token using the secret key

  • Environment Variables: Use .env files or other secure methods to manage secrets.

Error Handling

Implement robust error handling to manage unauthorized access and token verification failures gracefully.

import { hashPassword, comparePasswords } from '../utils/authUtils.js'; // Utility functions for password handling
import query from '../config/db.js'; // Database query utility

const userControllers = {
    // Example of user registration with password hashing
    registerUser: async (req, res) => {
        const { username, password } = req.body;
        try {
            if (!username || !password) {
                return res.status(400).json({ error: 'Username and password are required' });
            }
            const hashedPassword = await hashPassword(password);
            const sql = 'INSERT INTO users (username, password) VALUES (?, ?)';
            await query(sql, [username, hashedPassword]);
            res.status(201).json({ message: 'User registered successfully' });
        } catch (error) {
            console.error('Error registering user:', error);
            res.status(500).json({ error: 'Error registering user' });
        }
    },

    // Example of user login with password comparison
    loginUser: async (req, res) => {
        const { username, password } = req.body;
        try {
            if (!username || !password) {
                return res.status(400).json({ error: 'Username and password are required' });
            }
            const sql = 'SELECT password FROM users WHERE username = ?';
            const result = await query(sql, [username]);
            if (result.length === 0) {
                return res.status(404).json({ error: 'User not found' });
            }
            const isMatch = await comparePasswords(password, result[0].password);
            if (!isMatch) {
                return res.status(401).json({ error: 'Invalid credentials' });
            }
            const token = jwt.sign({ username }, process.env.TOKEN_ACCESS_SECRET, { expiresIn: '1h' });
            res.cookie('token', token, { httpOnly: true, secure: true }); // Set token in a secure cookie
            res.status(200).json({ message: 'Login successful' });
        } catch (error) {
            console.error('Error logging in user:', error);
            res.status(500).json({ error: 'Error logging in user' });
        }
    }
};

export default userControllers;
  • Password Hashing and Comparison: Ensure passwords are securely hashed and compared.

  • Token Management: Issue and verify tokens securely, and manage errors effectively.

  • Error Responses: Return meaningful error messages and status codes.

Last updated