We use cookies to enhance your experience on the site
CodeWorlds

User roles and permissions

In previous modules, we learned the basics of authentication, connecting external providers, and managing sessions and tokens. Now we will deal with another key element of application security - the system of user roles and permissions, i.e. authorization.

While authentication answers the question "Who are you?", authorization answers the question "What can you do?". A solid system of roles and permissions is essential in any application that requires different levels of access for different users.

Basic concepts of roles and permissions

Before we move on to the implementation, let's understand the basic concepts:

  1. Roles - categories of users with similar access levels (e.g. user, moderator, administrator)
  2. Permissions - specific actions a user can perform (e.g. creating posts, editing profiles, deleting accounts)
  3. RBAC (Role-Based Access Control) - access control based on roles, where permissions are assigned to roles, and roles to users
  4. ABAC (Attribute-Based Access Control) - a more advanced model where access decisions are made based on various attributes (role, time, location, etc.)

Data model for roles and permissions

Let's start by defining an appropriate data model in Prisma:

1// prisma/schema.prisma
2model User {
3  id            String    @id @default(cuid())
4  name          String?
5  email         String?   @unique
6  emailVerified DateTime?
7  image         String?
8  password      String?
9  accounts      Account[]
10  sessions      Session[]
11  role          Roles      @relation(fields: [roleId], references: [id])
12  roleId        String
13  permissions   UserPermission[]
14  createdAt     DateTime  @default(now())
15  updatedAt     DateTime  @updatedAt
16}
17
18model Roles {
19  id          String        @id @default(cuid())
20  name        String        @unique
21  description String?
22  users       User[]
23  permissions RolesPermission[]
24  createdAt   DateTime      @default(now())
25  updatedAt   DateTime      @updatedAt
26}
27
28model Permission {
29  id          String           @id @default(cuid())
30  name        String           @unique
31  description String?
32  roles       RolesPermission[]
33  users       UserPermission[]
34  createdAt   DateTime         @default(now())
35  updatedAt   DateTime         @updatedAt
36}
37
38model RolesPermission {
39  id           String     @id @default(cuid())
40  role         Roles       @relation(fields: [roleId], references: [id], onDelete: Cascade)
41  roleId       String
42  permission   Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
43  permissionId String
44  createdAt    DateTime   @default(now())
45  updatedAt    DateTime   @updatedAt
46
47  @@unique([roleId, permissionId])
48}
49
50model UserPermission {
51  id           String     @id @default(cuid())
52  user         User       @relation(fields: [userId], references: [id], onDelete: Cascade)
53  userId       String
54  permission   Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
55  permissionId String
56  granted      Boolean    @default(true) // true = granted, false = revoked
57  createdAt    DateTime   @default(now())
58  updatedAt    DateTime   @updatedAt
59
60  @@unique([userId, permissionId])
61}

This model supports:

  1. Hierarchical roles - users are assigned roles
  2. Role-based permissions - roles have sets of permissions
  3. Individual permissions - specific users can be granted or revoked specific permissions, regardless of their role

Initialization of basic roles and permissions

Let's create a script that initializes basic roles and permissions in our application:

