Cours Next.js · Module 7: Authentification et autorisation
Middleware do ochrony tras
Dans cette leçon6
In previous modules, we learned the basics of authentication, connecting external providers, session management, and implementation of the role and permission system. Now we will focus on middleware - a powerful Next.js mechanism that allows executing code before reaching the target request, which is an ideal place to implement route protection.
Middleware in Next.js works at the server level but is executed before page rendering or API request handling. This enables centralized access management, redirecting, header modification, and many other operations.
Podstawy middleware w Next.js
Let's start by understanding how middleware works in Next.js:
- Location - middleware is defined in the file
middleware.ts(lubmiddleware.js) in the main directory of the Next.js project. - Execution - middleware runs before page rendering or API request handling.
- Matching - you can specify for which paths the middleware should be executed.
- Action - middleware can redirect the user, modify the request/response, or simply proceed to the next step in the processing chain.
Simple middleware example:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 console.log('Middleware exécuté pour :', request.nextUrl.pathname);
7
8 // Continue normal request processing
9 return NextResponse.next();
10}
11
12// Apply middleware only to specific paths
13export const config = {
14 matcher: ['/dashboard/:path*', '/api/:path*'],
15};Implementacja middleware do ochrony tras
Now let's move on to the full implementation of middleware for route protection in a Next.js application.
1. Podstawowe middleware authentication
Let's start with simple middleware that checks if the user is logged in and protects specific paths:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6export async function middleware(request: NextRequest) {
7 // Récupérer le token de session depuis le cookie
8 const token = await getToken({ req: request });
9
10 // Check if user is logged in
11 const isLoggedIn = !!token;
12
13 // Current path
14 const { pathname } = request.nextUrl;
15
16 // Protected paths - requiring login
17 const protectedPaths = [
18 '/dashboard',
19 '/profile',
20 '/settings',
21 ];
22
23 // Paths only for unauthenticated users (e.g. login page)
24 const authPaths = ['/auth/login', '/auth/register'];
25
26 // Check if the current path is protected
27 const isProtectedPath = protectedPaths.some(path =>
28 pathname === path || pathname.startsWith(`${path}/`)
29 );
30
31 // Check if the current path is an authentication page
32 const isAuthPath = authPaths.some(path => pathname === path);
33
34 // If the user tries to access a protected path but is not logged in
35 if (isProtectedPath && !isLoggedIn) {
36 // Redirect to the login page with information about the target path
37 const url = new URL('/auth/login', request.url);
38 url.searchParams.set('callbackUrl', encodeURI(pathname));
39 return NextResponse.redirect(url);
40 }
41
42 // If the user is logged in and tries to access the login/registration page
43 if (isAuthPath && isLoggedIn) {
44 // Przekieruj na dashboard
45 return NextResponse.redirect(new URL('/dashboard', request.url));
46 }
47
48 // Dans les autres cas, continue le traitement normal
49 return NextResponse.next();
50}
51
52// Specify for which paths the middleware should run
53export const config = {
54 matcher: [
55 /*
56 * Match all paths except:
57 * 1. Static files (e.g. /favicon.ico, /images/*, etc.)
58 * 2. next-auth API Route (/api/auth/*, which handles authentication itself)
59 */
60 '/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
61 ],
62};2. Advanced middleware with role and permission control
Let's extend our middleware to also include user roles and permissions:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6// Structure describing access requirements for paths
7interface PathConfig {
8 requireAuth: boolean; // Czy wymagane jest zalogowanie
9 roles?: string[]; // Allowed roles (if empty, all roles have access)
10 permissions?: string[]; // Wymagane permissions
11}
12
13// Path access configuration
14const pathAccessConfig: Record<string, PathConfig> = {
15 // Administrative paths
16 '/admin': {
17 requireAuth: true,
18 roles: ['admin'],
19 },
20
21 // Moderator paths
22 '/moderator': {
23 requireAuth: true,
24 roles: ['admin', 'moderator'],
25 },
26
27 // Dashboard paths - require login
28 '/dashboard': {
29 requireAuth: true,
30 },
31
32 // User management page - requires user:manage permission
33 '/admin/users': {
34 requireAuth: true,
35 roles: ['admin'],
36 permissions: ['user:manage'],
37 },
38
39 // Public paths - available to everyone
40 '/': {
41 requireAuth: false,
42 },
43 '/about': {
44 requireAuth: false,
45 },
46 '/contact': {
47 requireAuth: false,
48 },
49
50 // Authentication paths - available only to unauthenticated
51 '/auth/login': {
52 requireAuth: false,
53 },
54 '/auth/register': {
55 requireAuth: false,
56 },
57};
58
59// Fonction utilitaire pour vérifier si le path correspond à la configuration
60function matchPathConfig(pathname: string): [string, PathConfig] | null {
61 // Check exact match
62 if (pathAccessConfig[pathname]) {
63 return [pathname, pathAccessConfig[pathname]];
64 }
65
66 // Check prefix match
67 for (const configPath in pathAccessConfig) {
68 // Ignore exact matches since we already checked them
69 if (configPath === pathname) continue;
70
71 // Check if the current path starts with the configured path + /
72 if (pathname.startsWith(`${configPath}/`)) {
73 return [configPath, pathAccessConfig[configPath]];
74 }
75 }
76
77 // Brak dopasowania
78 return null;
79}
80
81export async function middleware(request: NextRequest) {
82 // Récupérer le token de session depuis le cookie
83 const token = await getToken({ req: request });
84
85 // Current path
86 const { pathname } = request.nextUrl;
87
88 // Find access configuration for the current path
89 const pathMatch = matchPathConfig(pathname);
90
91 // If no configuration was found for this path, continue normal processing
92 if (!pathMatch) {
93 return NextResponse.next();
94 }
95
96 const [configPath, config] = pathMatch;
97
98 // Authentication pages - redirect to dashboard if the user is already logged in
99 if (
100 !config.requireAuth &&
101 (pathname === '/auth/login' || pathname === '/auth/register') &&
102 token
103 ) {
104 return NextResponse.redirect(new URL('/dashboard', request.url));
105 }
106
107 // If the path requires authentication and the user is not logged in
108 if (config.requireAuth && !token) {
109 // Redirect to the login page with information about the target path
110 const url = new URL('/auth/login', request.url);
111 url.searchParams.set('callbackUrl', pathname);
112 return NextResponse.redirect(url);
113 }
114
115 // If the path requires specific roles, check the user's permissions
116 if (token && config.roles && config.roles.length > 0) {
117 const userRoles = token.role as string;
118
119 if (!config.roles.includes(userRoles)) {
120 // User does not have the required role - redirect to the no-permissions page
121 return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
122 }
123 }
124
125 // If the path requires specific permissions, check the user's permissions
126 if (token && config.permissions && config.permissions.length > 0) {
127 const userPermissions = token.permissions as string[] || [];
128
129 // Check if the user has all required permissions
130 const hasAllPermissions = config.permissions.every(permission =>
131 userPermissions.includes(permission)
132 );
133
134 if (!hasAllPermissions) {
135 // User does not have the required permissions - redirect to the no-permissions page
136 return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
137 }
138 }
139
140 // All conditions met - kontynuuj
141 return NextResponse.next();
142}
143
144// Match for all paths except static files and authentication API
145export const config = {
146 matcher: [
147 '/((?!api/auth|_next/static|_next/image|images|favicon.ico).*)',
148 ],
149};3. Middleware pour l'API d'authentification
As you can see, the described middleware works for pages, redirecting the user in case of missing permissions. For API routes, we should return appropriate error codes rather than redirecting the user. Here is the middleware implementation specifically for API:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6// API path access configuration
7const apiAccessConfig: Record<string, {
8 roles?: string[];
9 permissions?: string[];
10}> = {
11 // API administratora
12 '/api/admin': {
13 roles: ['admin'],
14 },
15
16 // API moderatora
17 '/api/moderator': {
18 roles: ['admin', 'moderator'],
19 },
20
21 // User management API
22 '/api/users': {
23 permissions: ['user:read'],
24 },
25 '/api/users/create': {
26 permissions: ['user:create'],
27 },
28 '/api/users/update': {
29 permissions: ['user:update'],
30 },
31 '/api/users/delete': {
32 permissions: ['user:delete'],
33 },
34};
35
36// Fonction utilitaire pour vérifier si le path API correspond à la configuration
37function matchApiConfig(pathname: string) {
38 // Check exact match
39 if (apiAccessConfig[pathname]) {
40 return [pathname, apiAccessConfig[pathname]];
41 }
42
43 // Check prefix match
44 for (const configPath in apiAccessConfig) {
45 if (pathname.startsWith(`${configPath}/`)) {
46 return [configPath, apiAccessConfig[configPath]];
47 }
48 }
49
50 return null;
51}
52
53export async function middleware(request: NextRequest) {
54 // Récupérer le token de session
55 const token = await getToken({ req: request });
56
57 // Current path
58 const { pathname } = request.nextUrl;
59
60 // Check if this is an API request
61 const isApiRequest = pathname.startsWith('/api/');
62
63 // If it is not API, proceed to the next middleware
64 if (!isApiRequest) {
65 return NextResponse.next();
66 }
67
68 // Skip authentication routes
69 if (pathname.startsWith('/api/auth/')) {
70 return NextResponse.next();
71 }
72
73 // If it is API and the user is not logged in
74 if (!token) {
75 return NextResponse.json(
76 { error: 'Unauthorized - Authentication required' },
77 { status: 401 }
78 );
79 }
80
81 // Find access configuration for this API endpoint
82 const apiMatch = matchApiConfig(pathname);
83
84 // If no configuration, we assume being logged in is sufficient
85 if (!apiMatch) {
86 return NextResponse.next();
87 }
88
89 const [configPath, config] = apiMatch;
90
91 // Check user role
92 if (config.roles && config.roles.length > 0) {
93 const userRoles = token.role as string;
94
95 if (!config.roles.includes(userRoles)) {
96 return NextResponse.json(
97 { error: 'Forbidden - Insufficient role permissions' },
98 { status: 403 }
99 );
100 }
101 }
102
103 // Check user permissions
104 if (config.permissions && config.permissions.length > 0) {
105 const userPermissions = token.permissions as string[] || [];
106
107 const hasAllPermissions = config.permissions.every(permission =>
108 userPermissions.includes(permission)
109 );
110
111 if (!hasAllPermissions) {
112 return NextResponse.json(
113 { error: 'Forbidden - Insufficient permissions' },
114 { status: 403 }
115 );
116 }
117 }
118
119 // All conditions met
120 return NextResponse.next();
121}
122
123// Correspondance pour toutes les routes API
124export const config = {
125 matcher: ['/api/:path*'],
126};4. Combining middleware for pages and API
We can combine both approaches in a single middleware:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6// Page access configuration
7const pageAccessConfig: Record<string, {
8 requireAuth: boolean;
9 roles?: string[];
10 permissions?: string[];
11}> = {
12 '/admin': { requireAuth: true, roles: ['admin'] },
13 '/moderator': { requireAuth: true, roles: ['admin', 'moderator'] },
14 '/dashboard': { requireAuth: true },
15 '/profile': { requireAuth: true },
16 '/settings': { requireAuth: true },
17 '/': { requireAuth: false },
18 '/about': { requireAuth: false },
19 '/auth/login': { requireAuth: false },
20 '/auth/register': { requireAuth: false },
21};
22
23// API access configuration
24const apiAccessConfig: Record<string, {
25 roles?: string[];
26 permissions?: string[];
27}> = {
28 '/api/admin': { roles: ['admin'] },
29 '/api/moderator': { roles: ['admin', 'moderator'] },
30 '/api/users': { permissions: ['user:read'] },
31 '/api/users/create': { permissions: ['user:create'] },
32};
33
34// Helper functions for path matching (omitted for brevity)
35//
36export async function middleware(request: NextRequest) {
37 // Récupérer le token de session
38 const token = await getToken({ req: request });
39 const { pathname } = request.nextUrl;
40
41 // Check if this is an API request
42 const isApiRequest = pathname.startsWith('/api/');
43
44 // Skip authentication routes
45 if (pathname.startsWith('/api/auth/')) {
46 return NextResponse.next();
47 }
48
49 // API request handling
50 if (isApiRequest) {
51 // If it is API and the user is not logged in (for protected endpoints)
52 if (!token && !pathname.startsWith('/api/public/')) {
53 return NextResponse.json(
54 { error: 'Unauthorized - Authentication required' },
55 { status: 401 }
56 );
57 }
58
59 // Match API configuration
60 const apiMatch = matchApiConfig(pathname);
61 if (apiMatch && token) {
62 const [configPath, config] = apiMatch;
63
64 // Check role and permissions
65 if (!hasRequiredRoles(token, config.roles) ||
66 !hasRequiredPermissions(token, config.permissions)) {
67 return NextResponse.json(
68 { error: 'Forbidden - Insufficient permissions' },
69 { status: 403 }
70 );
71 }
72 }
73 }
74 // Page handling
75 else {
76 // Match page configuration
77 const pageMatch = matchPageConfig(pathname);
78 if (pageMatch) {
79 const [configPath, config] = pageMatch;
80
81 // Authentication pages - redirect if the user is logged in
82 if (
83 !config.requireAuth &&
84 (pathname === '/auth/login' || pathname === '/auth/register') &&
85 token
86 ) {
87 return NextResponse.redirect(new URL('/dashboard', request.url));
88 }
89
90 // If the page requires authentication and the user is not logged in
91 if (config.requireAuth && !token) {
92 const url = new URL('/auth/login', request.url);
93 url.searchParams.set('callbackUrl', pathname);
94 return NextResponse.redirect(url);
95 }
96
97 // If the user is logged in, check role and permissions
98 if (token && config.requireAuth) {
99 if (!hasRequiredRoles(token, config.roles) ||
100 !hasRequiredPermissions(token, config.permissions)) {
101 return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
102 }
103 }
104 }
105 }
106
107 // All conditions met
108 return NextResponse.next();
109}
110
111// Helper functions for checking roles and permissions
112function hasRequiredRoles(token: any, requiredRoless?: string[]): boolean {
113 if (!requiredRoless || requiredRoless.length === 0) return true;
114
115 const userRoles = token.role as string;
116 return requiredRoless.includes(userRoles);
117}
118
119function hasRequiredPermissions(token: any, requiredPermissions?: string[]): boolean {
120 if (!requiredPermissions || requiredPermissions.length === 0) return true;
121
122 const userPermissions = token.permissions as string[] || [];
123 return requiredPermissions.every(permission => userPermissions.includes(permission));
124}
125
126// Functions matching path configuration (implementation omitted for brevity)
127function matchPageConfig(pathname: string) { /* ... */ }
128function matchApiConfig(pathname: string) { /* ... */ }
129
130export const config = {
131 matcher: [
132 '/((?!_next/static|_next/image|images|favicon.ico).*)',
133 ],
134};Middleware for handling specific access patterns
Often we need middleware that handles specific use cases. Let's look at some examples:
1. Middleware do kontrole limitu requests (Rate Limiting)
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Simple in-memory request limit implementation
6// In production, you should use Redis or a similar solution
7const REQUESTS_PER_MINUTE = 60;
8const WINDOW_SIZE_MS = 60 * 1000; // 1 minuta
9
10interface RateLimitState {
11 requests: number;
12 timestamp: number;
13}
14
15const ipRequestCounts = new Map<string, RateLimitState>();
16
17export function middleware(request: NextRequest) {
18 // Récupérer l’adresse IP de l’utilisateur
19 const ip = request.ip || 'unknown';
20
21 // Check if it is API
22 if (!request.nextUrl.pathname.startsWith('/api/')) {
23 return NextResponse.next();
24 }
25
26 // Get the current limit state for this IP
27 const now = Date.now();
28 const state = ipRequestCounts.get(ip) || { requests: 0, timestamp: now };
29
30 // If the time window has passed, reset the counter
31 if (now - state.timestamp > WINDOW_SIZE_MS) {
32 state.requests = 0;
33 state.timestamp = now;
34 }
35
36 // Increment the request counter
37 state.requests++;
38 ipRequestCounts.set(ip, state);
39
40 // If the limit is exceeded, return error 429 (Too Many Requests)
41 if (state.requests > REQUESTS_PER_MINUTE) {
42 const response = NextResponse.json(
43 { error: 'Rate limit exceeded. Please try again later.' },
44 { status: 429 }
45 );
46
47 // Set headers informing about limits
48 response.headers.set('X-RateLimit-Limit', REQUESTS_PER_MINUTE.toString());
49 response.headers.set('X-RateLimit-Remaining', '0');
50 response.headers.set('Retry-After',
51 Math.ceil((state.timestamp + WINDOW_SIZE_MS - now) / 1000).toString()
52 );
53
54 return response;
55 }
56
57 // W granicach limitu - kontynuuj
58 const response = NextResponse.next();
59
60 // Set headers informing about limits
61 response.headers.set('X-RateLimit-Limit', REQUESTS_PER_MINUTE.toString());
62 response.headers.set(
63 'X-RateLimit-Remaining',
64 Math.max(0, REQUESTS_PER_MINUTE - state.requests).toString()
65 );
66
67 return response;
68}
69
70export const config = {
71 matcher: ['/api/:path*'],
72};2. Middleware pour la géolocalisation (accès par pays)
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// List of allowed country codes
6const ALLOWED_COUNTRIES = ['US', 'CA', 'GB', 'DE', 'FR', 'PL'];
7
8// List of pages subject to geographic restrictions
9const GEO_RESTRICTED_PATHS = [
10 '/premium-content',
11 '/members-only',
12];
13
14export function middleware(request: NextRequest) {
15 // Check if the path is subject to geographic restrictions
16 const isGeoRestrictedPath = GEO_RESTRICTED_PATHS.some(path =>
17 request.nextUrl.pathname === path ||
18 request.nextUrl.pathname.startsWith(`${path}/`)
19 );
20
21 if (isGeoRestrictedPath) {
22 // Get the user's country information from headers (available on the Vercel platform)
23 const country = request.geo?.country || 'Unknown';
24
25 // If the user's country is not on the allowed list
26 if (!ALLOWED_COUNTRIES.includes(country)) {
27 return NextResponse.redirect(new URL('/geo-restricted', request.url));
28 }
29 }
30
31 return NextResponse.next();
32}
33
34export const config = {
35 matcher: [
36 '/premium-content/:path*',
37 '/members-only/:path*',
38 ],
39};3. Middleware for modifying security headers
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 // Get the original response
7 const response = NextResponse.next();
8
9 // Add security headers
10
11 // Protect against XSS attacks
12 response.headers.set('X-XSS-Protection', '1; mode=block');
13
14 // Prevent MIME type "sniffing"
15 response.headers.set('X-Content-Type-Options', 'nosniff');
16
17 // Control how the page can be embedded in an iframe
18 response.headers.set('X-Frame-Options', 'SAMEORIGIN');
19
20 // Content Security Policy - protection plus avancée contre XSS
21 response.headers.set(
22 'Content-Security-Policy',
23 "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com;"
24 );
25
26 // HTTP Strict Transport Security - forces HTTPS use
27 response.headers.set(
28 'Strict-Transport-Security',
29 'max-age=31536000; includeSubDomains; preload'
30 );
31
32 // Permissions Policy - declare which browser features are allowed
33 response.headers.set(
34 'Permissions-Policy',
35 'camera=(), microphone=(), geolocation=(self), interest-cohort=()'
36 );
37
38 return response;
39}
40
41export const config = {
42 matcher: [
43 '/((?!_next/static|_next/image|images|favicon.ico).*)',
44 ],
45};4. Middleware pour les tests A/B
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// A/B experiment configuration
6const AB_TESTS = {
7 'new-landing-page': {
8 variants: ['control', 'variant-a', 'variant-b'],
9 weights: [0.34, 0.33, 0.33], // Traffic split
10 paths: ['/'], // Paths covered by the test
11 cookieName: 'ab-new-landing',
12 },
13 'checkout-flow': {
14 variants: ['original', 'simplified'],
15 weights: [0.5, 0.5],
16 paths: ['/checkout', '/cart'],
17 cookieName: 'ab-checkout',
18 },
19};
20
21export function middleware(request: NextRequest) {
22 const url = request.nextUrl;
23 const response = NextResponse.next();
24
25 // Itérer sur les tests définis
26 for (const [testName, test] of Object.entries(AB_TESTS)) {
27 // Check if the current path is covered by the test
28 const isTestPath = test.paths.some(path =>
29 url.pathname === path || url.pathname.startsWith(`${path}/`)
30 );
31
32 if (isTestPath) {
33 // Check if the user already has a test variant assigned
34 const existingVariant = request.cookies.get(test.cookieName)?.value;
35 let variant = existingVariant;
36
37 // If the user has no assigned variant, assign a new one
38 if (!variant) {
39 variant = assignVariant(test.variants, test.weights);
40
41 // Définir le cookie avec le variant assigné
42 response.cookies.set(test.cookieName, variant, {
43 maxAge: 60 * 60 * 24 * 30, // 30 dni
44 path: '/',
45 });
46 }
47
48 // Add a header with test variant information
49 response.headers.set(`X-AB-Test-${testName}`, variant);
50
51 // You can also set a rewrite to a different page for different variants
52 if (testName === 'new-landing-page' && url.pathname === '/') {
53 if (variant === 'variant-a') {
54 // Internal redirect (invisible to the user)
55 return NextResponse.rewrite(new URL('/landing-a', request.url));
56 } else if (variant === 'variant-b') {
57 return NextResponse.rewrite(new URL('/landing-b', request.url));
58 }
59 }
60 }
61 }
62
63 return response;
64}
65
66// Function that assigns a variant based on weights
67function assignVariant(variants: string[], weights: number[]): string {
68 const randomNum = Math.random();
69 let cumulativeWeight = 0;
70
71 for (let i = 0; i < variants.length; i++) {
72 cumulativeWeight += weights[i];
73 if (randomNum < cumulativeWeight) {
74 return variants[i];
75 }
76 }
77
78 // Fallback na pierwszy wariant
79 return variants[0];
80}
81
82export const config = {
83 matcher: [
84 '/', '/checkout', '/cart',
85 '/landing-a', '/landing-b',
86 ],
87};Testowanie middleware
Tests are very important to verify that middleware works correctly, especially since it handles key application security aspects. Here is a simple example of unit tests:
1// __tests__/middleware.test.ts
2import { NextRequest } from 'next/server';
3import { middleware } from '../middleware';
4import { getToken } from 'next-auth/jwt';
5
6// Mockujemy getToken z next-auth/jwt
7jest.mock('next-auth/jwt', () => ({
8 getToken: jest.fn(),
9}));
10
11describe('Auth Middleware', () => {
12 // Setup and reset before each test
13 beforeEach(() => {
14 jest.clearAllMocks();
15 });
16
17 it('should redirect unauthenticated user from protected route to login page', async () => {
18 // Mock getToken returning null (unauthenticated user)
19 (getToken as jest.Mock).mockResolvedValueOnce(null);
20
21 // Create a test request to a protected path
22 const req = new NextRequest(new URL('http://localhost:3000/dashboard'));
23
24 // Exécuter le middleware
25 const response = await middleware(req);
26
27 // Check that it redirects to the login page
28 expect(response).not.toBeNull();
29 expect(response.status).toBe(307); // Kod przekierowania
30 expect(response.headers.get('location')).toBe('http://localhost:3000/auth/login?callbackUrl=%2Fdashboard');
31 });
32
33 it('should allow authenticated user to access protected route', async () => {
34 // Mock getToken returning a token (logged-in user)
35 (getToken as jest.Mock).mockResolvedValueOnce({
36 name: 'Test User',
37 email: 'test@example.com',
38 role: 'user',
39 permissions: ['content:read']
40 });
41
42 // Create a test request to a protected path
43 const req = new NextRequest(new URL('http://localhost:3000/dashboard'));
44
45 // Exécuter le middleware
46 const response = await middleware(req);
47
48 // Check that it allows access
49 expect(response).not.toBeNull();
50 expect(response.status).toBe(200);
51 });
52
53 it('should redirect authenticated user from login page to dashboard', async () => {
54 // Mock getToken returning a token (logged-in user)
55 (getToken as jest.Mock).mockResolvedValueOnce({
56 name: 'Test User',
57 email: 'test@example.com',
58 });
59
60 // Create a test request to the login page
61 const req = new NextRequest(new URL('http://localhost:3000/auth/login'));
62
63 // Exécuter le middleware
64 const response = await middleware(req);
65
66 // Check that it redirects to the dashboard
67 expect(response).not.toBeNull();
68 expect(response.status).toBe(307);
69 expect(response.headers.get('location')).toBe('http://localhost:3000/dashboard');
70 });
71
72 it('should redirect user without admin role from admin page', async () => {
73 // Mock getToken returning a token with user role
74 (getToken as jest.Mock).mockResolvedValueOnce({
75 name: 'Test User',
76 email: 'test@example.com',
77 role: 'user',
78 permissions: ['content:read']
79 });
80
81 // Create a test request to the admin page
82 const req = new NextRequest(new URL('http://localhost:3000/admin/dashboard'));
83
84 // Exécuter le middleware
85 const response = await middleware(req);
86
87 // Check that it redirects to the no-permissions page
88 expect(response).not.toBeNull();
89 expect(response.status).toBe(307);
90 expect(response.headers.get('location')).toBe('http://localhost:3000/auth/unauthorized');
91 });
92});Dobre praktyki i wzorce
1. Performance optimization middleware
Middleware is executed on every request, so it's important that it's as efficient as possible:
- Limit external API calls - Middleware should be fast. Avoid API calls in middleware when possible.
- Use caching - If you must fetch data, consider caching it.
- Define matcher precisely - Limit middleware execution only to paths that need it.
2. Debugging middleware
Debugging middleware can be difficult because it runs "behind the scenes":
- Add logs - Use
console.log()to trace middleware activity. - Add debug headers - You can add custom response headers to track what is happening.
1// Example of adding debug headers
2const response = NextResponse.next();
3response.headers.set('X-Debug-Path', request.nextUrl.pathname);
4response.headers.set('X-Debug-Auth', token ? 'Authenticated' : 'Unauthenticated');
5return response;3. Organizacja kodu middleware
For complex applications, it's worth considering middleware modularization:
1// middleware/auth.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { getToken } from 'next-auth/jwt';
4
5export async function authMiddleware(req: NextRequest) {
6 // Implementacja middleware authentication
7 //
8}
9
10// middleware/security.ts
11import { NextRequest, NextResponse } from 'next/server';
12
13export function securityHeadersMiddleware(req: NextRequest) {
14 // Security headers middleware implementation
15 //
16}
17
18// middleware.ts
19import { NextRequest, NextResponse } from 'next/server';
20import { authMiddleware } from './middleware/auth';
21import { securityHeadersMiddleware } from './middleware/security';
22
23export async function middleware(req: NextRequest) {
24 // Check if the path requires authentication
25 if (req.nextUrl.pathname.startsWith('/dashboard') ||
26 req.nextUrl.pathname.startsWith('/admin')) {
27 return await authMiddleware(req);
28 }
29
30 // Ajouter les en-têtes de sécurité à toutes les réponses
31 return securityHeadersMiddleware(req);
32}
33
34export const config = { /* ... */ };Summary
Middleware in Next.js offers a powerful mechanism for implementing route protection and many other server-level features. In this module we learned:
- Middleware basics in Next.js - how it works and how to configure it
- Authentication middleware implementation - how to protect routes from unauthorized access
- Middleware with role and permission control - how to provide access based on user roles
- Specialized middleware - how to implement rate limiting, geolocation, security headers, and A/B tests
- Middleware testing - how to write tests for middleware
- Best practices and patterns - how to optimize and organize middleware code
A good middleware implementation is a key element of Next.js application security and performance. Thanks to the techniques presented in this module, you can create advanced, multi-layered protections for your applications.
In the next module, we will move on to implementing a Context Provider for authentication, which will enable easy access to authentication data throughout the application.
Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Démo : Middleware de protection des routes dans Next.js
4interface Route {
5 path: string;
6 label: string;
7 isProtected: boolean;
8 requiredRole?: string;
9 icon: string;
10}
11
12const routes: Route[] = [
13 { path: '/', label: 'Accueil', isProtected: false, icon: 'H' },
14 { path: '/about', label: 'À propos', isProtected: false, icon: 'A' },
15 { path: '/dashboard', label: 'Dashboard', isProtected: true, requiredRole: 'user', icon: 'D' },
16 { path: '/admin', label: 'Panneau Admin', isProtected: true, requiredRole: 'admin', icon: 'S' },
17 { path: '/admin/users', label: 'Utilisateurs', isProtected: true, requiredRole: 'admin', icon: 'U' },
18 { path: '/api/data', label: 'API : Données', isProtected: true, requiredRole: 'user', icon: 'P' },
19 { path: '/login', label: 'Connexion', isProtected: false, icon: 'L' },
20 { path: '/profile', label: 'Profil', isProtected: true, requiredRole: 'user', icon: 'R' },
21];
22
23function MiddlewareDemo() {
24 const [isLoggedIn, setIsLoggedIn] = useState(false);
25 const [userRole, setUserRole] = useState<'user' | 'admin'>('user');
26 const [requestLog, setRequestLog] = useState<{ path: string; result: string; color: string }[]>([]);
27
28 const simulateRequest = (route: Route) => {
29 let result = '';
30 let color = '';
31
32 if (!route.isProtected) {
33 result = '200 OK - Page publique';
34 color = '#10b981';
35 } else if (!isLoggedIn) {
36 result = '302 Redirect -> /login (aucun token)';
37 color = '#f59e0b';
38 } else if (route.requiredRole === 'admin' && userRole !== 'admin') {
39 result = '403 Forbidden - rôle requis : admin';
40 color = '#ef4444';
41 } else {
42 result = '200 OK - Accès accordé (' + userRole + ')';
43 color = '#10b981';
44 }
45
46 setRequestLog(prev => [{ path: route.path, result, color }, ...prev.slice(0, 9)]);
47 };
48
49 return (
50 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
51 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Middleware - Protection des Routes</h1>
52 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Simulation du middleware Next.js pour protéger les routes</p>
53
54 <div style={{ display: 'flex', gap: '16px', marginBottom: '24px', alignItems: 'center', flexWrap: 'wrap' }}>
55 <label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
56 <input type="checkbox" checked={isLoggedIn} onChange={e => setIsLoggedIn(e.target.checked)} />
57 <span>Connecté</span>
58 </label>
59 {isLoggedIn && (
60 <select value={userRole} onChange={e => setUserRole(e.target.value as 'user' | 'admin')} style={{ padding: '6px 12px', borderRadius: '6px', background: '#1a1a2e', color: '#fff', border: '1px solid #333' }}>
61 <option value="user">user</option>
62 <option value="admin">admin</option>
63 </select>
64 )}
65 </div>
66
67 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
68 <div>
69 <h3 style={{ color: '#64ffda', fontSize: '16px' }}>Routes</h3>
70 {routes.map(route => (
71 <button key={route.path} onClick={() => simulateRequest(route)} style={{
72 display: 'block', width: '100%', padding: '12px', marginBottom: '8px', borderRadius: '8px', border: '1px solid #333',
73 background: '#1a1a2e', color: '#e0e0e0', cursor: 'pointer', textAlign: 'left',
74 }}>
75 <code style={{ color: '#64ffda' }}>{route.path}</code>
76 <div style={{ fontSize: '12px', color: '#8892b0' }}>
77 {route.isProtected ? 'Protégée (rôle : ' + route.requiredRole + ')' : 'Publique'}
78 </div>
79 </button>
80 ))}
81 </div>
82 <div>
83 <h3 style={{ color: '#64ffda', fontSize: '16px' }}>Journal des requêtes</h3>
84 {requestLog.map((log, i) => (
85 <div key={i} style={{ padding: '10px', borderRadius: '6px', background: '#1a1a2e', border: '1px solid #333', marginBottom: '8px', fontSize: '13px' }}>
86 <code style={{ color: '#64ffda' }}>{log.path}</code>
87 <div style={{ color: log.color, marginTop: '4px' }}>{log.result}</div>
88 </div>
89 ))}
90 {requestLog.length === 0 && <p style={{ color: '#555' }}>Clique sur une route pour simuler une requête</p>}
91 </div>
92 </div>
93 </div>
94 );
95}
96
97export default MiddlewareDemo;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. bcrypt est utilisé pour :
2. Les jetons de rafraîchissement sont utilisés pour :
Tâches pratiques dans le jeu
- Éditeur de code
Crée un panneau de contrôle avec un système de permissions et un middleware
- Clique dans l'ordre
Place la syntaxe du middleware de vérification de session dans Next.js
- Éditeur de code
Crée un système de vérification d'email avec génération de jeton, envoi d'email et endpoint de vérification
- Tri vertical
Place les étapes de l'audit des événements d'authentification :