Cours Next.js · Module 7: Authentification et autorisation

Sessions utilisateur et gestion des jetons

17 min de lecture
Dans cette leçon14

Dans les modules précédents, nous avons vu les bases de l'authentification dans Next.js et la mise en place de la connexion avec des fournisseurs externes. Nous allons maintenant approfondir un aspect essentiel de tout système d'authentification : la gestion des sessions utilisateur et des jetons. Comprendre ces mécanismes est indispensable pour construire des applications web sûres et performantes.

Les bases de la gestion des sessions

Une session utilisateur est un mécanisme qui permet à l'application de « se souvenir » d'un utilisateur connecté pendant sa navigation entre les pages et les requêtes. Dans les applications web modernes, les sessions sont mises en œuvre de deux grandes manières :

  1. Sessions côté serveur - l'identifiant de session est stocké dans un cookie, et les données de session sur le serveur
  2. Sessions sans état (par ex. JWT) - toutes les informations nécessaires sont stockées dans le jeton, souvent dans un cookie

Dans Next.js, surtout combiné à NextAuth.js, nous avons accès aux deux approches.

Les sessions dans NextAuth.js

NextAuth.js propose deux grandes stratégies de gestion des sessions :

1. Stratégie JWT (par défaut)

Dans cette stratégie, toutes les informations de session sont stockées dans un JWT (JSON Web Token) enregistré dans un cookie :

1// Configuration de la stratégie JWT dans NextAuth.js
2export const authOptions = {
3  providers: [
4    // Provider configuration...
5  ],
6  session: {
7    strategy: "jwt",
8    maxAge: 30 * 24 * 60 * 60, // 30 jours en secondes
9    updateAge: 24 * 60 * 60,    // 24 heures en secondes
10  },
11  jwt: {
12    secret: process.env.NEXTAUTH_SECRET,
13    // You can also use private/public keys instead of a simple secret
14    // encryption: true, // Enables JWT encryption
15    // signingKey: process.env.JWT_SIGNING_PRIVATE_KEY,
16    // encryptionKey: process.env.JWT_ENCRYPTION_KEY,
17  }
18};

Avantages :

  • Meilleure évolutivité - le serveur n'a pas besoin de stocker les données de session
  • Fonctionne bien dans un environnement serverless
  • Plus simple à mettre en œuvre

Inconvénients :

  • Taille de données limitée (les jetons ne doivent pas être trop volumineux)
  • Plus difficile d'invalider un jeton avant son expiration
  • Les données sensibles sont stockées dans le jeton (bien que chiffrées)

2. Stratégie basée sur la base de données

Dans cette stratégie, seul l'identifiant de session est stocké dans le cookie, et le reste des données de session est conservé dans la base de données :

1// Configuration de la stratégie base de données dans NextAuth.js
2import { PrismaAdapter } from "@auth/prisma-adapter";
3import { PrismaClient } from "@prisma/client";
4
5const prisma = new PrismaClient();
6
7export const authOptions = {
8  providers: [
9    // Provider configuration...
10  ],
11  adapter: PrismaAdapter(prisma),
12  session: {
13    strategy: "database",
14    maxAge: 30 * 24 * 60 * 60, // 30 jours en secondes
15    updateAge: 24 * 60 * 60,    // 24 heures en secondes
16  }
17};

