Learn how to implement Role-Based Access Control (RBAC) in Next.js with JWT authentication, refresh tokens, secure API routes, and role-based UI rendering. This step-by-step guide covers authentication, protected routes, middleware security, and dynamic UI updates for a secure Next.js application
📌 Introduction
Managing access control in a Next.js application is crucial, especially for admin dashboards, multi-user systems, and API security. In this guide, we'll implement RBAC (Role-Based Access Control) with:
✅ JWT Authentication (Access & Refresh Tokens)
✅ Local Storage & Cookies for Session Management
✅ Role-Based UI Rendering
✅ Client-Side Protection (HOC: AuthGuard)
✅ Server-Side Middleware for Secure Routing
✅ Secure API Protection
By the end, your app will have secure authentication, refresh token handling, and role-based page access.
🛠 1️⃣ Authentication System (Login & Refresh Tokens)
🔹 User Authentication with JWT Tokens
We'll generate two JWTs:
- Access Token (expires in 15 min)
- Refresh Token (used to get a new Access Token when expired)
🔹 Mock User Data
We simulate a user database:
const users = [
{ id: 1, username: "admin", password: "admin123", role: "admin" },
{ id: 2, username: "user", password: "user123", role: "user" },
];
🔹 2️⃣ Create API Routes for Login & Token Refresh
📌 pages/api/login.js (Generate Access & Refresh Tokens)
import jwt from "jsonwebtoken";
import { serialize } from "cookie";
const SECRET_KEY = "SECRET_KEY";
const REFRESH_SECRET = "REFRESH_SECRET";
export default function handler(req, res) {
if (req.method !== "POST") return res.status(405).end();
const { username, password } = req.body;
const user = users.find(
(u) => u.username === username && u.password === password
);
if (!user) return res.status(401).json({ message: "Invalid credentials" });
// Generate Tokens
const accessToken = jwt.sign({ id: user.id, role: user.role }, SECRET_KEY, {
expiresIn: "15m",
});
const refreshToken = jwt.sign(
{ id: user.id, role: user.role },
REFRESH_SECRET,
{ expiresIn: "7d" }
);
res.setHeader(
"Set-Cookie",
serialize("refreshToken", refreshToken, { httpOnly: true, path: "/" })
);
res.status(200).json({ accessToken, role: user.role });
}
✅ Stores refresh token in cookies
✅ Returns access token & user role
📌 pages/api/refresh.js (Generate New Access Token)
import jwt from "jsonwebtoken";
const SECRET_KEY = "SECRET_KEY";
const REFRESH_SECRET = "REFRESH_SECRET";
export default function handler(req, res) {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken)
return res.status(403).json({ message: "No refresh token" });
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
const newAccessToken = jwt.sign(
{ id: decoded.id, role: decoded.role },
SECRET_KEY,
{ expiresIn: "15m" }
);
res.status(200).json({ accessToken: newAccessToken });
} catch {
res.status(403).json({ message: "Invalid refresh token" });
}
}
✅ Verifies refresh token & issues new access token
🔐 3️⃣ Client-Side Authentication
🔹 Create a Login Page
pages/login.js
import { useState } from "react";
import { useRouter } from "next/router";
export default function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const router = useRouter();
const handleLogin = async (e) => {
e.preventDefault();
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (res.ok) {
const data = await res.json();
localStorage.setItem("accessToken", data.accessToken);
localStorage.setItem("role", data.role);
router.push("/dashboard");
} else {
alert("Invalid credentials");
}
};
return (
<form onSubmit={handleLogin}>
<input
type="text"
placeholder="Username"
onChange={(e) => setUsername(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit">Login</button>
</form>
);
}
🔹 Stores access token & role in localStorage
🔹 Redirects users after login
🛡 4️⃣ Implementing Role-Based Page Protection (HOC)
🔹 AuthGuard.js (Restrict Pages Based on Role)
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
const AuthGuard = ({ children, allowedRoles }) => {
const [authenticated, setAuthenticated] = useState(false);
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem("accessToken");
const role = localStorage.getItem("role");
if (!token || !allowedRoles.includes(role)) {
router.push("/login");
} else {
setAuthenticated(true);
}
}, []);
if (!authenticated) return null;
return children;
};
export default AuthGuard;
✅ Restricts pages based on roles
🎯 5️⃣ Role-Based UI Rendering
📌 components/Navbar.js
import { useEffect, useState } from "react";
export default function Navbar() {
const [role, setRole] = useState("");
useEffect(() => {
setRole(localStorage.getItem("role"));
}, []);
return (
<nav>
<a href="/">Home</a>
{role === "admin" && <a href="/admin-dashboard">Admin Panel</a>}
{role && <a href="/user-dashboard">User Dashboard</a>}
{!role && <a href="/login">Login</a>}
</nav>
);
}
✅ Dynamically updates UI based on role
⚙ 6️⃣ Secure Route Protection Using Middleware
📌 middleware.js
import { NextResponse } from "next/server";
import jwt from "jsonwebtoken";
const SECRET_KEY = "SECRET_KEY";
export function middleware(req) {
const token = req.cookies.get("token");
try {
if (token) {
const decoded = jwt.verify(token, SECRET_KEY);
if (
req.nextUrl.pathname.startsWith("/admin-dashboard") &&
decoded.role !== "admin"
) {
return NextResponse.redirect(new URL("/login", req.url));
}
} else {
return NextResponse.redirect(new URL("/login", req.url));
}
} catch {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
✅ Prevents direct access even if localStorage is modified
✅ Conclusion
🎯 Built a Secure Authentication System
🔹 Implemented JWT with Refresh Tokens
🔹 Added Role-Based Page & UI Protection
🔹 Secured API & Middleware for route protection
Now your Next.js app is fully secured with RBAC & Refresh Tokens! 🚀


