Middleware not working as expected with different user roles
Unanswered
aka posted this in #help-forum
akaOP
I have a Next.js 14 app and I have designed the middleware to block some routes when the role of the user that is logged in is "USER" and to allow all routes when the role is "ADMIN". Also there is a part which is supposed to block every route except "/login" if the user is not authenticated and to redirect the user to the "/login" in case he wants to visit a certain page without being logged in first.
I am using JWT tokens for authentication with an additional claim "role" which is a string and I am generating those tokens on the backend (Spring Boot).
All of the above is working fine the first time the user logs in and navigates through the application. Also works when the user manually tries to enter the route in the browser itself.
The problem occurs when the user tries to visit a certain page (for example "/class-coverage-plans/new") as an "ADMIN" and the app lets the user do it, but after that the current user logs out and, quickly afterwards, a different user with the role "USER" logs in and tries to visit the same route as the admin did (the "USER" is not supposed to be allowed to visit the said route) and the app lets the user visit it.
What solves my problem is:
- pressing the refresh button in the browser after a different user logs in
- navigating with
Here is the main part of my middleware function:
Sorry for the long message and thanks!
I am using JWT tokens for authentication with an additional claim "role" which is a string and I am generating those tokens on the backend (Spring Boot).
All of the above is working fine the first time the user logs in and navigates through the application. Also works when the user manually tries to enter the route in the browser itself.
The problem occurs when the user tries to visit a certain page (for example "/class-coverage-plans/new") as an "ADMIN" and the app lets the user do it, but after that the current user logs out and, quickly afterwards, a different user with the role "USER" logs in and tries to visit the same route as the admin did (the "USER" is not supposed to be allowed to visit the said route) and the app lets the user visit it.
What solves my problem is:
- pressing the refresh button in the browser after a different user logs in
- navigating with
window.location.href = "/"; after a successful login instead of router.push("/");Here is the main part of my middleware function:
if (isAuthenticated(token)) {
if (pathname === "/login") {
return NextResponse.redirect(new URL("/", request.url));
} else if (
role === "USER" &&
isRouteRestrictedForUser(request, restrictedRoutesForUser)
) {
return NextResponse.redirect(new URL("/", request.url));
} else {
return NextResponse.next();
}
} else {
if (pathname === "/login") {
return NextResponse.next();
} else {
return NextResponse.redirect(new URL("/login", request.url));
}
}Sorry for the long message and thanks!
4 Replies
akaOP
Please tell me if additional clarification is needed as well as more code snippets since I couldn't include more because the message was already lengthy
Southeastern blueberry bee
can you share all the middleware please
akaOP
import { NextRequest, NextResponse } from "next/server";
import { extractUserRole, isAuthenticated } from "./app/utility/auth";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token")?.value;
const { pathname } = request.nextUrl;
const role = extractUserRole(token);
const restrictedRoutesForUser = ["/class-coverage-plans/new"];
if (isAuthenticated(token)) {
if (pathname === "/login") {
return NextResponse.redirect(new URL("/", request.url));
} else if (
role === "USER" &&
isRouteRestrictedForUser(request, restrictedRoutesForUser)
) {
return NextResponse.redirect(new URL("/", request.url));
} else {
return NextResponse.next();
}
} else {
if (pathname === "/login") {
return NextResponse.next();
} else {
return NextResponse.redirect(new URL("/login", request.url));
}
}
}
function isRouteRestrictedForUser(
request: NextRequest,
restrictedRoutes: string[]
) {
return (
restrictedRoutes.includes(request.nextUrl.pathname) ||
isDynamicClassCoveragePlanRoute(request)
);
}
function isDynamicClassCoveragePlanRoute(request: NextRequest): boolean {
const dynamicRoutePattern = /^\/class-coverage-plans\/\d+$/;
return dynamicRoutePattern.test(request.nextUrl.pathname);
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico).*)", // Exclude static files and assets and the /login route
],
};This is all the middleware
import { jwtDecode } from "jwt-decode";
import CustomJwtPayload from "../types/CustomJwtPayload";
export const decodeToken = (token: string | undefined) => {
try {
return token ? jwtDecode<CustomJwtPayload>(token) : null;
} catch (error) {
console.error("Invalid token:", error);
return null;
}
};
export const isTokenExpired = (token: string | undefined) => {
const decodedToken = decodeToken(token);
if (!decodedToken || !decodedToken.exp) {
return true;
}
const currentTime = Date.now() / 1000; // Convert to seconds
return decodedToken.exp < currentTime;
};
export const isAuthenticated = (token: string | undefined) => {
return !!(token && !isTokenExpired(token)); //!! converts to a boolean value but preserves the 'truthiness'
};
export const extractUserRole = (token: string | undefined) => {
const decodedToken = decodeToken(token);
if (decodedToken) {
const role = decodedToken.role;
return role ? role : null;
}
return null;
};Here are also the functions that are imported and used in the middleware