Avantages :

  • Invalidation de session plus facile (on peut supprimer l'entrée de la base de données)
  • Aucune limite sur la quantité de données
  • Plus sûr pour les données sensibles

Inconvénients :

  • Nécessite un accès à la base de données à chaque requête
  • Évolutivité légèrement plus difficile
  • Nécessite un adaptateur de base de données

Structure du jeton JWT dans NextAuth.js

Lorsque nous utilisons la stratégie JWT, NextAuth.js crée un jeton contenant les informations de base de l'utilisateur :

1{
2  "name": "John Doe",
3  "email": "john@example.com",
4  "sub": "cl5ab1gxk0001t8kgxs4t8j1h", // user identifier
5  "iat": 1658309600, // date d'émission du jeton
6  "exp": 1660901600, // token expiration time
7  "jti": "4b32c6a4-d2c5-4f1a-a2b3-5c84c1d94208" // identifiant unique du jeton
8}

Nous pouvons enrichir le jeton avec des données supplémentaires à l'aide du callback jwt :

1export const authOptions = {
2  //
3  callbacks: {
4    async jwt({ token, user, account }) {
5      // On first login we have access to the user object
6      if (user) {
7        token.role = user.role;
8        token.userId = user.id;
9        token.provider = account?.provider;
10      }
11
12      // Here we can add token refresh logic
13      // e.g. checking if the OAuth access token has not expired
14
15      return token;
16    },
17
18    async session({ session, token }) {
19      // Add data from the token to the session object (available on the client)
20      if (session.user) {
21        session.user.role = token.role;
22        session.user.id = token.userId;
23        session.user.provider = token.provider;
24      }
25
26      return session;
27    }
28  }
29};

Configuration des cookies de session

Par défaut, NextAuth.js configure les cookies de session selon les meilleures pratiques de sécurité, mais nous pouvons personnaliser ces paramètres :

1export const authOptions = {
2  //
3  cookies: {
4    sessionToken: {
5      name: `__Secure-next-auth.session-token`,
6      options: {
7        httpOnly: true,
8        sameSite: "lax",
9        path: "/",
10        secure: true,  // Mets à false pour les tests locaux sans HTTPS
11        maxAge: 30 * 24 * 60 * 60, // 30 jours en secondes
12      },
13    },
14    // You can also configure other cookies used by NextAuth.js
15    callbackUrl: {
16      name: `__Secure-next-auth.callback-url`,
17      options: { /* ... */ },
18    },
19    csrfToken: {
20      name: `__Host-next-auth.csrf-token`,
21      options: { /* ... */ },
22    },
23  },
24};

Gestion des sessions côté client

Dans les composants client, nous pouvons utiliser le hook useSession pour accéder aux données de session et à son statut :

1'use client';
2
3import { useSession } from 'next-auth/react';
4
5export default function ProfileComponent() {
6  const { data: session, status } = useSession();
7
8  if (status === 'loading') {
9    return <p>Chargement...</p>;
10  }
11
12  if (status === 'unauthenticated') {
13    return <p>Accès réservé aux utilisateurs connectés</p>;
14  }
15
16  return (
17    <div>
18      <h1>Profil</h1>
19      <p>Bonjour, {session.user.name} !</p>
20      <p>Email: {session.user.email}</p>
21      {session.user.role === 'admin' && (
22        <p>Tu as les permissions d'administrateur</p>
23      )}
24    </div>
25  );
26}

Nous pouvons aussi exiger une session à l'aide de l'option required :

1'use client';
2
3import { useSession } from 'next-auth/react';
4import { redirect } from 'next/navigation';
5
6export default function ProtectedComponent() {
7  const { data: session, status } = useSession({
8    required: true,
9    onUnauthenticated() {
10      redirect('/auth/login?callbackUrl=/profile');
11    },
12  });
13
14  // Lorsque required est défini, nous n'avons pas besoin de vérifier le statut 'unauthenticated'
15  if (status === 'loading') {
16    return <p>Chargement...</p>;
17  }
18
19  return (
20    <div>
21      <h1>Contenu protégé</h1>
22      <p>Cette page n'est visible que par les utilisateurs connectés.</p>
23    </div>
24  );
25}

Gestion des sessions côté serveur

Dans les composants serveur ou les Route Handlers, nous pouvons utiliser getServerSession pour accéder aux données de session :

1// Komponent serverowy (Server Component)
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
6export default async function ServerProfilePage() {
7  const session = await getServerSession(authOptions);
8
9  if (!session) {
10    redirect('/auth/login?callbackUrl=/profile');
11  }
12
13  return (
14    <div>
15      <h1>Profil (rendu côté serveur)</h1>
16      <p>Bonjour, {session.user.name} !</p>
17      <p>Email: {session.user.email}</p>
18      {/* Contenu visible uniquement par les utilisateurs connectés */}
19    </div>
20  );
21}
22
23// Route Handler (API Route)
24import { getServerSession } from "next-auth/next";
25import { NextResponse } from "next/server";
26import { authOptions } from "@/app/api/auth/[...nextauth]/route";
27
28export async function GET() {
29  const session = await getServerSession(authOptions);
30
31  if (!session) {
32    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
33  }
34
35  // Fetch data specific to the logged-in user
36  const userData = await fetchUserData(session.user.id);
37
38  return NextResponse.json(userData);
39}

Refresh tokens et gestion des accès OAuth

Si nous utilisons des fournisseurs OAuth, nous voudrons peut-être stocker et rafraîchir les jetons d'accès pour utiliser les API de ces fournisseurs :

1export const authOptions = {
2  //
3  callbacks: {
4    async jwt({ token, user, account }) {
5      // On first login, save access/refresh tokens
6      if (account && user) {
7        token.accessToken = account.access_token;
8        token.refreshToken = account.refresh_token;
9        token.accessTokenExpires = account.expires_at * 1000; // Conversion en millisecondes
10        token.userId = user.id;
11        return token;
12      }
13
14      // If the access token has not expired, return it
15      if (Date.now() < token.accessTokenExpires) {
16        return token;
17      }
18
19      // If the access token has expired, refresh it
20      return refreshAccessToken(token);
21    },
22
23    async session({ session, token }) {
24      // Pass the access token to the client (but not the refresh token!)
25      session.accessToken = token.accessToken;
26      session.user.id = token.userId;
27
28      return session;
29    },
30  },
31};
32
33async function refreshAccessToken(token) {
34  try {
35    // Example of refreshing a Google OAuth token
36    const url = "https://oauth2.googleapis.com/token";
37    const response = await fetch(url, {
38      headers: { "Content-Type": "application/x-www-form-urlencoded" },
39      method: "POST",
40      body: new URLSearchParams({
41        client_id: process.env.GOOGLE_CLIENT_ID,
42        client_secret: process.env.GOOGLE_CLIENT_SECRET,
43        grant_type: "refresh_token",
44        refresh_token: token.refreshToken,
45      }),
46    });
47
48    const refreshedTokens = await response.json();
49
50    if (!response.ok) {
51      throw refreshedTokens;
52    }
53
54    return {
55      ...token,
56      accessToken: refreshedTokens.access_token,
57      accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
58      // Keep the refresh token unless we received a new one
59      refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
60    };
61  } catch (error) {
62    console.error("Error refreshing access token", error);
63    // The refresh token may have expired, the user will need to log in again
64    return {
65      ...token,
66      error: "RefreshAccessTokenError",
67    };
68  }
69}

Déconnexion et invalidation de session

NextAuth.js fournit la fonction signOut() pour se déconnecter :

1'use client';
2
3import { signOut } from 'next-auth/react';
4
5export function LogoutButton() {
6  return (
7    <button
8      onClick={() => signOut({ callbackUrl: '/' })}
9      className="bg-red-500 text-white px-4 py-2 rounded"
10    >
11      Se déconnecter
12    </button>
13  );
14}

Avec la stratégie basée sur la base de données, nous pouvons invalider manuellement une session en la supprimant de la base de données :

1// app/api/auth/invalidate-sessions/route.ts
2import { NextResponse } from "next/server";
3import { getServerSession } from "next-auth/next";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5import { prisma } from "@/lib/prisma";
6
7export async function POST(request: Request) {
8  const session = await getServerSession(authOptions);
9
10  if (!session || session.user.role !== 'admin') {
11    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
12  }
13
14  const { userId } = await request.json();
15
16  // Remove all sessions of a user with the given ID
17  await prisma.session.deleteMany({
18    where: {
19      userId,
20    },
21  });
22
23  return NextResponse.json({ success: true });
24}

Avec la stratégie JWT, c'est plus difficile, car les jetons sont autonomes. Nous pouvons toutefois mettre en place une « liste noire » de jetons ou un mécanisme de rotation des secrets :

1// Mise en œuvre d'une liste noire pour les jetons JWT
2const revokedTokens = new Set();
3
4export const authOptions = {
5  //
6  callbacks: {
7    async jwt({ token }) {
8      // Check if the token is on the blacklist
9      if (revokedTokens.has(token.jti)) {
10        return null; // Token invalidated
11      }
12      return token;
13    },
14  },
15  events: {
16    async signOut({ token }) {
17      // Ajoute le jeton à la liste noire lors de la déconnexion
18      if (token?.jti) {
19        revokedTokens.add(token.jti);
20      }
21    },
22  },
23};

Cette solution simple ne fonctionne que pour un seul serveur. Pour plusieurs serveurs ou des solutions serverless, il faudrait utiliser un stockage de données partagé (par ex. Redis) pour conserver la liste des jetons invalidés.

Détection d'inactivité et déconnexion automatique

Nous pouvons mettre en place un mécanisme de détection de l'inactivité de l'utilisateur et de déconnexion automatique :

1'use client';
2
3import { useSession, signOut } from 'next-auth/react';
4import { useEffect, useState } from 'react';
5
6export function InactivityDetector({ timeoutMinutes = 30 }) {
7  const { data: session } = useSession();
8
9  useEffect(() => {
10    if (!session) return;
11
12    let timeout;
13    const timeoutMs = timeoutMinutes * 60 * 1000;
14
15    const resetTimeout = () => {
16      if (timeout) clearTimeout(timeout);
17      timeout = setTimeout(() => {
18        signOut({ callbackUrl: '/auth/login?timeout=true' });
19      }, timeoutMs);
20    };
21
22    // Reset the timer on every user activity
23    const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart'];
24
25    // Set the initial timer
26    resetTimeout();
27
28    // Add event listeners
29    events.forEach(event => {
30      window.addEventListener(event, resetTimeout);
31    });
32
33    return () => {
34      // Clear timer and listeners on unmount
35      if (timeout) clearTimeout(timeout);
36      events.forEach(event => {
37        window.removeEventListener(event, resetTimeout);
38      });
39    };
40  }, [session, timeoutMinutes]);
41
42  // Le composant n'affiche rien de visible
43  return null;
44}

Tu peux ensuite ajouter ce composant au layout principal de l'application :

1// app/layout.tsx
2import { Providers } from "./providers";
3import { InactivityDetector } from "@/components/InactivityDetector";
4
5export default function RootLayout({
6  children,
7}: {
8  children: React.ReactNode;
9}) {
10  return (
11    <html lang="pl">
12      <body>
13        <Providers>
14          {children}
15          <InactivityDetector timeoutMinutes={30} />
16        </Providers>
17      </body>
18    </html>
19  );
20}

Gérer plusieurs appareils et sessions

Si nous utilisons la stratégie basée sur la base de données, nous pouvons mettre en place la gestion de plusieurs sessions pour un même utilisateur, ce qui lui permet de consulter ses sessions actives et de se déconnecter d'appareils précis :

1// app/settings/sessions/page.tsx
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5import { prisma } from "@/lib/prisma";
6import { SessionList } from "./SessionList";
7
8export default async function SessionsPage() {
9  const session = await getServerSession(authOptions);
10
11  if (!session) {
12    redirect('/auth/login?callbackUrl=/settings/sessions');
13  }
14
15  // Fetch all active user sessions
16  const userSessions = await prisma.session.findMany({
17    where: {
18      userId: session.user.id,
19      expires: {
20        gt: new Date(), // Uniquement les sessions actives
21      },
22    },
23    orderBy: {
24      expires: 'desc',
25    },
26  });
27
28  // Mark the current session
29  const currentSessionToken = session.user.sessionToken; // You must adjust to have access to the token
30  const enhancedSessions = userSessions.map(s => ({
31    ...s,
32    isCurrent: s.sessionToken === currentSessionToken,
33  }));
34
35  return (
36    <div className="max-w-4xl mx-auto p-6">
37      <h1 className="text-2xl font-bold mb-6">Sessions actives</h1>
38      <SessionList sessions={enhancedSessions} currentSessionId={session.user.id} />
39    </div>
40  );
41}
42
43// app/settings/sessions/SessionList.tsx (komponent kliencki)
44'use client';
45
46import { useState } from 'react';
47import { useRouter } from 'next/navigation';
48
49export function SessionList({ sessions, currentSessionId }) {
50  const [isLoading, setIsLoading] = useState(false);
51  const [error, setError] = useState('');
52  const router = useRouter();
53
54  const handleRevokeSession = async (sessionToken) => {
55    if (confirm('Es-tu sûr de vouloir déconnecter cet appareil ?')) {
56      setIsLoading(true);
57      setError('');
58
59      try {
60        const response = await fetch('/api/auth/revoke-session', {
61          method: 'POST',
62          headers: {
63            'Content-Type': 'application/json',
64          },
65          body: JSON.stringify({ sessionToken }),
66        });
67
68        if (!response.ok) {
69          throw new Error('Failed to invalidate session');
70        }
71
72        // Refresh the page
73        router.refresh();
74      } catch (e) {
75        setError(e.message);
76      } finally {
77        setIsLoading(false);
78      }
79    }
80  };
81
82  const formatDate = (dateString) => {
83    return new Date(dateString).toLocaleString();
84  };
85
86  return (
87    <div>
88      {error && (
89        <div className="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg">
90          {error}
91        </div>
92      )}
93
94      <div className="overflow-x-auto">
95        <table className="min-w-full divide-y divide-gray-200">
96          <thead className="bg-gray-50">
97            <tr>
98              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Appareil</th>
99              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dernière activité</th>
100              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expire</th>
101              <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
102            </tr>
103          </thead>
104          <tbody className="bg-white divide-y divide-gray-200">
105            {sessions.map((session) => (
106              <tr key={session.id}>
107                <td className="px-6 py-4 whitespace-nowrap">
108                  <div className="flex items-center">
109                    <div className="ml-4">
110                      <div className="text-sm font-medium text-gray-900">
111                        {session.isCurrent ? 'Appareil actuel' : 'Autre appareil'}
112                      </div>
113                    </div>
114                  </div>
115                </td>
116                <td className="px-6 py-4 whitespace-nowrap">
117                  <div className="text-sm text-gray-900">{formatDate(session.updatedAt)}</div>
118                </td>
119                <td className="px-6 py-4 whitespace-nowrap">
120                  <div className="text-sm text-gray-900">{formatDate(session.expires)}</div>
121                </td>
122                <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
123                  <button
124                    onClick={() => handleRevokeSession(session.sessionToken)}
125                    disabled={isLoading || session.isCurrent}
126                    className={`text-red-600 hover:text-red-900 ${(isLoading || session.isCurrent) ? 'opacity-50 cursor-not-allowed' : ''}`}
127                  >
128                    {session.isCurrent ? 'Session actuelle' : 'Déconnecter'}
129                  </button>
130                </td>
131              </tr>
132            ))}
133          </tbody>
134        </table>
135      </div>
136    </div>
137  );
138}
139
140// app/api/auth/revoke-session/route.ts
141import { NextResponse } from "next/server";
142import { getServerSession } from "next-auth/next";
143import { authOptions } from "@/app/api/auth/[...nextauth]/route";
144import { prisma } from "@/lib/prisma";
145
146export async function POST(request: Request) {
147  const session = await getServerSession(authOptions);
148
149  if (!session) {
150    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
151  }
152
153  const { sessionToken } = await request.json();
154
155  // Find the session, making sure it belongs to the logged-in user
156  const sessionToRevoke = await prisma.session.findFirst({
157    where: {
158      sessionToken,
159      userId: session.user.id,
160    },
161  });
162
163  if (!sessionToRevoke) {
164    return NextResponse.json(
165      { error: "Session not found or not owned by current user" },
166      { status: 404 }
167    );
168  }
169
170  // Remove the session
171  await prisma.session.delete({
172    where: {
173      id: sessionToRevoke.id,
174    },
175  });
176
177  return NextResponse.json({ success: true });
178}

Sécurité des jetons et des sessions

Lorsque tu mets en place la gestion des sessions et des jetons, garde à l'esprit plusieurs aspects clés de sécurité :

  1. Utilise des secrets solides - assure-toi que NEXTAUTH_SECRET est une longue chaîne de caractères aléatoires

  2. Sécurise les cookies - utilise les attributs :

    • HttpOnly : empêche l'accès au cookie depuis JavaScript
    • Secure : nécessite HTTPS
    • SameSite : protège contre les attaques CSRF
  3. Définis une durée de vie adaptée - les jetons/sessions ne doivent pas rester valides trop longtemps

  4. Limite les données dans le jeton - le jeton JWT doit contenir le minimum d'informations nécessaires

  5. Mets en place la révocation des sessions - permets aux utilisateurs de se déconnecter de tous leurs appareils

  6. Surveille les activités suspectes - détecte les tentatives de connexion répétées et les schémas d'accès inhabituels

  7. Rotation des secrets - change périodiquement les secrets utilisés pour signer les jetons

Tests des sessions et des jetons

Les tests sont un élément clé pour garantir la fiabilité des mécanismes de session et de jeton. Voici un exemple de test unitaire pour la fonctionnalité de rafraîchissement de jeton :

1// __tests__/refreshToken.test.ts
2import { refreshAccessToken } from '@/lib/auth';
3import fetchMock from 'jest-fetch-mock';
4
5fetchMock.enableMocks();
6
7beforeEach(() => {
8  fetchMock.resetMocks();
9});
10
11describe('refreshAccessToken', () => {
12  it('should refresh the token successfully', async () => {
13    // Prépare un mock de la réponse du serveur OAuth
14    fetchMock.mockResponseOnce(JSON.stringify({
15      access_token: 'new_access_token',
16      expires_in: 3600
17    }));
18
19    const originalToken = {
20      accessToken: 'old_access_token',
21      refreshToken: 'refresh_token',
22      accessTokenExpires: Date.now() - 1000, // Expired token
23    };
24
25    const refreshedToken = await refreshAccessToken(originalToken);
26
27    expect(refreshedToken.accessToken).toBe('new_access_token');
28    expect(refreshedToken.refreshToken).toBe('refresh_token'); // Should keep the old refresh token
29    expect(refreshedToken.accessTokenExpires).toBeGreaterThan(Date.now());
30    expect(fetchMock).toHaveBeenCalledTimes(1);
31  });
32
33  it('should handle refresh token errors', async () => {
34    // Simulate refresh error
35    fetchMock.mockRejectOnce(new Error('Failed to refresh token'));
36
37    const originalToken = {
38      accessToken: 'old_access_token',
39      refreshToken: 'refresh_token',
40      accessTokenExpires: Date.now() - 1000,
41    };
42
43    const result = await refreshAccessToken(originalToken);
44
45    expect(result.error).toBe('RefreshAccessTokenError');
46    expect(result.accessToken).toBe('old_access_token'); // Should keep the old token
47  });
48});

Bonnes pratiques dans le contexte de l'App Router de Next.js

Dans l'architecture App Router de Next.js, il est important de suivre ces principes lors de la mise en place de l'authentification :

  1. Utilise un middleware pour protéger des répertoires entiers ou des groupes de routes :
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  const token = await getToken({ req: request });
8
9  // Check if the user has access to the requested page
10  if (!token) {
11    const url = new URL('/auth/login', request.url);
12    url.searchParams.set('callbackUrl', request.nextUrl.pathname);
13    return NextResponse.redirect(url);
14  }
15
16  // Checking permissions based on path and role
17  if (request.nextUrl.pathname.startsWith('/admin') && token.role !== 'admin') {
18    return NextResponse.redirect(new URL('/dashboard', request.url));
19  }
20
21  return NextResponse.next();
22}
23
24export const config = {
25  matcher: [
26    '/dashboard/:path*',
27    '/settings/:path*',
28    '/admin/:path*',
29    '/api/protected/:path*'
30  ],
31};
  1. Utilise des Server Components quand c'est possible, pour réduire la quantité de code JavaScript côté client et améliorer la sécurité des opérations sensibles :
