Implementing JWT Authentication and Password Encryption with Bcrypt in a Node.js Application

Shubham Pal - Jun 24 - - Dev Community

Introduction

Authentication and authorization are fundamental aspects of modern web applications. They ensure that users can securely access resources based on their identity and permissions. JSON Web Tokens (JWT) and Bcrypt are widely used technologies for implementing secure authentication systems. In this blog, we’ll explore why we need JWT and Bcrypt, and how to implement them in a Node.js application.

Why Use JWT and Bcrypt?

JWT (JSON Web Token)

JWT is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as

a JSON object that is digitally signed using a secure algorithm. JWTs are widely used because they are:

  • Stateless: JWTs can be verified without storing session data on the server, making them scalable.
  • Compact: They are compact in size, making them ideal for mobile applications or inter-service communication.
  • Self-contained: JWTs contain all the information needed for authentication, reducing the need for multiple database lookups.

Bcrypt

Bcrypt is a password hashing function designed to be computationally intensive and slow, making it more resistant to brute-force attacks. It provides:

  • Salted Hashing: Each password is hashed with a unique salt, preventing rainbow table attacks.
  • Adjustable Cost Factor: The computational cost can be adjusted to slow down hashing, increasing security.

Implementing JWT and Bcrypt in a Node.js Application

Project Structure

/my-app
├── /backend
│   ├── config/
│   │   └── mongoose.connect.js
│   ├── controllers/
│   │   └── auth.controllers.js
│   ├── models/
│   │   └── user.models.js
│   ├── routes/
│   │   └── auth.routes.js
│   ├── middleware/
│   │   └── hashPassword.js
│   │   └── validateLogin.js
│   └── app.js
└── package.json
Enter fullscreen mode Exit fullscreen mode

Implementation

Setting Up Express Server

// app.js
const express = require('express');
const authRoutes = require('./routes/auth.routes');
const connectToDatabase = require('./config/mongoose.connect');

const app = express();
const PORT = process.env.PORT || 5000;

// Middleware
app.use(express.json());

// Routes
app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.use('/auth', authRoutes);

// Database connection and server startup
async function startServer() {
  try {
    await connectToDatabase();
    app.listen(PORT, () => {
      console.log(`Server is running on port ${PORT}`);
    });
  } catch (error) {
    console.error('Failed to start server:', error);
    process.exit(1);
  }
}

startServer();
Enter fullscreen mode Exit fullscreen mode

Setting Up Database Connection

We'll use Mongoose as the database.

// config/mongoose.connect.js
const mongoose = require('mongoose');
require('dotenv').config();

const MongoDb = process.env.MONGO_DB_URI;
mongoose.set('strictQuery', true);

if (!MongoDb) {
  console.error('MONGO_DB_URI is not defined. Please check your environment variables.');
  process.exit(1);
}

async function connectToDatabase() {
  try {
    await mongoose.connect(MongoDb);
    console.log('Connected to MongoDB');
  } catch (error) {
    console.error('Error connecting to MongoDB', error);
  }
}

module.exports = connectToDatabase;
Enter fullscreen mode Exit fullscreen mode

Creating User Model

Define the basic structure of user data.

// models/user.models.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  }
});

const User = mongoose.model('User', userSchema);

module.exports = User;
Enter fullscreen mode Exit fullscreen mode

Auth Controller

Create an authentication controller to handle registration and login.

// controllers/auth.controllers.js
const User = require('../models/user.models');
const jwt = require('jsonwebtoken');

// Controller for handling user login
const loginUser = async (req, res) => {
  try {
    const token = jwt.sign(
      { id: req.user._id, username: req.user.username },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    res.status(200).json({
      message: 'Login successful',
      token,
      user: { id: req.user._id, username: req.user.username }
    });
  } catch (error) {
    res.status(500).json({ message: 'Server error', error: error.message });
  }
};

// Controller for getting all users
const getAllUsers = async (req, res) => {
  try {
    const users = await User.find({}).select('_id username');
    res.status(200).json(users);
  } catch (error) {
    res.status(500).json({ message: 'Error fetching users', error: error.message });
  }
};

// Controller for registering users
const registerUser = async (req, res) => {
  const { username, password } = req.body;
  try {
    const existingUser = await User.findOne({ username });
    if (existingUser) {
      return res.status(400).json({ message: 'Username already exists' });
    }
    const newUser = await User.create({
      username,
      password,
    });
    res.status(201).json({
      message: 'User registered successfully',
      user: {
        id: newUser._id,
        username: newUser.username,
      }
    });
  } catch (error) {
    console.error('Registration error:', error);
    res.status(500).json({ message: 'Server error', error: error.message });
  }
};

module.exports = {
  registerUser,
  loginUser,
  getAllUsers,
};
Enter fullscreen mode Exit fullscreen mode

Auth Routes

Create routes for registering and logging in users and redirect those routes to respective controllers.

// routes/auth.routes.js
const express = require('express');
const { registerUser, loginUser, getAllUsers } = require('../controllers/auth.controllers');
const hashPassword = require('../middleware/hashPassword');
const validateLogin = require('../middleware/validateLogin');

const router = express.Router();
router.post('/register', hashPassword, registerUser);
router.post('/login', validateLogin, loginUser);
router.get('/users', getAllUsers);

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Middleware

Validate credentials and hash the original password using bcrypt.

// middleware/validateLogin.js
const User = require('../models/user.models');
const bcrypt = require('bcrypt');

const validateLogin = async (req, res, next) => {
  const { username, password } = req.body;
  try {
    const user = await User.findOne({ username });
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    const isMatch = await bcrypt.compare(password, user.password);
    if (!isMatch) {
      return res.status(400).json({ message: 'Invalid credentials' });
    }
    req.user = user;
    next();
  } catch (error) {
    res.status(500).json({ message: 'Server error', error: error.message });
  }
};

module.exports = validateLogin;
Enter fullscreen mode Exit fullscreen mode
// middleware/hashPassword.js
const bcrypt = require('bcrypt');

const hashPassword = async (req, res, next) => {
  try {
    const { password } = req.body;
    const hashedPassword = await bcrypt.hash(password, 10);
    req.body.password = hashedPassword;
    next();
  } catch (error) {
    res.status(500).json({ message: 'Error hashing password', error: error.message });
  }
};

module.exports = hashPassword;
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this blog, we explored the importance of using JWT for stateless authentication and Bcrypt for secure password hashing. We implemented a simple authentication system in a Node.js application with Express, demonstrating how to register and log in users securely. This setup provides a robust foundation for building more complex authentication and authorization mechanisms in your web applications. For a detailed implementation and to explore the complete project structure, you can visit the GitHub repository.

.