1// prisma/seed.ts
2import { PrismaClient } from '@prisma/client';
3
4const prisma = new PrismaClient();
5
6async function main() {
7  // Creating basic roles
8  const roles = [
9    {
10      name: 'admin',
11      description: 'Administrator with full system access'
12    },
13    {
14      name: 'moderator',
15      description: 'Moderator with content management access'
16    },
17    {
18      name: 'user',
19      description: 'Standard user with basic permissions'
20    },
21    {
22      name: 'guest',
23      description: 'Guest with minimal read-only access'
24    }
25  ];
26
27  for (const role of roles) {
28    await prisma.role.upsert({
29      where: { name: role.name },
30      update: {},
31      create: role,
32    });
33  }
34
35  // Creating basic permissions
36  const permissions = [
37    // User permissions
38    { name: 'user:read', description: 'Can view user profiles' },
39    { name: 'user:create', description: 'Can create new users' },
40    { name: 'user:update', description: 'Can update user data' },
41    { name: 'user:delete', description: 'Can delete users' },
42    
43    // Content permissions
44    { name: 'content:read', description: 'Can read content' },
45    { name: 'content:create', description: 'Can create new content' },
46    { name: 'content:update', description: 'Can update content' },
47    { name: 'content:delete', description: 'Can delete content' },
48    { name: 'content:publish', description: 'Can publish content' },
49    
50    // Administrative permissions
51    { name: 'admin:access', description: 'Has access to the admin panel' },
52    { name: 'admin:settings', description: 'Can change system settings' },
53    { name: 'admin:roles', description: 'Can manage roles and permissions' },
54  ];
55
56  for (const permission of permissions) {
57    await prisma.permission.upsert({
58      where: { name: permission.name },
59      update: {},
60      create: permission,
61    });
62  }
63
64  // Assigning permissions to roles
65  const rolePermissions = [
66    // Administrator permissions
67    { roleName: 'admin', permissionNames: permissions.map(p => p.name) },
68    
69    // Moderator permissions
70    { 
71      roleName: 'moderator', 
72      permissionNames: [
73        'user:read',
74        'content:read', 'content:create', 'content:update', 'content:delete', 'content:publish',
75        'admin:access',
76      ]
77    },
78    
79    // Standard user permissions
80    {
81      roleName: 'user',
82      permissionNames: [
83        'user:read',
84        'content:read', 'content:create', 'content:update',
85      ]
86    },
87    
88    // Guest permissions
89    {
90      roleName: 'guest',
91      permissionNames: [
92        'user:read',
93        'content:read',
94      ]
95    },
96  ];
97
98  for (const { roleName, permissionNames } of rolePermissions) {
99    const role = await prisma.role.findUnique({ where: { name: roleName } });
100    
101    if (role) {
102      for (const permName of permissionNames) {
103        const permission = await prisma.permission.findUnique({ 
104          where: { name: permName } 
105        });
106        
107        if (permission) {
108          await prisma.rolePermission.upsert({
109            where: {
110              roleId_permissionId: {
111                roleId: role.id,
112                permissionId: permission.id
113              }
114            },
115            update: {},
116            create: {
117              roleId: role.id,
118              permissionId: permission.id
119            }
120          });
121        }
122      }
123    }
124  }
125
126  // Create an administrative user (if not exists)
127  const adminRoles = await prisma.role.findUnique({ where: { name: 'admin' } });
128  if (adminRoles) {
129    const adminEmail = 'admin@example.com';
130    const existingAdmin = await prisma.user.findUnique({
131      where: { email: adminEmail }
132    });
133
134    if (!existingAdmin) {
135      await prisma.user.create({
136        data: {
137          name: 'Administrator',
138          email: adminEmail,
139          // In production, the password should be hashed!
140          password: '$2a$10$iqJSHD.BGr0E2IxQwYgJmeP3NvhPrXAeLSaGCj6IR/XU5QtjVu5Tm', // "secret"
141          roleId: adminRoles.id,
142        }
143      });
144      console.log('Administrative user created');
145    }
146  }
147
148  console.log('Roles and permissions initialization completed successfully');
149}
150
151main()
152  .catch((e) => {
153    console.error(e);
154    process.exit(1);
155  })
156  .finally(async () => {
157    await prisma.$disconnect();
158  });

To run this script, add the following configuration to

package.json
:

1"prisma": {
2  "seed": "ts-node prisma/seed.ts"
3},
4"scripts": {
5  "db:seed": "prisma db seed"
6}

Extending the NextAuth session type

To have access to user role information in the NextAuth session, let's extend the types:

1// types/next-auth.d.ts
2import { DefaultSession } from 'next-auth';
3
4declare module 'next-auth' {
5  interface Session {
6    user: {
7      id: string;
8      role: string;
9      permissions: string[];
10    } & DefaultSession['user'];
11  }
12
13  interface User {
14    role: string;
15    roleId: string;
16    permissions: string[];
17  }
18}
19
20declare module 'next-auth/jwt' {
21  interface JWT {
22    userId: string;
23    role: string;
24    permissions: string[];
25  }
26}

Implementing the NextAuth configuration

Now let's modify the NextAuth configuration to include roles and permissions:

