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
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();
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;
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;
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,
};
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;
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;
// 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;
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.
Top comments (0)