Check DB or update session when DB role changes

I need to block access to certain routes based on the user's role and companies associated with they. The problem I'm having is that if a user is already connected with a session, when their role is updated on the DB the session does not update, so it keeps the same role and companies, keeping the same permission as before.
Solution:
This is my initial implemetation, works great: ``` import { signIn, useSession } from "next-auth/react"; import { useRouter } from "next/router"; import { useEffect } from "react";...
Jump to solution
4 Replies
felipe.franco
felipe.franco11mo ago
Currently I have this middleware.ts that checks the user role and the companies that the user has access to. The roles are: - USER : Can access only the /dashboard/[companyId] route if the companyId is present in the token.companiesIds; - ADMIN: Currently the same access as the USER role (other routes will be implemented in the future); - SUPERADMIN: Can access all /dashboard/[companyId] independent if the companyId is present in their token.companiesIds. All requests that try to access "/" or a /dashboard/[companyId] are redirected to /dashboard/[companyId] using the first companyId in token.companiesIds.
// middleware.ts
import { type NextRequestWithAuth, withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";

export default withAuth(
function middleware(request: NextRequestWithAuth) {
console.log("middleware", request.nextUrl.pathname);

const token = request.nextauth.token;
const pathname = request.nextUrl.pathname;
const companyId = pathname.split("/")[2] ?? "";

// Check if the user is authenticated
if (!token) {
console.log("redirecting to auth", request.nextUrl.pathname);
return NextResponse.rewrite(new URL("/auth/signin", request.url));
}

const defaultCompanyId = token.companiesId[0];
const isCompanyInToken = token.companiesId.includes(companyId);

// Object to define redirection rules for each role
const roleRedirectRules = {
USER: !isCompanyInToken,
ADMIN: !isCompanyInToken && !companyId,
SUPERADMIN: !companyId,
};

const isDashboardWithoutCompanyId =
!companyId && pathname.startsWith("/dashboard");
const shouldRedirect =
roleRedirectRules[token.role] || isDashboardWithoutCompanyId;

// Redirects based on the role's rules
if (shouldRedirect) {
console.log("rewrite to default company id", defaultCompanyId);
return NextResponse.redirect(
new URL(`/dashboard/${defaultCompanyId}`, request.url)
);
}
},
{
pages: {
signIn: "/auth/signin",
},
}
);

export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico).*)",
"/",
"/dashboard/:path*",
],
};
// middleware.ts
import { type NextRequestWithAuth, withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";

export default withAuth(
function middleware(request: NextRequestWithAuth) {
console.log("middleware", request.nextUrl.pathname);

const token = request.nextauth.token;
const pathname = request.nextUrl.pathname;
const companyId = pathname.split("/")[2] ?? "";

// Check if the user is authenticated
if (!token) {
console.log("redirecting to auth", request.nextUrl.pathname);
return NextResponse.rewrite(new URL("/auth/signin", request.url));
}

const defaultCompanyId = token.companiesId[0];
const isCompanyInToken = token.companiesId.includes(companyId);

// Object to define redirection rules for each role
const roleRedirectRules = {
USER: !isCompanyInToken,
ADMIN: !isCompanyInToken && !companyId,
SUPERADMIN: !companyId,
};

const isDashboardWithoutCompanyId =
!companyId && pathname.startsWith("/dashboard");
const shouldRedirect =
roleRedirectRules[token.role] || isDashboardWithoutCompanyId;

// Redirects based on the role's rules
if (shouldRedirect) {
console.log("rewrite to default company id", defaultCompanyId);
return NextResponse.redirect(
new URL(`/dashboard/${defaultCompanyId}`, request.url)
);
}
},
{
pages: {
signIn: "/auth/signin",
},
}
);

export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico).*)",
"/",
"/dashboard/:path*",
],
};
// ./server/auth.ts

//... imports

/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
* object and keep type safety.
*
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
*/
declare module "next-auth" {
interface User {
id: string;
role: Role;
companiesId: string[];
}

interface Session {
user?: User;
}
}

/**
* Module augmentation for `next-auth/jwt` types. Allows us to add custom properties to the `jwt`
* object and keep type safety.
*
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
*/
declare module "next-auth/jwt" {
interface JWT {
id: string;
role: Role;
companiesId: string[];
}
}
// ./server/auth.ts

//... imports

/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
* object and keep type safety.
*
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
*/
declare module "next-auth" {
interface User {
id: string;
role: Role;
companiesId: string[];
}

interface Session {
user?: User;
}
}

/**
* Module augmentation for `next-auth/jwt` types. Allows us to add custom properties to the `jwt`
* object and keep type safety.
*
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
*/
declare module "next-auth/jwt" {
interface JWT {
id: string;
role: Role;
companiesId: string[];
}
}
/**
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
*
* @see https://next-auth.js.org/configuration/options
*/
export const authOptions: NextAuthOptions = {
session: { strategy: "jwt" },

callbacks: {
async jwt({ token, user }) {
const companiesId = await prisma.company
.findMany({
where: {
users: {
some: {
id: user?.id,
},
},
},
select: {
slug: true,
},
})
.then((companies) => companies.map((company) => company.slug));

if (user) token.role = user.role;
if (companiesId) token.companiesId = companiesId;

return token;
},

session({ session, token }) {
const tokenUser = z.object({
name: z.string(),
email: z.string(),
picture: z.string().nullable(),
sub: z.string(),
role: z.nativeEnum(Role),
companiesId: z.array(z.string()),
iat: z.number(),
exp: z.number(),
jti: z.string(),
});

const parsedToken = tokenUser.safeParse(token);

if (parsedToken.success) {
session.user = {
id: parsedToken.data.sub,
name: parsedToken.data.name,
email: parsedToken.data.email,
image: parsedToken.data.picture,
role: parsedToken.data.role,
companiesId: parsedToken.data.companiesId,
};
}

return session;
},

async signIn({ user }) {
if (user?.email === null || user.email === undefined) return false;

const dbUser = await prisma.user.findUnique({
where: {
email: user.email,
},
});

return dbUser !== null;
},

redirect({ baseUrl, url }) {
console.log("redirect", baseUrl, url);
return url;
},
},

//... provider configs and rest
/**
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
*
* @see https://next-auth.js.org/configuration/options
*/
export const authOptions: NextAuthOptions = {
session: { strategy: "jwt" },

callbacks: {
async jwt({ token, user }) {
const companiesId = await prisma.company
.findMany({
where: {
users: {
some: {
id: user?.id,
},
},
},
select: {
slug: true,
},
})
.then((companies) => companies.map((company) => company.slug));

if (user) token.role = user.role;
if (companiesId) token.companiesId = companiesId;

return token;
},

session({ session, token }) {
const tokenUser = z.object({
name: z.string(),
email: z.string(),
picture: z.string().nullable(),
sub: z.string(),
role: z.nativeEnum(Role),
companiesId: z.array(z.string()),
iat: z.number(),
exp: z.number(),
jti: z.string(),
});

const parsedToken = tokenUser.safeParse(token);

if (parsedToken.success) {
session.user = {
id: parsedToken.data.sub,
name: parsedToken.data.name,
email: parsedToken.data.email,
image: parsedToken.data.picture,
role: parsedToken.data.role,
companiesId: parsedToken.data.companiesId,
};
}

return session;
},

async signIn({ user }) {
if (user?.email === null || user.email === undefined) return false;

const dbUser = await prisma.user.findUnique({
where: {
email: user.email,
},
});

return dbUser !== null;
},

redirect({ baseUrl, url }) {
console.log("redirect", baseUrl, url);
return url;
},
},

//... provider configs and rest
Is there a way to do this without using getServerSideProps on every route? (using /pages directory) I thought about doing a wrapper component with the getServerSideProps check, and than wrapping the whole app with it, is there a cleaner way to do it?
Neto
Neto11mo ago
if you are using jwts you can't change the token after was created your options are: use db sessions
felipe.franco
felipe.franco11mo ago
YES! thank you, that is exactly what I needed, I was using JWT to protect the routes using middleware.ts, but that was the wrong choice. Found a great article about protecting routes using a "AuthGuard" component: https://dev.to/ivandotv/protecting-static-pages-in-next-js-application-1e50
DEV Community
Protecting static pages in Next.js application
In this article, I will explain how to structure your Next.js application so you can protect your sta...
Solution
felipe.franco
felipe.franco11mo ago
This is my initial implemetation, works great:
import { signIn, useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";

export function AuthGuard({ children }: { children: JSX.Element }) {
const { status, data: session } = useSession();
const router = useRouter();

const adminRoutes = ["/admin"];
const isAdminRoute = adminRoutes.some((route) =>
router.pathname.startsWith(route)
);
const isRootRoute = router.pathname === "/";
const isDashboardRouteAndIdNotSet =
router.pathname.startsWith("/dashboard") && !router.query.companySlug;

useEffect(() => {
if (status !== "loading") {
//auth is initialized and there is no user
if (!session) {
void signIn();
}
}
}, [router, status, session]);

/* show loading indicator while the auth provider is still initializing */
if (status === "loading") {
return (
<div className="flex min-h-screen items-center justify-center">
<h1 className="text-5xl">Securing route</h1>
</div>
);
}

// if auth initialized with a valid user show protected page
if (session) {
// if user tries to access root route or /dashboard without a slug and is logged in, redirect to dashboard/[companySlug]
if (isRootRoute || isDashboardRouteAndIdNotSet) {
console.log("redirecting to dashboard");
const companyName = session.user?.companies[0]?.name;
void router.push(`/dashboard/${companyName}`);
return null;
}

const isSuperAdmin = session.user?.role === "SUPERADMIN";

// if user is not admin and tries to access admin routes, redirect to home
if (!isSuperAdmin && isAdminRoute) {
void router.push("/");
return null;
}

return <>{children}</>;
}

/* otherwise don't return anything, will do a redirect from useEffect */
return null;
}
import { signIn, useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";

export function AuthGuard({ children }: { children: JSX.Element }) {
const { status, data: session } = useSession();
const router = useRouter();

const adminRoutes = ["/admin"];
const isAdminRoute = adminRoutes.some((route) =>
router.pathname.startsWith(route)
);
const isRootRoute = router.pathname === "/";
const isDashboardRouteAndIdNotSet =
router.pathname.startsWith("/dashboard") && !router.query.companySlug;

useEffect(() => {
if (status !== "loading") {
//auth is initialized and there is no user
if (!session) {
void signIn();
}
}
}, [router, status, session]);

/* show loading indicator while the auth provider is still initializing */
if (status === "loading") {
return (
<div className="flex min-h-screen items-center justify-center">
<h1 className="text-5xl">Securing route</h1>
</div>
);
}

// if auth initialized with a valid user show protected page
if (session) {
// if user tries to access root route or /dashboard without a slug and is logged in, redirect to dashboard/[companySlug]
if (isRootRoute || isDashboardRouteAndIdNotSet) {
console.log("redirecting to dashboard");
const companyName = session.user?.companies[0]?.name;
void router.push(`/dashboard/${companyName}`);
return null;
}

const isSuperAdmin = session.user?.role === "SUPERADMIN";

// if user is not admin and tries to access admin routes, redirect to home
if (!isSuperAdmin && isAdminRoute) {
void router.push("/");
return null;
}

return <>{children}</>;
}

/* otherwise don't return anything, will do a redirect from useEffect */
return null;
}