1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from 'next-auth';
3import { authOptions } from '@/lib/auth';
4
5const handler = NextAuth(authOptions);
6export { handler as GET, handler as POST };
7
8// lib/auth.ts
9import { NextAuthOptions } from 'next-auth';
10import { PrismaAdapter } from '@auth/prisma-adapter';
11import { prisma } from '@/lib/prisma';
12import CredentialsProvider from 'next-auth/providers/credentials';
13import GoogleProvider from 'next-auth/providers/google';
14import { compare } from 'bcrypt';
15
16export const authOptions: NextAuthOptions = {
17  adapter: PrismaAdapter(prisma),
18  providers: [
19    GoogleProvider({
20      clientId: process.env.GOOGLE_CLIENT_ID!,
21      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
22    }),
23    CredentialsProvider({
24      name: 'Credentials',
25      credentials: {
26        email: { label: "Email", type: "email" },
27        password: { label: "Password", type: "password" }
28      },
29      async authorize(credentials) {
30        if (!credentials?.email || !credentials?.password) {
31          return null;
32        }
33
34        const user = await prisma.user.findUnique({
35          where: { email: credentials.email },
36          include: {
37            role: true,
38            permissions: {
39              include: {
40                permission: true,
41              },
42            },
43          },
44        });
45
46        if (!user || !user.password) {
47          return null;
48        }
49
50        const isPasswordValid = await compare(credentials.password, user.password);
51
52        if (!isPasswordValid) {
53          return null;
54        }
55
56        // Fetch the role permissions
57        const rolePermissions = await prisma.rolePermission.findMany({
58          where: { roleId: user.roleId },
59          include: { permission: true },
60        });
61
62        // Create a list of permissions (role permissions + individual permissions)
63        const allPermissions = new Set<string>();
64        
65        // Add the role permissions
66        rolePermissions.forEach(rp => {
67          allPermissions.add(rp.permission.name);
68        });
69        
70        // Add/remove individual user permissions
71        user.permissions.forEach(up => {
72          if (up.granted) {
73            allPermissions.add(up.permission.name);
74          } else {
75            allPermissions.delete(up.permission.name);
76          }
77        });
78
79        return {
80          id: user.id,
81          email: user.email,
82          name: user.name,
83          image: user.image,
84          role: user.role.name,
85          roleId: user.roleId,
86          permissions: Array.from(allPermissions),
87        };
88      },
89    }),
90  ],
91  callbacks: {
92    async jwt({ token, user }) {
93      if (user) {
94        token.userId = user.id;
95        token.role = user.role;
96        token.permissions = user.permissions;
97      }
98      return token;
99    },
100    async session({ session, token }) {
101      if (session.user) {
102        session.user.id = token.userId;
103        session.user.role = token.role;
104        session.user.permissions = token.permissions;
105      }
106      return session;
107    },
108  },
109  pages: {
110    signIn: '/auth/login',
111    error: '/auth/error',
112  },
113  session: {
114    strategy: 'jwt',
115  },
116};

Authorization utilities

Now let's create utilities for easily checking user roles and permissions:

1// lib/auth-utils.ts
2import { getServerSession } from 'next-auth/next';
3import { authOptions } from '@/lib/auth';
4import { redirect } from 'next/navigation';
5import { Session } from 'next-auth';
6
7// Helper functions for checking roles and permissions
8export function hasRoles(session: Session | null, role: string | string[]): boolean {
9  if (!session?.user?.role) return false;
10  
11  if (Array.isArray(role)) {
12    return role.includes(session.user.role);
13  }
14  
15  return session.user.role === role;
16}
17
18export function hasPermission(session: Session | null, permission: string | string[]): boolean {
19  if (!session?.user?.permissions) return false;
20  
21  if (Array.isArray(permission)) {
22    return permission.some(p => session.user.permissions.includes(p));
23  }
24  
25  return session.user.permissions.includes(permission);
26}
27
28// Server function - checks role and redirects if no access
29export async function requireRoles(role: string | string[], redirectTo: string = '/auth/unauthorized') {
30  const session = await getServerSession(authOptions);
31  
32  if (!session) {
33    redirect('/auth/login');
34  }
35  
36  if (!hasRoles(session, role)) {
37    redirect(redirectTo);
38  }
39  
40  return session;
41}
42
43// Server function - checks permission and redirects if no access
44export async function requirePermission(permission: string | string[], redirectTo: string = '/auth/unauthorized') {
45  const session = await getServerSession(authOptions);
46  
47  if (!session) {
48    redirect('/auth/login');
49  }
50  
51  if (!hasPermission(session, permission)) {
52    redirect(redirectTo);
53  }
54  
55  return session;
56}