1// app/admin/dashboard/page.tsx (Server Component)
2import { getServerSession } from "next-auth/next";
3import { redirect } from "next/navigation";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
6export default async function AdminDashboard() {
7  const session = await getServerSession(authOptions);
8
9  if (!session || session.user.role !== 'admin') {
10    redirect('/');
11  }
12
13  // Fetch administrative data directly on the server
14  // without exposing API keys or other sensitive information to the client
15  const adminData = await fetchAdminData();
16
17  return (
18    <div>
19      <h1>Panneau d'administration</h1>
20      {/* Affiche les données d'administration */}
21    </div>
22  );
23}
  1. Utilise des Route Handlers pour sécuriser l'API :
1// app/api/protected/user-data/route.ts
2import { getServerSession } from "next-auth/next";
3import { NextResponse } from "next/server";
4import { authOptions } from "@/app/api/auth/[...nextauth]/route";
5
6export async function GET() {
7  const session = await getServerSession(authOptions);
8
9  if (!session) {
10    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
11  }
12
13  // Exécute les opérations protégées
14  const userData = await fetchUserData(session.user.id);
15
16  // Return data without sensitive information
17  const sanitizedData = sanitizeUserData(userData);
18
19  return NextResponse.json(sanitizedData);
20}

Résumé

Dans ce module, nous avons vu en détail comment gérer les sessions utilisateur et les jetons dans les applications Next.js :

  1. Nous avons découvert différentes stratégies de session dans NextAuth.js - JWT et base de données
  2. Nous avons appris à enrichir les jetons et les sessions avec nos propres données
  3. Nous avons compris comment sécuriser les cookies et configurer les paramètres de session
  4. Nous avons découvert les mécanismes de rafraîchissement des jetons pour les fournisseurs OAuth
  5. Nous avons mis en place la gestion de plusieurs sessions et la déconnexion d'appareils précis
  6. Nous avons abordé les meilleures pratiques de sécurité pour les jetons et les sessions

