Inteliny
SecurityLevel: Intermediate30mVerified Production Blueprint

How to Build Secure JWT Authentication & Refresh Token Flow in MERN Stack

Learn to implement secure JWT authentication and refresh token flow in a MERN stack application

Inteliny Engineering

Principal Architect

Overview & Architecture Scope

This guide covers the implementation of secure JWT authentication and refresh token flow in a MERN stack application. It is intended for developers who have a basic understanding of the MERN stack and want to add secure authentication to their application. By the end of this guide, you will have a fully functional secure JWT authentication system with refresh tokens, secure cookies, and CSRF protection. The tools and prerequisites needed for this guide include Node.js, Express.js, MongoDB, and React.js. You will achieve a secure authentication system that protects user data and prevents common web vulnerabilities.

Prerequisites & System Requirements

Ensure your development workstation or staging server fulfills the following prerequisites before initiating commands:

Node.js v18.0+ runtime environment
MongoDB v6.0+ database cluster
Active AWS or Cloudflare account with DNS access
Linux Ubuntu 22.04 LTS server instance
Basic knowledge of CLI bash & Git workflow

Target System Architecture Diagram

Browser
NGINX
Node / Express
MongoDB / Redis

Interactive Execution Checklist

0/11 Completed

Step-by-Step Implementation Guide

1

Step 1: Set up the Project Structure

Create a new MERN stack project with the required dependencies, including express.js, mongoose, and jsonwebtoken. Initialize a new Node.js project using npm init and install the required dependencies using npm install.

Execute Command Terminal:

bash
npm init -y && npm install express mongoose jsonwebtoken
Make sure to install the latest versions of the dependencies
2

Step 2: Configure MongoDB and Mongoose

Set up a new MongoDB database and connect to it using Mongoose. Create a new Mongoose model for the user collection.

Execute Command Terminal:

javascript
const mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/auth', { useNewUrlParser: true, useUnifiedTopology: true }); const userSchema = new mongoose.Schema({ username: String, password: String }); const User = mongoose.model('User', userSchema);
Make sure to replace the MongoDB connection string with your own
3

Step 3: Implement User Registration

Create a new API endpoint for user registration. Hash the user's password using a secure hashing algorithm like bcrypt.

Execute Command Terminal:

javascript
const bcrypt = require('bcrypt'); const registerUser = async (req, res) => { const { username, password } = req.body; const hashedPassword = await bcrypt.hash(password, 10); const user = new User({ username, password: hashedPassword }); await user.save(); res.json({ message: 'User registered successfully' }); };
Make sure to use a secure hashing algorithm like bcrypt
4

Step 4: Implement User Login

Create a new API endpoint for user login. Verify the user's password using the hashed password stored in the database.

Execute Command Terminal:

javascript
const loginUser = async (req, res) => { const { username, password } = req.body; const user = await User.findOne({ username }); if (!user) return res.status(401).json({ message: 'Invalid username or password' }); const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) return res.status(401).json({ message: 'Invalid username or password' }); const token = jwt.sign({ userId: user._id }, process.env.SECRET_KEY, { expiresIn: '1h' }); res.json({ token }); };
Make sure to use a secure secret key for signing the JWT
5

Step 5: Implement JWT Access Token and Refresh Token

Create a new API endpoint for generating a new access token using the refresh token.

Execute Command Terminal:

javascript
const generateAccessToken = async (req, res) => { const { refreshToken } = req.body; const user = await User.findOne({ refreshToken }); if (!user) return res.status(401).json({ message: 'Invalid refresh token' }); const accessToken = jwt.sign({ userId: user._id }, process.env.SECRET_KEY, { expiresIn: '1h' }); res.json({ accessToken }); };
Make sure to use a secure secret key for signing the JWT
6

Step 6: Implement Secure Cookies and HttpOnly Cookies

Set the access token and refresh token in secure cookies using the HttpOnly flag.

Execute Command Terminal:

