Cours Next.js · Module 7: Authentification et autorisation

Rôles et permissions des utilisateurs

21 min de lecture
Dans cette leçon15

Dans les modules précédents, nous avons vu les bases de l'authentification, la connexion de fournisseurs externes et la gestion des sessions et des jetons. Nous allons maintenant nous occuper d'un autre élément essentiel de la sécurité des applications : le système de rôles et de permissions des utilisateurs, c'est-à-dire l'autorisation.

Alors que l'authentification répond à la question « Qui es-tu ? », l'autorisation répond à la question « Que peux-tu faire ? ». Un bon système de rôles et de permissions est indispensable dans toute application qui nécessite différents niveaux d'accès pour différents utilisateurs.

Concepts de base des rôles et des permissions

Avant de passer à la mise en œuvre, comprenons les concepts de base :

  1. Rôles - catégories d'utilisateurs ayant des niveaux d'accès similaires (par ex. utilisateur, modérateur, administrateur)
  2. Permissions - actions précises qu'un utilisateur peut effectuer (par ex. créer des publications, modifier des profils, supprimer des comptes)
  3. RBAC (Role-Based Access Control) - contrôle d'accès basé sur les rôles, où les permissions sont attribuées aux rôles, et les rôles aux utilisateurs
  4. ABAC (Attribute-Based Access Control) - un modèle plus avancé où les décisions d'accès sont prises en fonction de divers attributs (rôle, heure, emplacement, etc.)

Modèle de données pour les rôles et les permissions

Commençons par définir un modèle de données approprié dans 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 = accordée, false = retirée
57  createdAt    DateTime   @default(now())
58  updatedAt    DateTime   @updatedAt
59
60  @@unique([userId, permissionId])
61}

Ce modèle prend en charge :

  1. Rôles hiérarchiques - des rôles sont attribués aux utilisateurs
  2. Permissions basées sur les rôles - les rôles disposent d'ensembles de permissions
  3. Permissions individuelles - on peut accorder ou retirer des permissions précises à des utilisateurs particuliers, indépendamment de leur rôle

Initialisation des rôles et permissions de base

Créons un script qui initialise les rôles et permissions de base dans notre 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: 'Utilisateur standard avec des permissions de base'
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    // Permissions administratives
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    // Permissions de l’administrateur
67    { roleName: 'admin', permissionNames: permissions.map(p => p.name) },
68
69    // Permissions du modérateur
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    // Permissions de l’utilisateur standard
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('Utilisateur administrateur créé');
145    }
146  }
147
148  console.log('Initialisation des rôles et permissions terminée avec succès');
149}
150
151main()
152  .catch((e) => {
153    console.error(e);
154    process.exit(1);
155  })
156  .finally(async () => {
157    await prisma.$disconnect();
158  });

Pour exécuter ce script, ajoute la configuration suivante à package.json :

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

Étendre le type de session NextAuth

Pour avoir accès aux informations de rôle de l'utilisateur dans la session NextAuth, étendons les 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}

Mise en œuvre de la configuration NextAuth

Modifions maintenant la configuration de NextAuth pour y inclure les rôles et les 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        // Récupérer les permissions du rôle
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        // Ajouter les permissions du rôle
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};

Utilitaires d'autorisation

Créons maintenant des utilitaires pour vérifier facilement les rôles et permissions de l'utilisateur :

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}

Mise en œuvre des composants de protection d'accès

Créons maintenant des composants client qui permettront de contrôler facilement l'accès à certaines parties de l'interface utilisateur :

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}

Mise en œuvre du middleware

Nous pouvons aussi mettre en place un middleware pour contrôler l'accès à des chemins entiers :

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 role lub moderatora
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  // Zabezpieczenie 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};

Exemples d'utilisation dans l'application

1. Protection des routes à l'aide de fonctions serveur

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">Panneau d'administration</h1>
11      <p>Bonjour, {session.user.name} ! Tu as un accès administrateur complet.</p>
12      {/* Contenu réservé aux administrateurs */}
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">Modification du contenu</h1>
30      {/* Formulaire de modification */}
31    </div>
32  );
33}

2. Protection des composants d'interface

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">MojaAplikacja</Link>
19            <Link href="/dashboard">Dashboard</Link>
20
21            {/* Lien visible uniquement par les modérateurs et les administrateurs */}
22            <RolesGuard role={['admin', 'moderator']}>
23              <Link href="/moderator/content" className="text-blue-600">
24                Gestion du contenu
25              </Link>
26            </RolesGuard>
27
28            {/* Lien réservé aux administrateurs */}
29            <AdminOnly>
30              <Link href="/admin/dashboard" className="text-red-600">
31                Panneau d'administration
32              </Link>
33            </AdminOnly>
34
35            {/* Lien pour les utilisateurs ayant la permission de créer du contenu */}
36            <PermissionGuard permission="content:create">
37              <Link href="/content/new" className="text-green-600">
38                Créer du contenu
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">Se connecter</Link>
53            )}
54          </div>
55        </div>
56      </div>
57    </nav>
58  );
59}

3. Mise en œuvre de Route Handlers avec autorisation

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}

Panneau d'administration pour gérer les rôles et les permissions

Extrait de l'implémentation du panneau d'administration pour gérer les rôles et les 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">Gestion des rôles</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          Ajouter un nouveau rôle
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                Nom
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                Utilisateurs
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                    Edytuj
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">Rola:</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.

Rozszerzenie RBAC o dodatkowe warunki (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 = niedziela, 6 = sobota
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  // On vérifie la permission 'content:update' ainsi que les conditions supplémentaires
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});