Implementation of access protection components

Now let's create client components that will allow easy access control to parts of the user interface:

1// components/RolesGuard.tsx
2'use client';
3
4import { useSession } from 'next-auth/react';
5import { ReactNode } from 'react';
6import { hasRoles } from '@/lib/auth-utils';
7
8interface RolesGuardProps {
9  children: ReactNode;
10  role: string | string[];
11  fallback?: ReactNode;
12}
13
14export function RolesGuard({ children, role, fallback = null }: RolesGuardProps) {
15  const { data: session } = useSession();
16  
17  if (!hasRoles(session, role)) {
18    return fallback;
19  }
20  
21  return <>{children}</>;
22}
23
24// components/PermissionGuard.tsx
25'use client';
26
27import { useSession } from 'next-auth/react';
28import { ReactNode } from 'react';
29import { hasPermission } from '@/lib/auth-utils';
30
31interface PermissionGuardProps {
32  children: ReactNode;
33  permission: string | string[];
34  fallback?: ReactNode;
35}
36
37export function PermissionGuard({ children, permission, fallback = null }: PermissionGuardProps) {
38  const { data: session } = useSession();
39  
40  if (!hasPermission(session, permission)) {
41    return fallback;
42  }
43  
44  return <>{children}</>;
45}
46
47// components/AdminOnly.tsx
48'use client';
49
50import { RolesGuard } from '@/components/RolesGuard';
51import { ReactNode } from 'react';
52
53export function AdminOnly({ children, fallback = null }: { children: ReactNode, fallback?: ReactNode }) {
54  return (
55    <RolesGuard role="admin" fallback={fallback}>
56      {children}
57    </RolesGuard>
58  );
59}

Implementing middleware

We can also implement middleware for access control to entire paths:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6export async function middleware(request: NextRequest) {
7  const token = await getToken({ req: request });
8  
9  // Administrative paths
10  if (request.nextUrl.pathname.startsWith('/admin')) {
11    if (!token) {
12      return NextResponse.redirect(new URL('/auth/login?callbackUrl=/admin', request.url));
13    }
14    
15    // Check if the user has the administrator role
16    if (token.role !== 'admin') {
17      return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
18    }
19  }
20  
21  // Moderator paths
22  if (request.nextUrl.pathname.startsWith('/moderator')) {
23    if (!token) {
24      return NextResponse.redirect(new URL('/auth/login?callbackUrl=/moderator', request.url));
25    }
26    
27    // Check if the user has the administrator or moderator role
28    if (token.role !== 'admin' && token.role !== 'moderator') {
29      return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
30    }
31  }
32  
33  // Protected paths - accessible only to logged-in users
34  if (
35    request.nextUrl.pathname.startsWith('/dashboard') ||
36    request.nextUrl.pathname.startsWith('/profile') ||
37    request.nextUrl.pathname.startsWith('/settings')
38  ) {
39    if (!token) {
40      const callbackUrl = encodeURIComponent(request.nextUrl.pathname);
41      return NextResponse.redirect(new URL(`/auth/login?callbackUrl=${callbackUrl}`, request.url));
42    }
43  }
44  
45  // Securing API endpoints
46  if (request.nextUrl.pathname.startsWith('/api/protected')) {
47    if (!token) {
48      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
49    }
50    
51    // Additional permission checks can be added depending on the API path
52    if (request.nextUrl.pathname.startsWith('/api/protected/admin') && token.role !== 'admin') {
53      return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
54    }
55  }
56  
57  return NextResponse.next();
58}
59
60export const config = {
61  matcher: [
62    '/admin/:path*',
63    '/moderator/:path*',
64    '/dashboard/:path*',
65    '/profile/:path*', 
66    '/settings/:path*',
67    '/api/protected/:path*',
68  ],
69};

Examples of use in the application

1. Route protection using server functions

