Role Based Access Control

A SaaS application typically implements a role-based access control system to manage user permissions and restrict access to specific features or content.

For instance, free-tier users may only have access to basic functionalities and limited content, while paid users can unlock premium features and exclusive resources.

Additionally, administrators are granted elevated privileges, enabling them to oversee and manage the entire system, including user accounts, content, and application settings.

This tiered, role-based access structure ensures a secure and organized user experience.

Extending the User table

In order to implement such system using Auth.js, we must expand the default User table to include information about the user's role.

Go to prisma/schema.prisma and add a role column like this:

prisma
1model User {
2  id            String          @id @default(cuid())
3  name          String?
4  email         String?         @unique
5  emailVerified DateTime?
6  image         String?
7  role          String?         @default("free user")  // Add a new "role" column
8  accounts      Account[]
9  sessions      Session[]
10
11  createdAt DateTime @default(now())
12  updatedAt DateTime @updatedAt
13}

Apply the new schema by running the following command:

bash
1npx prisma migrate dev

You should see a new role column inside the User table. For now, just for demonstration purposes, you can manually change its value:

Edit User Role

You may need to restart the dev server in order for the changes to take effect.

Extending the session

Previously, we discussed how to protect certain routes by checking for sessions.

In order to implement a role-based access control system, you must also extend the default session to mirror the User table.

Go to the auth.js and add a session() callback:

libs/auth.js

javascript
1. . .
2
3export const { handlers, signIn, signOut, auth } = NextAuth({
4  adapter: PrismaAdapter(prisma),
5  providers: [. . .],
6  pages: {. . .},
7  session: {. . .},
8  callbacks: {
9    session({ session, user }) {
10      session.user.role = user.role;
11      return session;
12    },
13  },
14});

The session() callback will be executed every time the session is checked, by executing auth(). And our example makes sure that the returned session contains a session.user.role key that mirrors user.role from the database.

Implementing the access control logic

And finally, go to your page.jsx and check for the session?.user?.role instead of just the session:

page.jsx

jsx
1import { auth } from "@/auth";
2
3export default async function Page() {
4  const session = await auth();
5
6  if (session?.user?.role === "admin") {
7    return <p>You are an admin, welcome!</p>;
8  }
9
10  return <p>You are not authorized to view this page!</p>;
11}

The ?. is optional chaining operator, which provides a safer way (compared to .) to access a property deeply nested within an object.

If any of the chained properties doe not exist, it returns undefined instead of throwing an error.

Updating the user settings page

jsx
1import { auth } from "@/libs/auth";
2import prisma from "@/libs/db";
3import { redirect } from "next/navigation";
4
5export default async function SettingsPage() {
6  const session = await auth();
7
8  return (
9    <div className="flex min-h-[80vh] items-center justify-center bg-gray-100">
10      <div className="w-full max-w-md bg-white p-8 rounded-lg shadow-md">
11        <h1 className="text-2xl font-bold text-center mb-6">User Settings</h1>
12
13        <form
14          action={. . .}
15          className="space-y-6">
16          <div>
17          . . .
18          <div>
19            <label
20              htmlFor="role"
21              className="block text-sm font-medium text-gray-700">
22              Role
23            </label>
24            <input
25              type="text"
26              id="role"
27              name="role"
28              defaultValue={session?.user?.role}
29              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 cursor-not-allowed"
30              readOnly
31            />
32          </div>
33
34          <div>
35            <button
36              type="submit"
37              className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
38              Update Profile
39            </button>
40          </div>
41        </form>
42      </div>
43    </div>
44  );
45}

Dashboard with user role