Dobre praktyki i zalecenia

  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:
    • Ochrona tras 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.

Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Démo : Rôles et permissions des utilisateurs (RBAC)
4type Role = 'admin' | 'editor' | 'viewer' | 'moderator';
5
6interface Permission { id: string; name: string; description: string; }
7
8const allPermissions: Permission[] = [
9  { id: 'read', name: 'Lecture', description: 'Consultation du contenu' },
10  { id: 'write', name: 'Écriture', description: 'Création de nouveaux contenus' },
11  { id: 'edit', name: 'Édition', description: 'Modification du contenu' },
12  { id: 'delete', name: 'Suppression', description: 'Suppression du contenu' },
13  { id: 'publish', name: 'Publication', description: 'Publication du contenu' },
14  { id: 'manage_users', name: 'Gestion', description: 'Gestion des utilisateurs' },
15  { id: 'settings', name: 'Paramètres', description: 'Paramètres système' },
16  { id: 'moderate', name: 'Modération', description: 'Modération des commentaires' },
17];
18
19const rolePermissions: Record<Role, string[]> = {
20  admin: ['read', 'write', 'edit', 'delete', 'publish', 'manage_users', 'settings', 'moderate'],
21  editor: ['read', 'write', 'edit', 'publish'],
22  moderator: ['read', 'edit', 'moderate', 'delete'],
23  viewer: ['read'],
24};
25
26const roleColors: Record<Role, string> = { admin: '#ef4444', editor: '#f59e0b', moderator: '#8b5cf6', viewer: '#64748b' };
27
28const users = [
29  { id: '1', name: 'Anna Nowak', role: 'admin' as Role },
30  { id: '2', name: 'Piotr Wisniewski', role: 'editor' as Role },
31  { id: '3', name: 'Maria Kowalczyk', role: 'moderator' as Role },
32  { id: '4', name: 'Jan Lewandowski', role: 'viewer' as Role },
33];
34
35function RBACDemo() {
36  const [userList, setUserList] = useState(users);
37  const [selected, setSelected] = useState(users[0]);
38  const [testResult, setTestResult] = useState('');
39
40  const hasPermission = (role: Role, permId: string) => rolePermissions[role].includes(permId);
41
42  const changeRole = (userId: string, newRole: Role) => {
43    setUserList(prev => prev.map(u => u.id === userId ? { ...u, role: newRole } : u));
44    if (selected.id === userId) setSelected(prev => ({ ...prev, role: newRole }));
45  };
46
47  return (
48    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
49      <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>RBAC - Rôles et Permissions</h1>
50      <p style={{ color: '#8892b0', marginBottom: '24px' }}>Système de rôles et permissions des utilisateurs</p>
51
52      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
53        <div>
54          <h3 style={{ color: '#64ffda' }}>Utilisateurs</h3>
55          {userList.map(user => (
56            <div key={user.id} onClick={() => setSelected(user)} style={{
57              padding: '12px', marginBottom: '8px', borderRadius: '8px', cursor: 'pointer',
58              background: selected.id === user.id ? '#1a2a3e' : '#1a1a2e',
59              border: selected.id === user.id ? '1px solid #64ffda' : '1px solid #333',
60            }}>
61              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
62                <strong>{user.name}</strong>
63                <select value={user.role} onChange={e => changeRole(user.id, e.target.value as Role)} onClick={e => e.stopPropagation()}
64                  style={{ padding: '4px 8px', borderRadius: '4px', background: roleColors[user.role] + '30', color: roleColors[user.role], border: '1px solid ' + roleColors[user.role], fontSize: '12px' }}>
65                  {(Object.keys(rolePermissions) as Role[]).map(r => <option key={r} value={r}>{r}</option>)}
66                </select>
67              </div>
68            </div>
69          ))}
70        </div>
71        <div>
72          <h3 style={{ color: '#64ffda' }}>Permissions : {selected.name}</h3>
73          {allPermissions.map(perm => {
74            const allowed = hasPermission(selected.role, perm.id);
75            return (
76              <div key={perm.id} onClick={() => setTestResult(allowed ? selected.name + ' A : ' + perm.name : selected.name + ' N\'A PAS : ' + perm.name)} style={{
77                padding: '10px 14px', borderRadius: '6px', cursor: 'pointer', marginBottom: '6px',
78                background: allowed ? '#10b98120' : '#ef444420', border: allowed ? '1px solid #10b981' : '1px solid #ef4444',
79                display: 'flex', justifyContent: 'space-between', alignItems: 'center',
80              }}>
81                <div>
82                  <strong style={{ color: allowed ? '#10b981' : '#ef4444' }}>{perm.name}</strong>
83                  <div style={{ fontSize: '12px', color: '#8892b0' }}>{perm.description}</div>
84                </div>
85                <span>{allowed ? 'V' : 'X'}</span>
86              </div>
87            );
88          })}
89        </div>
90      </div>
91      {testResult && <div style={{ marginTop: '16px', padding: '12px', borderRadius: '8px', background: '#1a1a2e', border: '1px solid #333' }}>{testResult}</div>}
92    </div>
93  );
94}
95
96export default RBACDemo;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Le middleware dans Next.js pour l'authentification peut être utilisé pour :

  2. 2. RBAC (contrôle d'accès basé sur les rôles) signifie :

Tâches pratiques dans le jeu

  • Éditeur de code

    Implémente un tableau de bord avec contrôle d'accès et rôles utilisateurs

  • Éditeur de code

    Crée middleware.ts qui protège des routes spécifiques et redirige les utilisateurs non autorisés

  • Tri vertical

    Place les éléments dans le bon ordre : import { notFound } → from 'next/navigat

  • Clique dans l'ordre

    Place la syntaxe de configuration du fournisseur OAuth dans NextAuth

  • Éditeur de code

    Crée un système de rôles (admin, user, moderator) avec vérification des permissions et rendu conditionnel

Articles utiles