1// app/admin/dashboard/page.tsx
2import { requireRoles } from '@/lib/auth-utils';
3
4export default async function AdminDashboardPage() {
5  // Redirects if the user is not an administrator
6  const session = await requireRoles('admin');
7  
8  return (
9    <div className="p-6">
10      <h1 className="text-2xl font-bold mb-4">Administration panel</h1>
11      <p>Hello, {session.user.name}! You have full administrative access.</p>
12      {/* Content available only to administrators */}
13    </div>
14  );
15}
16
17// app/content/edit/[id]/page.tsx
18import { requirePermission } from '@/lib/auth-utils';
19
20export default async function EditContentPage({ params }: { params: Promise<{ id: string }> }) {
21  // Redirects if the user does not have permission to edit content
22  const session = await requirePermission('content:update');
23  
24  // Fetch content for editing
25  const content = await fetchContent((await params).id);
26  
27  return (
28    <div className="p-6">
29      <h1 className="text-2xl font-bold mb-4">Content editing</h1>
30      {/* Edit form */}
31    </div>
32  );
33}

2. UI component protection

1// components/NavBar.tsx
2'use client';
3
4import { useSession } from 'next-auth/react';
5import Link from 'next/link';
6import { AdminOnly } from '@/components/AdminOnly';
7import { RolesGuard } from '@/components/RolesGuard';
8import { PermissionGuard } from '@/components/PermissionGuard';
9
10export function NavBar() {
11  const { data: session } = useSession();
12  
13  return (
14    <nav className="bg-white shadow-sm">
15      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
16        <div className="flex justify-between h-16">
17          <div className="flex space-x-8 items-center">
18            <Link href="/" className="font-bold text-xl">MyApplication</Link>
19            <Link href="/dashboard">Dashboard</Link>
20            
21            {/* Link visible only to moderators and administrators */}
22            <RolesGuard role={['admin', 'moderator']}>
23              <Link href="/moderator/content" className="text-blue-600">
24                Content management
25              </Link>
26            </RolesGuard>
27            
28            {/* Link only for administrators */}
29            <AdminOnly>
30              <Link href="/admin/dashboard" className="text-red-600">
31                Administrator panel
32              </Link>
33            </AdminOnly>
34            
35            {/* Link for users with content creation permission */}
36            <PermissionGuard permission="content:create">
37              <Link href="/content/new" className="text-green-600">
38                Create content
39              </Link>
40            </PermissionGuard>
41          </div>
42          
43          <div className="flex items-center">
44            {session ? (
45              <div className="flex items-center space-x-4">
46                <span>{session.user.name}</span>
47                <span className="px-2 py-1 bg-gray-200 rounded-full text-xs">
48                  {session.user.role}
49                </span>
50              </div>
51            ) : (
52              <Link href="/auth/login">Log in</Link>
53            )}
54          </div>
55        </div>
56      </div>
57    </nav>
58  );
59}

3. Implementing Route Handlers with authorization

1// app/api/protected/users/route.ts
2import { NextResponse } from 'next/server';
3import { getServerSession } from 'next-auth/next';
4import { authOptions } from '@/lib/auth';
5import { hasPermission } from '@/lib/auth-utils';
6import { prisma } from '@/lib/prisma';
7
8export async function GET() {
9  const session = await getServerSession(authOptions);
10  
11  if (!session) {
12    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
13  }
14  
15  // Check if the user has permission to read users
16  if (!hasPermission(session, 'user:read')) {
17    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
18  }
19  
20  try {
21    const users = await prisma.user.findMany({
22      select: {
23        id: true,
24        name: true,
25        email: true,
26        image: true,
27        role: {
28          select: {
29            name: true,
30          },
31        },
32        createdAt: true,
33      }
34    });
35    
36    return NextResponse.json(users);
37  } catch (error) {
38    console.error('Error fetching users:', error);
39    return NextResponse.json(
40      { error: 'Failed to fetch users' },
41      { status: 500 }
42    );
43  }
44}
45
46// app/api/protected/users/[id]/route.ts
47import { NextResponse } from 'next/server';
48import { getServerSession } from 'next-auth/next';
49import { authOptions } from '@/lib/auth';
50import { hasPermission } from '@/lib/auth-utils';
51import { prisma } from '@/lib/prisma';
52
53export async function DELETE(
54  request: Request,
55  { params }: { params: Promise<{ id: string }> }
56) {
57  const session = await getServerSession(authOptions);
58  
59  if (!session) {
60    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
61  }
62  
63  // Check if the user has permission to delete users
64  if (!hasPermission(session, 'user:delete')) {
65    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
66  }
67  
68  try {
69    // Check that the user is not trying to delete themselves
70    if ((await params).id === session.user.id) {
71      return NextResponse.json(
72        { error: 'Cannot delete your own account' },
73        { status: 400 }
74      );
75    }
76    
77    // Check that the user is not trying to delete an administrator (if they themselves are not an administrator)
78    if (session.user.role !== 'admin') {
79      const userToDelete = await prisma.user.findUnique({
80        where: { id: (await params).id },
81        include: { role: true },
82      });
83      
84      if (userToDelete?.role.name === 'admin') {
85        return NextResponse.json(
86          { error: 'Cannot delete admin accounts' },
87          { status: 403 }
88        );
89      }
90    }
91    
92    // Delete user
93    await prisma.user.delete({
94      where: { id: (await params).id },
95    });
96    
97    return NextResponse.json({ success: true });
98  } catch (error) {
99    console.error('Error deleting user:', error);
100    return NextResponse.json(
101      { error: 'Failed to delete user' },
102      { status: 500 }
103    );
104  }
105}

