In today's digital landscape, ensuring the security of your application is paramount, especially when dealing with user data. One method that has emerged as a robust solution for securing APIs is JSON Web Token (JWT) authentication. In this blog post, we will explore how to implement JWT authentication in a Node.js application as part of our CRUD app with MongoDB and TypeScript.
Understanding JWT
Before we jump into implementation, let’s clarify what JWT is. JSON Web Tokens are an open standard used to share security information between two parties. This transfer is compact, URL-safe, and can be verified and trusted since it is digitally signed.
A JWT consists of three parts:
- Header: Indicates the type of token and the signing algorithm being used (typically HMAC SHA256).
- Payload: Contains the claims, which are the information we want to store (typically user ID, roles, and other user-specific data).
- Signature: Ensures that the token is not altered. It is formed by combining the encoded header, encoded payload, and a secret key.
Let’s break that down further in the context of our CRUD application.
Setting Up Your Node.js Environment
To follow along, ensure you have Node.js installed, and you're working in a project set up with TypeScript. You can initialize your project with:
mkdir my-crud-app cd my-crud-app npm init -y npm install express mongoose jsonwebtoken bcryptjs dotenv npm install --save-dev typescript @types/node @types/express @types/mongoose @types/jsonwebtoken @types/bcryptjs
Also, don't forget to set up your tsconfig.json
.
Identity Management: User Model
First, let’s create a user model. In your models
directory, create a file named User.ts
:
import mongoose, { Schema, Document } from 'mongoose'; export interface IUser extends Document { username: string; password: string; } const UserSchema: Schema = new Schema({ username: { type: String, required: true, unique: true }, password: { type: String, required: true } }); export const User = mongoose.model<IUser>('User', UserSchema);
In our user model, we are defining a simple schema that consists of a username
and password
.
Registering a User
Now we need to create a route to register users. Create a file named auth.ts
in your routes
directory:
import express from 'express'; import bcrypt from 'bcryptjs'; import { User } from '../models/User'; import jwt from 'jsonwebtoken'; const router = express.Router(); router.post('/register', async (req, res) => { try { const { username, password } = req.body; // Check if user already exists const existingUser = await User.findOne({ username }); if (existingUser) { return res.status(400).json({ message: 'User already exists' }); } const hashedPassword = await bcrypt.hash(password, 10); const user = new User({ username, password: hashedPassword }); await user.save(); // Generate JWT token const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET || 'your_secret', { expiresIn: '1h' }); res.status(201).json({ token }); } catch (error) { res.status(500).json({ message: 'Server error' }); } }); export default router;
Explanation
- We import necessary packages like
bcryptjs
for hashing passwords andjsonwebtoken
for creating JWTs. - We create a new user after checking if the user already exists.
- Upon successful registration, we return a JWT token, which can be used for subsequent requests.
Authenticating a User
Next, let’s create a route to authenticate a user. We’ll extend our auth.ts
file:
router.post('/login', async (req, res) => { const { username, password } = req.body; const user = await User.findOne({ username }); if (!user || !(await bcrypt.compare(password, user.password))) { return res.status(401).json({ message: 'Invalid credentials' }); } const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET || 'your_secret', { expiresIn: '1h' }); res.json({ token }); });
Explanation
- In the login route, we check the user's credentials against the database and compare the hash with the provided password.
- If valid, we generate a JWT token and send it back to the client.
Protecting Routes with JWT Middleware
Now, we need to protect certain routes. Create a middleware function that verifies the JWT token:
import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; export const verifyToken = (req: Request, res: Response, next: NextFunction) => { const token = req.headers['authorization']?.split(' ')[1]; if (!token) { return res.status(403).json({ message: 'Access denied' }); } jwt.verify(token, process.env.JWT_SECRET || 'your_secret', (err, decoded) => { if (err) { return res.status(401).json({ message: 'Invalid token' }); } // Add user information to the request for further usage req.user = decoded; next(); }); };
Explanation
- This middleware checks for the JWT in the
Authorization
header. - If valid, it adds the decoded user information to the request object for further use in subsequent middleware or route handlers.
Example: Securing a Protected Route
Let’s create a protected route for retrieving user information in your routes
directory:
router.get('/user', verifyToken, (req, res) => { res.json({ id: req.user.id, username: req.user.username }); });
Explanation
- This route uses the
verifyToken
middleware to ensure only authenticated users can access it. If the token is valid, the user’s information is sent back.
Final Steps
To complete the implementation, don’t forget to initialize your Express app and connect to MongoDB:
import express from 'express'; import mongoose from 'mongoose'; import dotenv from 'dotenv'; import authRoutes from './routes/auth'; dotenv.config(); const app = express(); app.use(express.json()); mongoose.connect(process.env.MONGODB_URI || 'your_mongodb_uri', { useNewUrlParser: true, useUnifiedTopology: true }) .then(() => console.log('MongoDB connected')) .catch(err => console.log(err)); app.use('/auth', authRoutes); app.listen(5000, () => console.log('Server running on port 5000'));
Explanation
- We set up the Express app, connect to MongoDB, and include our authentication routes to handle registration and login.
Implementing JWT authentication not only secures your application but also creates a scalable and practical way of managing user sessions. With this framework, you can further enhance your CRUD application with secure access controls and protect your user data effectively.