Securing your fullstack application is critical to protect user data and maintain trust. One of the most common ways to implement authentication and authorization in modern applications is through JSON Web Tokens (JWT).
In this guide, we’ll explore how to secure your Next.js app by setting up JWT-based authentication and implementing role-based authorization.
What Is JWT and Why Use It?
JWT is a compact, self-contained way to securely transmit information between parties as a JSON object. It is often used for:
- Authentication: Verifying the identity of a user.
- Authorization: Controlling access to resources based on user roles or permissions.
How JWT works
- Login: The client sends login credentials (e.g., email and password) to the backend.
- Token generation: If valid, the server generates a JWT containing user details and sends it back.
-
Storage: The client stores the JWT (e.g., in
httpOnly
cookies or local storage). - Subsequent Requests: The client includes the JWT in the request headers to access protected resources.
- Verification: The server verifies the token's validity before granting access.
Step 1: Set Up Your Next.js and Node.js Environment
Before diving into the code, ensure you have the following:
- Node.js installed.
- A new Next.js project ready to run: run the command
npx create-next-app@latest your-project-name
to create a Next.js app. - The following dependencies installed on your backend folder:
npm install jsonwebtoken bcryptjs express cors body-parser cookie-parser
Your Next.js environment is now ready to create some magic 🚀
Step 2: Build the Backend API
We’ll use Node.js with Express to handle authentication.
Create the Express server
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const bodyParser = require('body-parser');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const app = express();
const PORT = 5001;
// Middleware
app.use(cors({ origin: 'http://localhost:3000', credentials: true }));
app.use(bodyParser.json());
app.use(cookieParser());
// Secret key for JWT
const SECRET_KEY = 'your-secret-key';
// Mock user database
const users = [
{ id: 1, email: 'user@user.com', password: bcrypt.hashSync('aw3$0m3AndHaRdPwD!', 10), role: 'user' },
{ id: 2, email: 'admin@admin.com', password: bcrypt.hashSync('aw3$0m3AndHaRdPwDButAdm1n!', 10), role: 'admin' },
];
Create Login route
app.post('/api/login', (req, res) => {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user || !bcrypt.compareSync(password, user.password)) {
return res.status(401).json({ message: 'Invalid email or password' });
}
// Generate JWT
const token = jwt.sign({ id: user.id, role: user.role }, SECRET_KEY, { expiresIn: '1h' });
// Set as httpOnly cookie
res.cookie('token', token, { httpOnly: true }).json({ message: 'Logged in successfully' });
});
Create Protected route
app.get('/api/protected', (req, res) => {
const token = req.cookies.token;
if (!token) {
return res.status(401).json({ message: 'Unauthorized' });
}
try {
const decoded = jwt.verify(token, SECRET_KEY);
res.json({ message: 'Welcome to the protected route!', user: decoded });
} catch (err) {
res.status(401).json({ message: 'Invalid token' });
}
});
Create Logout route
app.post('/api/logout', (req, res) => {
res.clearCookie('token').json({ message: 'Logged out successfully' });
});
Don't forget to add at the end of your file:
// Start the server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
This is a very basic implementation of an Express app that will allow us to send HTTP request on the implemented routes 🎯
Start the server to test using the commande: node server.js
where server.js is the name of your file.
Step 3: Implement the Next.js frontend
First of all, don't forget to add the axios
dependency by using the command: npm install axios
in your Next.js app
Login page
Create a new file pages/login.js
:
import { useState } from 'react';
import axios from 'axios';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleLogin = async (e) => {
e.preventDefault();
try {
const response = await axios.post('http://localhost:5000/api/login', { email, password }, { withCredentials: true });
alert(response.data.message);
} catch (err) {
alert(err.response?.data?.message || 'Login failed');
}
};
return (
<form onSubmit={handleLogin}>
<input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} />
<input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit">Login</button>
</form>
);
}
This page renders a very simple login form where you can use the login information you added in the users
array variable in your server implementation. This is obviously not a good thing to do and you should never do this, this is a simple implementation to demonstrate the power of JWT security 💪
Add a protected page
Create a new file pages/protected.js
:
import axios from 'axios';
import { useEffect, useState } from 'react';
export default function Protected() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get('http://localhost:5000/api/protected', { withCredentials: true });
setData(response.data);
} catch (err) {
alert('Access denied');
}
};
fetchData();
}, []);
async function handleLogout() {
try {
await axios.post('http://localhost:5001/api/logout', {}, { withCredentials: true });
router.push('/login');
} catch (error) {
alert('Could not logout');
}
}
if (!data) return <p>Loading...</p>;
return <div>
{JSON.stringify(data)}
<button onClick={handleLogout}>Logout</button>
</div>;
}
This page renders the result of the server GET
request on the route /api/protected
✅
If you try to access this route without being logged in, an error alert will be displayed on the page and you will not be able to access its content ❌
Step 4: Add Role-Based Authorization
Enhance your backend to enforce roles:
function authorizeRoles(allowedRoles) {
return (req, res, next) => {
const token = req.cookies.token;
if (!token) return res.status(401).json({ message: 'Unauthorized' });
try {
const decoded = jwt.verify(token, SECRET_KEY);
if (!allowedRoles.includes(decoded.role)) {
return res.status(403).json({ message: 'Forbidden' });
}
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ message: 'Invalid token' });
}
};
}
// Example: Admin-only route
app.get('/api/admin', authorizeRoles(['admin']), (req, res) => {
res.json({ message: 'Welcome Admin!' });
});
This route uses a validation middleware authorizeRoles
that is the function we added right before. This function verifies there is a token and that the user trying to access the resource has the right authorization.
Conclusion
So here is how you can simply, and very fast (as always with my guides 🤩), secure your fullstack application with JWT technology in Next.js and Node.
This approach provides a reliable and scalable solution for managing user sessions while ensuring a seamless user experience.
Stay proactive about security by keeping dependencies updated, implementing HTTPS, and following best practices for managing tokens.
Happy coding!
Top comments (1)
Why did you write the backend in NodeJS and not NextJS? Since you're using NextJS on the frontend?