Admin panel for managing roles and permissions

Fragment of the admin panel implementation for managing roles and permissions:

1// app/admin/roles/page.tsx
2import { requirePermission } from '@/lib/auth-utils';
3import Link from 'next/link';
4import { prisma } from '@/lib/prisma';
5
6export default async function RolessAdminPage() {
7  await requirePermission('admin:roles');
8  
9  const roles = await prisma.role.findMany({
10    include: {
11      _count: {
12        select: {
13          users: true,
14          permissions: true,
15        },
16      },
17    },
18    orderBy: {
19      name: 'asc',
20    },
21  });
22  
23  return (
24    <div className="p-6">
25      <div className="flex justify-between items-center mb-6">
26        <h1 className="text-2xl font-bold">Role management</h1>
27        <Link 
28          href="/admin/roles/new" 
29          className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700"
30        >
31          Add new role
32        </Link>
33      </div>
34      
35      <div className="bg-white shadow overflow-hidden rounded-md">
36        <table className="min-w-full divide-y divide-gray-200">
37          <thead className="bg-gray-50">
38            <tr>
39              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
40                Name
41              </th>
42              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
43                Description
44              </th>
45              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
46                Users
47              </th>
48              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
49                Permissions
50              </th>
51              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
52                Actions
53              </th>
54            </tr>
55          </thead>
56          <tbody className="bg-white divide-y divide-gray-200">
57            {roles.map((role) => (
58              <tr key={role.id}>
59                <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
60                  {role.name}
61                </td>
62                <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
63                  {role.description}
64                </td>
65                <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
66                  {role._count.users}
67                </td>
68                <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
69                  {role._count.permissions}
70                </td>
71                <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
72                  <Link 
73                    href={`/admin/roles/${role.id}`}
74                    className="text-blue-600 hover:text-blue-900 mr-4"
75                  >
76                    Edit
77                  </Link>
78                  <Link 
79                    href={`/admin/roles/${role.id}/permissions`}
80                    className="text-green-600 hover:text-green-900"
81                  >
82                    Permissions
83                  </Link>
84                </td>
85              </tr>
86            ))}
87          </tbody>
88        </table>
89      </div>
90    </div>
91  );
92}

Debugging and permission diagnostics

It can be useful to create a diagnostic component that shows the current roles and permissions of the logged-in user:

