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! ๐