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.
Before we move on to the implementation, let's understand the basic concepts:
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:
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}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}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};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}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}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};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}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}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}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}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.
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}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});Minimal permissions - grant users only the necessary permissions (principle of least privilege)
Hierarchical roles - design roles hierarchically, where higher roles contain all permissions of lower roles plus additional ones
Separation of authentication and authorization - treat them as separate security layers
Detailed logs - log all attempts to access protected resources, especially unsuccessful ones
Regular audits - periodically review and verify assigned roles and permissions
Removing unused permissions - systematically remove unused roles and permissions
Emergency access - provide an emergency access mechanism for administrators in case of role system problems
Penetration testing - regularly test the system for security vulnerabilities
In this module, we implemented an advanced role and permission system for Next.js applications. We learned:
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.