1// components/PermissionDebugger.tsx
2'use client';
3
4import { useSession } from 'next-auth/react';
5import { useState } from 'react';
6
7export function PermissionDebugger() {
8  const { data: session, status } = useSession();
9  const [showDetails, setShowDetails] = useState(false);
10  
11  if (status !== 'authenticated') {
12    return null;
13  }
14  
15  return (
16    <div className="fixed bottom-4 right-4 p-4 bg-white border border-gray-300 rounded-lg shadow-lg max-w-md z-50">
17      <div className="flex justify-between items-center mb-2">
18        <h3 className="font-bold text-lg">Permission diagnostics</h3>
19        <button 
20          onClick={() => setShowDetails(!showDetails)}
21          className="text-blue-600 hover:text-blue-800"
22        >
23          {showDetails ? 'Hide details' : 'Show details'}
24        </button>
25      </div>
26      
27      <div className="mb-2">
28        <span className="font-semibold">User:</span> {session.user.name}
29      </div>
30      
31      <div className="mb-2">
32        <span className="font-semibold">Role:</span> 
33        <span className="ml-2 px-2 py-1 bg-blue-100 text-blue-800 rounded-full text-xs">
34          {session.user.role}
35        </span>
36      </div>
37      
38      {showDetails && (
39        <div>
40          <div className="font-semibold mb-1">Permissions:</div>
41          <div className="max-h-40 overflow-y-auto">
42            {session.user.permissions.length > 0 ? (
43              <ul className="list-disc pl-5 space-y-1">
44                {session.user.permissions.sort().map((permission) => (
45                  <li key={permission} className="text-sm">
46                    {permission}
47                  </li>
48                ))}
49              </ul>
50            ) : (
51              <p className="text-sm text-red-600">No permissions</p>
52            )}
53          </div>
54        </div>
55      )}
56    </div>
57  );
58}

This component can be used during the development phase or for administrators in the production environment.

Extending RBAC with additional conditions (ABAC)

We can extend our authorization system with additional conditions, implementing elements of attribute-based access control (ABAC):

1// lib/auth-conditions.ts
2import { Session } from 'next-auth';
3
4interface AccessConditions {
5  isOwner?: (userId: string, resourceId: string) => Promise<boolean>;
6  isInSameTeam?: (userId: string, resourceId: string) => Promise<boolean>;
7  isWithinBusinessHours?: () => boolean;
8  isFromAllowedIP?: (ipAddress: string) => boolean;
9  // More conditions can be added as needed
10}
11
12export async function checkAccessConditions(
13  session: Session | null, 
14  permission: string,
15  resourceId: string,
16  conditions: AccessConditions,
17  ipAddress?: string
18): Promise<boolean> {
19  // First check the basic permission
20  if (!session?.user?.permissions?.includes(permission)) {
21    return false;
22  }
23  
24  // If it's an administrator, always allow access
25  if (session.user.role === 'admin') {
26    return true;
27  }
28  
29  // Check additional conditions
30  if (conditions.isOwner && resourceId) {
31    const isOwner = await conditions.isOwner(session.user.id, resourceId);
32    if (!isOwner) {
33      return false;
34    }
35  }
36  
37  if (conditions.isInSameTeam && resourceId) {
38    const isInSameTeam = await conditions.isInSameTeam(session.user.id, resourceId);
39    if (!isInSameTeam) {
40      return false;
41    }
42  }
43  
44  if (conditions.isWithinBusinessHours) {
45    const isWithinBusinessHours = conditions.isWithinBusinessHours();
46    if (!isWithinBusinessHours) {
47      return false;
48    }
49  }
50  
51  if (conditions.isFromAllowedIP && ipAddress) {
52    const isFromAllowedIP = conditions.isFromAllowedIP(ipAddress);
53    if (!isFromAllowedIP) {
54      return false;
55    }
56  }
57  
58  // All conditions met
59  return true;
60}
61
62// Example condition implementations
63export async function isContentOwner(userId: string, contentId: string): Promise<boolean> {
64  const content = await prisma.content.findUnique({
65    where: { id: contentId },
66    select: { authorId: true },
67  });
68  
69  return content?.authorId === userId;
70}
71
72export function isWithinBusinessHours(): boolean {
73  const now = new Date();
74  const hours = now.getHours();
75  const day = now.getDay(); // 0 = Sunday, 6 = Saturday
76  
77  // Working hours: 9:00 - 17:00, Monday to Friday
78  return day >= 1 && day <= 5 && hours >= 9 && hours < 17;
79}

Example usage:

1// app/api/protected/content/[id]/route.ts
2import { NextResponse } from 'next/server';
3import { getServerSession } from 'next-auth/next';
4import { authOptions } from '@/lib/auth';
5import { checkAccessConditions, isContentOwner } from '@/lib/auth-conditions';
6import { headers } from 'next/headers';
7
8export async function PUT(
9  request: Request,
10  { params }: { params: Promise<{ id: string }> }
11) {
12  const session = await getServerSession(authOptions);
13  const headersList = await headers();
14  const ipAddress = headersList.get('x-forwarded-for') || 'unknown';
15  
16  // We check the 'content:update' permission and additional conditions
17  const hasAccess = await checkAccessConditions(
18    session,
19    'content:update',
20    (await params).id,
21    { isOwner: isContentOwner },
22    ipAddress as string
23  );
24  
25  if (!hasAccess) {
26    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
27  }
28  
29  // Continue with content update
30  //
31}

Testing the roles and permissions system

Example unit test for permission checking function:

1// __tests__/auth-utils.test.ts
2import { hasPermission, hasRoles } from '@/lib/auth-utils';
3
4describe('Auth Utils', () => {
5  describe('hasRoles', () => {
6    it('should return false for null session', () => {
7      expect(hasRoles(null, 'admin')).toBe(false);
8    });
9    
10    it('should return false if user has no role', () => {
11      const session = { user: {} } as any;
12      expect(hasRoles(session, 'admin')).toBe(false);
13    });
14    
15    it('should return true if user has the specified role', () => {
16      const session = { user: { role: 'admin' } } as any;
17      expect(hasRoles(session, 'admin')).toBe(true);
18    });
19    
20    it('should return false if user does not have the specified role', () => {
21      const session = { user: { role: 'user' } } as any;
22      expect(hasRoles(session, 'admin')).toBe(false);
23    });
24    
25    it('should return true if user has one of the specified roles', () => {
26      const session = { user: { role: 'moderator' } } as any;
27      expect(hasRoles(session, ['admin', 'moderator'])).toBe(true);
28    });
29  });
30  
31  describe('hasPermission', () => {
32    it('should return false for null session', () => {
33      expect(hasPermission(null, 'user:read')).toBe(false);
34    });
35    
36    it('should return false if user has no permissions', () => {
37      const session = { user: {} } as any;
38      expect(hasPermission(session, 'user:read')).toBe(false);
39    });
40    
41    it('should return true if user has the specified permission', () => {
42      const session = { user: { permissions: ['user:read', 'user:create'] } } as any;
43      expect(hasPermission(session, 'user:read')).toBe(true);
44    });
45    
46    it('should return false if user does not have the specified permission', () => {
47      const session = { user: { permissions: ['user:read'] } } as any;
48      expect(hasPermission(session, 'user:delete')).toBe(false);
49    });
50    
51    it('should return true if user has one of the specified permissions', () => {
52      const session = { user: { permissions: ['user:read', 'content:read'] } } as any;
53      expect(hasPermission(session, ['user:delete', 'content:read'])).toBe(true);
54    });
55  });
56});

Best practices and recommendations

  1. Minimal permissions - grant users only the necessary permissions (principle of least privilege)

  2. Hierarchical roles - design roles hierarchically, where higher roles contain all permissions of lower roles plus additional ones

  3. Separation of authentication and authorization - treat them as separate security layers

  4. Detailed logs - log all attempts to access protected resources, especially unsuccessful ones

  5. Regular audits - periodically review and verify assigned roles and permissions

  6. Removing unused permissions - systematically remove unused roles and permissions

  7. Emergency access - provide an emergency access mechanism for administrators in case of role system problems

  8. Penetration testing - regularly test the system for security vulnerabilities

Summary

In this module, we implemented an advanced role and permission system for Next.js applications. We learned:

  1. Data model for RBAC - how to design a database structure supporting roles and permissions
  2. Integration with NextAuth.js - how to extend the session with role and permission information
  3. Authorization utilities - implementation of helper functions for checking roles and permissions
  4. Securing different application elements:
    • Route protection using middleware
    • Component protection using Guards
    • Securing API endpoints
  5. ABAC extension - adding conditional authorization based on various attributes

A solid role and permission system is the foundation of application security, especially those with many user types and complex access requirements. Thanks to the solutions presented in this module, you can build a flexible and secure authorization system tailored to your application's needs.

In the next module, we will discuss how to create middleware for route protection, which constitutes an additional security layer in Next.js applications.

Go to CodeWorlds