javascript
const setSecureCookies = (res, accessToken, refreshToken) => { res.cookie('accessToken', accessToken, { httpOnly: true, secure: true, sameSite: 'strict' }); res.cookie('refreshToken', refreshToken, { httpOnly: true, secure: true, sameSite: 'strict' }); };
Make sure to use the HttpOnly flag to prevent JavaScript access to the cookies
7

Step 7: Implement Token Rotation and CSRF Protection

Rotate the access token and refresh token on each request. Verify the CSRF token on each request.

Execute Command Terminal:

javascript
const rotateTokens = async (req, res) => { const { accessToken, refreshToken } = req.cookies; const user = await User.findOne({ refreshToken }); if (!user) return res.status(401).json({ message: 'Invalid refresh token' }); const newAccessToken = jwt.sign({ userId: user._id }, process.env.SECRET_KEY, { expiresIn: '1h' }); const newRefreshToken = jwt.sign({ userId: user._id }, process.env.SECRET_KEY, { expiresIn: '30d' }); setSecureCookies(res, newAccessToken, newRefreshToken); };
Make sure to rotate the tokens on each request to prevent token fixation attacks
8

Step 8: Implement Logout Flow

Clear the access token and refresh token cookies on logout.

Execute Command Terminal:

javascript
const logoutUser = (res) => { res.clearCookie('accessToken'); res.clearCookie('refreshToken'); };
Make sure to clear the cookies on logout to prevent token reuse
9

Step 9: Implement RBAC and Protected Routes

Implement role-based access control (RBAC) using middleware functions. Protect routes using the access token and refresh token.

Execute Command Terminal:

javascript
const authenticateUser = async (req, res, next) => { const { accessToken } = req.cookies; if (!accessToken) return res.status(401).json({ message: 'Unauthorized' }); const user = await User.findOne({ accessToken }); if (!user) return res.status(401).json({ message: 'Unauthorized' }); req.user = user; next(); };
Make sure to use middleware functions to protect routes
10

Step 10: Test the Application

Test the application using tools like Postman or cURL. Verify that the access token and refresh token are being generated and rotated correctly.

Execute Command Terminal:

javascript
const testApplication = async () => { const response = await axios.post('https://example.com/login', { username: 'test', password: 'test' }); const { accessToken, refreshToken } = response.data; console.log(accessToken, refreshToken); };
Make sure to test the application thoroughly to ensure that it is working as expected
11

Step 11: Deploy the Application to Production

Deploy the application to a production environment using tools like Docker or Kubernetes. Verify that the application is working as expected in the production environment.

Execute Command Terminal:

javascript
const deployApplication = async () => { const response = await axios.post('https://example.com/deploy', { environment: 'production' }); console.log(response.data); };
Make sure to deploy the application to a production environment to ensure that it is available to users

Pro Tips & Optimizations

Use a secure secret key for signing the JWT
Use the HttpOnly flag to prevent JavaScript access to the cookies
Rotate the tokens on each request to prevent token fixation attacks
Use middleware functions to protect routes

Common Pitfalls to Avoid

Not using a secure secret key for signing the JWT
Not using the HttpOnly flag to prevent JavaScript access to the cookies
Not rotating the tokens on each request
Not using middleware functions to protect routes
Conclusion & Next Steps

In this guide, we implemented secure JWT authentication and refresh token flow in a MERN stack application. We used middleware functions to protect routes and rotated the tokens on each request to prevent token fixation attacks. We also tested the application using tools like Postman or cURL and deployed it to a production environment using tools like Docker or Kubernetes. By following the steps outlined in this guide, you can implement secure JWT authentication in your own MERN stack application.

Production Best Practices & Hardening

Security Hardening

Disable root SSH access, enforce key-based auth, and enable UFW firewall on ports 80/443.

Memory Management

Set Node.js max-old-space-size to 80% of total RAM to avoid Linux OOM-killer crashes.

Frequently Asked Questions

Yes, all NGINX, Docker, and PM2 deployment steps can be packaged into Infrastructure as Code (IaC) playbooks.

Need Help Implementing This?

Partner with Inteliny's principal architects to audit your stack, automate CI/CD, and accelerate deployment.

How to Build Secure JWT Authentication & Refresh Token Flow in MERN Stack | Inteliny Knowledge Base