Une bonne gestion des sessions et des jetons est un aspect essentiel de la sécurité des applications web. Un système d'authentification bien conçu protège non seulement les données des utilisateurs, mais assure aussi une bonne expérience utilisateur, en permettant une connexion, une déconnexion et une gestion des accès faciles depuis différents appareils.

Dans le prochain module, nous nous occuperons de la mise en place d'un système de rôles et de permissions qui permettra une gestion plus fine de l'accès des utilisateurs aux différentes fonctionnalités de l'application.

Code de cette leçon : App.tsx
1import React, { useState, useEffect } from 'react';
2
3// Démo : Sessions utilisateurs et gestion des tokens
4interface SessionInfo {
5  id: string;
6  createdAt: string;
7  ipAddress: string;
8  userAgent: string;
9}
10
11function SessionTokenDemo() {
12  const [activeTab, setActiveTab] = useState<'session' | 'jwt' | 'refresh'>('session');
13  const [sessions, setSessions] = useState<SessionInfo[]>([]);
14  const [jwtPayload, setJwtPayload] = useState<any>(null);
15  const [tokenExpiry, setTokenExpiry] = useState(100);
16  const [isRefreshing, setIsRefreshing] = useState(false);
17
18  useEffect(() => {
19    if (jwtPayload && tokenExpiry > 0) {
20      const timer = setInterval(() => {
21        setTokenExpiry(prev => prev <= 1 ? 0 : prev - 1);
22      }, 100);
23      return () => clearInterval(timer);
24    }
25  }, [jwtPayload, tokenExpiry]);
26
27  const createSession = () => {
28    const session: SessionInfo = {
29      id: 'ses_' + Math.random().toString(36).substr(2, 9),
30      createdAt: new Date().toLocaleTimeString(),
31      ipAddress: '192.168.1.' + Math.floor(Math.random() * 255),
32      userAgent: ['Chrome/120', 'Firefox/121', 'Safari/17'][Math.floor(Math.random() * 3)],
33    };
34    setSessions(prev => [...prev, session]);
35  };
36
37  const generateJWT = () => {
38    const now = Math.floor(Date.now() / 1000);
39    setJwtPayload({ sub: 'user_001', email: 'jan@quantum.city', role: 'admin', iat: now, exp: now + 3600 });
40    setTokenExpiry(100);
41  };
42
43  const refreshToken = async () => {
44    setIsRefreshing(true);
45    await new Promise(r => setTimeout(r, 800));
46    const now = Math.floor(Date.now() / 1000);
47    setJwtPayload((prev: any) => prev ? { ...prev, iat: now, exp: now + 3600 } : null);
48    setTokenExpiry(100);
49    setIsRefreshing(false);
50  };
51
52  return (
53    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
54      <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Sessions et Tokens</h1>
55      <p style={{ color: '#8892b0', marginBottom: '24px' }}>Gestion des sessions utilisateurs et des tokens JWT</p>
56
57      <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
58        {[{ id: 'session' as const, label: 'Sessions' }, { id: 'jwt' as const, label: 'JWT' }, { id: 'refresh' as const, label: 'Refresh' }].map(tab => (
59          <button key={tab.id} onClick={() => setActiveTab(tab.id)} style={{
60            padding: '10px 20px', borderRadius: '8px', border: 'none', cursor: 'pointer',
61            background: activeTab === tab.id ? '#64ffda' : '#1a1a2e', color: activeTab === tab.id ? '#0f0f23' : '#8892b0', fontWeight: 'bold',
62          }}>
63            {tab.label}
64          </button>
65        ))}
66      </div>
67
68      {activeTab === 'session' && (
69        <div>
70          <button onClick={createSession} style={{ padding: '10px 20px', borderRadius: '8px', background: '#7c3aed', color: '#fff', border: 'none', cursor: 'pointer', marginBottom: '16px' }}>+ Créer une session</button>
71          {sessions.map(s => (
72            <div key={s.id} style={{ background: '#1a1a2e', padding: '12px', borderRadius: '8px', border: '1px solid #333', marginBottom: '8px', display: 'flex', justifyContent: 'space-between' }}>
73              <div>
74                <code style={{ color: '#64ffda' }}>{s.id}</code>
75                <div style={{ fontSize: '12px', color: '#8892b0' }}>IP: {s.ipAddress} | UA: {s.userAgent} | {s.createdAt}</div>
76              </div>
77              <button onClick={() => setSessions(prev => prev.filter(x => x.id !== s.id))} style={{ background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', cursor: 'pointer', fontSize: '12px' }}>Revoke</button>
78            </div>
79          ))}
80          {sessions.length === 0 && <p style={{ color: '#555' }}>Aucune session</p>}
81        </div>
82      )}
83
84      {activeTab === 'jwt' && (
85        <div>
86          <button onClick={generateJWT} style={{ padding: '10px 20px', borderRadius: '8px', background: '#f59e0b', color: '#0f0f23', border: 'none', cursor: 'pointer', marginBottom: '16px', fontWeight: 'bold' }}>Générer un JWT</button>
87          {jwtPayload && (
88            <div style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', border: '1px solid #333' }}>
89              <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px' }}>
90                <span>Token:</span>
91                <div style={{ flex: 1, height: '8px', background: '#333', borderRadius: '4px', overflow: 'hidden' }}>
92                  <div style={{ width: tokenExpiry + '%', height: '100%', background: tokenExpiry > 30 ? '#10b981' : tokenExpiry > 10 ? '#f59e0b' : '#ef4444', transition: 'width 0.1s' }} />
93                </div>
94                <span style={{ color: tokenExpiry > 30 ? '#10b981' : '#ef4444' }}>{tokenExpiry}%</span>
95              </div>
96              {tokenExpiry === 0 && <div style={{ color: '#ef4444', fontWeight: 'bold', marginBottom: '8px' }}>Token expiré !</div>}
97              <pre style={{ background: '#0f0f23', padding: '12px', borderRadius: '6px', fontSize: '13px', overflow: 'auto' }}>{JSON.stringify(jwtPayload, null, 2)}</pre>
98            </div>
99          )}
100        </div>
101      )}
102
103      {activeTab === 'refresh' && (
104        <div style={{ background: '#1a1a2e', padding: '24px', borderRadius: '8px', border: '1px solid #333' }}>
105          <h3 style={{ color: '#64ffda', marginTop: 0 }}>Refresh Token Flow</h3>
106          {['Le access token expire', 'Le client envoie le refresh token', 'Le serveur vérifie le refresh token', 'Nouveau access + refresh token'].map((step, i) => (
107            <div key={i} style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
108              <div style={{ width: '32px', height: '32px', borderRadius: '50%', background: '#7c3aed', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '14px', fontWeight: 'bold', flexShrink: 0 }}>{i + 1}</div>
109              <span>{step}</span>
110            </div>
111          ))}
112          <button onClick={refreshToken} disabled={isRefreshing} style={{ marginTop: '12px', padding: '10px 20px', borderRadius: '8px', background: isRefreshing ? '#333' : '#7c3aed', color: '#fff', border: 'none', cursor: isRefreshing ? 'default' : 'pointer' }}>
113            {isRefreshing ? 'Rafraîchissement...' : 'Simuler un Refresh'}
114          </button>
115        </div>
116      )}
117    </div>
118  );
119}
120
121export default SessionTokenDemo;

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. OAuth 2.0, c'est :

  2. 2. PKCE (clé de preuve pour l'échange de code) est utilisé dans :

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un composant ProtectedRoute utilisant useSession pour bloquer les accès non autorisés

  • Éditeur de code

    Crée un fournisseur Credentials dans NextAuth avec authentification email/mot de passe et hachage bcrypt

Articles utiles