Cours Next.js · Module 7: Authentification et autorisation

Middleware do ochrony tras

19 min de lecture
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:

  1. Location - middleware is defined in the file middleware.ts (lub middleware.js) in the main directory of the Next.js project.
  2. Execution - middleware runs before page rendering or API request handling.
  3. Matching - you can specify for which paths the middleware should be executed.
  4. 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:

  1. Middleware basics in Next.js - how it works and how to configure it
  2. Authentication middleware implementation - how to protect routes from unauthorized access
  3. Middleware with role and permission control - how to provide access based on user roles
  4. Specialized middleware - how to implement rate limiting, geolocation, security headers, and A/B tests
  5. Middleware testing - how to write tests for middleware
  6. 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. 1. bcrypt est utilisé pour :

  2. 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 :

Articles utiles