Cours Next.js · Module 7: Authentification et autorisation

Mise en œuvre de NextAuth.js / Auth.js

17 min de lecture
Dans cette leçon14

NextAuth.js (désormais aussi appelé Auth.js) est une solution d'authentification complète pour les applications Next.js. Elle fournit une infrastructure prête à l'emploi pour gérer les sessions, les jetons, la connexion, l'inscription et l'intégration avec des fournisseurs d'authentification externes. Dans ce module, nous allons voir en détail la mise en œuvre de cet outil dans les applications Next.js.

Pourquoi utiliser NextAuth.js ?

NextAuth.js offre de nombreux avantages :

  1. Solution prête à l'emploi - pas besoin d'implémenter l'authentification de zéro
  2. Prise en charge de divers fournisseurs - intégration avec plus de 50 fournisseurs d'authentification
  3. Sécurité - applique les bonnes pratiques de sécurité
  4. Gestion des sessions - gère les sessions côté serveur comme côté client
  5. Flexibilité - peut être adapté aux besoins spécifiques de l'application
  6. Prise en charge de l'App Router - fonctionne avec la nouvelle architecture de Next.js
  7. Typage TypeScript étendu - offre un support complet des types

Installation et configuration de base

Commençons par installer NextAuth.js :

1npm install next-auth@latest
2# ou
3yarn add next-auth@latest
4# ou
5pnpm add next-auth@latest

Configuration de base pour l'App Router

Pour configurer NextAuth.js dans l'architecture App Router, nous devons créer un fichier qui gère la route API d'authentification :

1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from "next-auth";
3import GithubProvider from "next-auth/providers/github";
4
5export const authOptions = {
6  providers: [
7    GithubProvider({
8      clientId: process.env.GITHUB_ID!,
9      clientSecret: process.env.GITHUB_SECRET!
10    })
11  ],
12  // Options de configuration supplémentaires...
13};
14
15const handler = NextAuth(authOptions);
16export { handler as GET, handler as POST };

Ce fichier crée une route API dynamique qui gère tous les points d'authentification nécessaires.

Ajouter le composant SessionProvider

Pour pouvoir utiliser le hook useSession dans les composants côté client, nous devons ajouter SessionProvider à notre application. Le mieux est de le faire dans un fichier Provider ou directement dans le layout principal :

1// app/providers.tsx
2'use client';
3
4import { SessionProvider } from "next-auth/react";
5import { ReactNode } from "react";
6
7export function Providers({ children }: { children: ReactNode }) {
8  return <SessionProvider>{children}</SessionProvider>;
9}
10
11// app/layout.tsx
12import { Providers } from "./providers";
13
14export default function RootLayout({
15  children,
16}: {
17  children: React.ReactNode;
18}) {
19  return (
20    <html lang="en">
21      <body>
22        <Providers>{children}</Providers>
23      </body>
24    </html>
25  );
26}

Configuration des variables d'environnement

NextAuth.js nécessite plusieurs variables d'environnement. Crée ou mets à jour le fichier .env.local à la racine du projet :

1# Configuration de base pour NextAuth.js
2NEXTAUTH_URL=http://localhost:3000
3NEXTAUTH_SECRET=ta_cle_secrete_pour_le_chiffrement_jwt
4
5# Données d'authentification GitHub (exemple)
6GITHUB_ID=ton_github_client_id
7GITHUB_SECRET=ton_github_client_secret

En production :

  • NEXTAUTH_URL doit être l'URL complète de ton application
  • NEXTAUTH_SECRET doit être une longue chaîne de caractères aléatoires. Tu peux la générer avec la commande openssl rand -base64 32.

Configuration de différents fournisseurs d'authentification

NextAuth.js prend en charge de nombreux fournisseurs d'authentification. Voyons comment en configurer quelques-uns parmi les plus populaires :

1. Authentification avec GitHub

1import GithubProvider from "next-auth/providers/github";
2
3export const authOptions = {
4  providers: [
5    GithubProvider({
6      clientId: process.env.GITHUB_ID!,
7      clientSecret: process.env.GITHUB_SECRET!,
8      // Opcjonalne parametry
9      scope: "read:user user:email", // Specifies access scope
10      profile(profile) {
11        // Customizing user profile
12        return {
13          id: profile.id.toString(),
14          name: profile.name || profile.login,
15          email: profile.email,
16          image: profile.avatar_url,
17          // Dodatkowe pola
18          username: profile.login,
19        };
20      },
21    }),
22  ],
23  // Other configuration options...
24};

2. Authentification avec Google

1import GoogleProvider from "next-auth/providers/google";
2
3export const authOptions = {
4  providers: [
5    GoogleProvider({
6      clientId: process.env.GOOGLE_CLIENT_ID!,
7      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
8      // Opcjonalne parametry
9      authorization: {
10        params: Promise<{
11          prompt: "consent",
12          access_type: "offline",
13          response_type: "code"
14        }>
15      }
16    }),
17  ],
18  // Other configuration options...
19};

3. Authentification par identifiants (e-mail/mot de passe)

1import CredentialsProvider from "next-auth/providers/credentials";
2import { compare } from "bcrypt";
3import { prisma } from "@/lib/prisma"; // We assume you have a configured Prisma instance
4
5export const authOptions = {
6  providers: [
7    CredentialsProvider({
8      // Name displayed on the login page
9      name: "Credentials",
10      // Login form field configuration
11      credentials: {
12        email: { label: "Email", type: "email" },
13        password: { label: "Password", type: "password" }
14      },
15      async authorize(credentials) {
16        // Check if credentials exist
17        if (!credentials?.email || !credentials?.password) {
18          return null;
19        }
20
21        try {
22          // Find the user in the database
23          const user = await prisma.user.findUnique({
24            where: { email: credentials.email },
25          });
26
27          if (!user || !user.password) {
28            return null;
29          }
30
31          // Check if the password is correct
32          const isPasswordValid = await compare(
33            credentials.password,
34            user.password
35          );
36
37          if (!isPasswordValid) {
38            return null;
39          }
40
41          // Return the user object (without password)
42          return {
43            id: user.id,
44            name: user.name,
45            email: user.email,
46            role: user.role,
47            // Other fields you want to include
48          };
49        } catch (error) {
50          console.error("Authentication error:", error);
51          return null;
52        }
53      },
54    }),
55  ],
56  // Other configuration options...
57};

4. Authentification depuis plusieurs fournisseurs

Tu peux combiner plusieurs fournisseurs d'authentification :

1export const authOptions = {
2  providers: [
3    GithubProvider({
4      clientId: process.env.GITHUB_ID!,
5      clientSecret: process.env.GITHUB_SECRET!,
6    }),
7    GoogleProvider({
8      clientId: process.env.GOOGLE_CLIENT_ID!,
9      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
10    }),
11    CredentialsProvider({
12      // Configuration...
13      async authorize(credentials) {
14        // Logique d'authentification...
15      },
16    }),
17    // Tu peux ajouter d'autres fournisseurs...
18  ],
19  // Autres options de configuration...
20};

Configuration des options de session et JWT

NextAuth.js propose différentes stratégies de session :

1export const authOptions = {
2  // Provider configuration...
3
4  session: {
5    // Choose strategy: 'jwt' or 'database'
6    strategy: 'jwt',
7
8    // Maximum session lifetime (in seconds)
9    maxAge: 30 * 24 * 60 * 60, // 30 dni
10
11    // Session update on each request, extending its validity
12    updateAge: 24 * 60 * 60, // 24 godziny
13  },
14
15  // Configuration JWT (uniquement pour la stratégie 'jwt')
16  jwt: {
17    // Secret key used to sign JWT tokens
18    // By default uses NEXTAUTH_SECRET
19    secret: process.env.JWT_SECRET,
20
21    // You can customize JWT encoder/decoder
22    // encode: async ({ secret, token, maxAge }) => {},
23    // decode: async ({ secret, token }) => {},
24  },
25};

Pages d'authentification personnalisées

Par défaut, NextAuth.js fournit des pages de connexion simples, mais tu peux les remplacer par les tiennes :

1export const authOptions = {
2  // Provider configuration...
3
4  pages: {
5    signIn: '/auth/login',
6    signOut: '/auth/logout',
7    error: '/auth/error', // Authentication error (e.g. invalid credentials)
8    verifyRequest: '/auth/verify-request', // Utilisé pour les Magic Links
9    newUser: '/auth/new-user' // Page affichée après la première inscription
10  },
11};

Il faut ensuite créer ces pages dans ton application. Voici un exemple de page de connexion :

1// app/auth/login/page.tsx
2'use client';
3
4import { signIn, useSession } from 'next-auth/react';
5import { useRouter, useSearchParams } from 'next/navigation';
6import { useState, useEffect } from 'react';
7
8export default function LoginPage() {
9  const router = useRouter();
10  const searchParams = useSearchParams();
11  const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
12  const { status } = useSession();
13
14  const [email, setEmail] = useState('');
15  const [password, setPassword] = useState('');
16  const [error, setError] = useState('');
17  const [isLoading, setIsLoading] = useState(false);
18
19  // Redirect the logged-in user
20  useEffect(() => {
21    if (status === 'authenticated') {
22      router.push(callbackUrl);
23    }
24  }, [status, router, callbackUrl]);
25
26  // GitHub login handling
27  const handleGitHubSignIn = () => {
28    setIsLoading(true);
29    signIn('github', { callbackUrl });
30  };
31
32  // Google login handling
33  const handleGoogleSignIn = () => {
34    setIsLoading(true);
35    signIn('google', { callbackUrl });
36  };
37
38  // Credentials login handling
39  const handleSubmit = async (e: React.FormEvent) => {
40    e.preventDefault();
41    setIsLoading(true);
42    setError('');
43
44    try {
45      const result = await signIn('credentials', {
46        redirect: false,
47        email,
48        password,
49      });
50
51      if (result?.error) {
52        setError('E-mail ou mot de passe invalide');
53        setIsLoading(false);
54      } else {
55        router.push(callbackUrl);
56      }
57    } catch (error) {
58      setError('Une erreur est survenue lors de la connexion');
59      setIsLoading(false);
60    }
61  };
62
63  if (status === 'loading') {
64    return <div>Chargement...</div>;
65  }
66
67  return (
68    <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-md">
69      <h1 className="text-2xl font-bold mb-6 text-center">Se connecter</h1>
70
71      {error && (
72        <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
73          {error}
74        </div>
75      )}
76
77      <div className="space-y-4 mb-6">
78        <button
79          type="button"
80          onClick={handleGitHubSignIn}
81          disabled={isLoading}
82          className="w-full flex items-center justify-center gap-2 p-2 border border-gray-300 rounded-md hover:bg-gray-50"
83        >
84          <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
85            {/* Logo GitHub */}
86            <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />
87          </svg>
88          Continuer avec GitHub
89        </button>
90
91        <button
92          type="button"
93          onClick={handleGoogleSignIn}
94          disabled={isLoading}
95          className="w-full flex items-center justify-center gap-2 p-2 border border-gray-300 rounded-md hover:bg-gray-50"
96        >
97          <svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
98            {/* Logo Google */}
99            <path d="M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12-5.385 12-12 12zm0-22.5C6.21 1.5 1.5 6.21 1.5 12S6.21 22.5 12 22.5 22.5 17.79 22.5 12 17.79 1.5 12 1.5zm-5.25 9v3h3v3h3v-3h3v-3h-3v-3h-3v3h-3z" />
100          </svg>
101          Continuer avec Google
102        </button>
103      </div>
104
105      <div className="relative my-6">
106        <div className="absolute inset-0 flex items-center">
107          <div className="w-full border-t border-gray-300"></div>
108        </div>
109        <div className="relative flex justify-center text-sm">
110          <span className="px-2 bg-white text-gray-500">
111            ou connecte-toi avec ton e-mail
112          </span>
113        </div>
114      </div>
115
116      <form onSubmit={handleSubmit} className="space-y-4">
117        <div>
118          <label htmlFor="email" className="block text-sm font-medium mb-1">
119            Email
120          </label>
121          <input
122            id="email"
123            type="email"
124            value={email}
125            onChange={(e) => setEmail(e.target.value)}
126            required
127            className="w-full px-3 py-2 border border-gray-300 rounded-md"
128          />
129        </div>
130
131        <div>
132          <label htmlFor="password" className="block text-sm font-medium mb-1">
133            Mot de passe
134          </label>
135          <input
136            id="password"
137            type="password"
138            value={password}
139            onChange={(e) => setPassword(e.target.value)}
140            required
141            className="w-full px-3 py-2 border border-gray-300 rounded-md"
142          />
143        </div>
144
145        <button
146          type="submit"
147          disabled={isLoading}
148          className="w-full bg-blue-500 text-white py-2 px-4 rounded-md hover:bg-blue-600 disabled:opacity-50"
149        >
150          {isLoading ? 'Connexion en cours...' : 'Se connecter'}
151        </button>
152      </form>
153    </div>
154  );
155}

Callbacks et événements

NextAuth.js permet de réagir à différents événements du cycle de vie de l'authentification grâce aux callbacks :

1export const authOptions = {
2  // Provider configuration...
3
4  callbacks: {
5    // Called when creating a session
6    async session({ session, token }) {
7      // Ajoute les données du jeton à la session
8      if (token.sub && session.user) {
9        session.user.id = token.sub;
10      }
11
12      // Add custom fields, e.g. user role
13      if (token.role && session.user) {
14        session.user.role = token.role as string;
15      }
16
17      return session;
18    },
19
20    // Called when creating a JWT
21    async jwt({ token, user, account, profile }) {
22      // Add user data on first login
23      if (user) {
24        token.role = user.role;
25        // Autres champs personnalisés...
26      }
27
28      return token;
29    },
30
31    // Called after login
32    async signIn({ user, account, profile, email, credentials }) {
33      // Check if the user has access to the application
34      return true; // Return false to deny access
35    },
36
37    // Called during redirect
38    async redirect({ url, baseUrl }) {
39      // Redirect rules
40      if (url.startsWith(baseUrl)) {
41        return url;
42      }
43      // By default redirect to the base page
44      return baseUrl;
45    },
46  },
47
48  // Événements
49  events: {
50    async signIn(message) {
51      // Called after login
52      console.log('User signed in:', message);
53    },
54    async signOut(message) {
55      // Called after logout
56      console.log('User signed out:', message);
57    },
58    async createUser(message) {
59      // Called after user creation
60      console.log('User created:', message);
61    },
62    async linkAccount(message) {
63      // Called after account linking
64      console.log('Account linked:', message);
65    },
66    async session(message) {
67      // Called after session creation
68      console.log('Session created/updated:', message);
69    },
70  },
71};

Étendre les types pour TypeScript

Si tu utilises TypeScript, tu peux étendre les types de NextAuth.js pour y inclure des champs personnalisés :

1// In the type extension file (e.g. types/next-auth.d.ts)
2import 'next-auth';
3import 'next-auth/jwt';
4
5declare module 'next-auth' {
6  interface Session {
7    user: {
8      id: string;
9      name?: string | null;
10      email?: string | null;
11      image?: string | null;
12      role?: string;
13      // Ajoute d'autres champs personnalisés...
14    };
15  }
16
17  interface User {
18    id: string;
19    name?: string | null;
20    email?: string | null;
21    image?: string | null;
22    role?: string;
23    // Ajouter d'autres champs personnalisés...
24  }
25}
26
27declare module 'next-auth/jwt' {
28  interface JWT {
29    role?: string;
30    // Ajouter d'autres champs personnalisés...
31  }
32}

Adaptateurs de base de données

Pour utiliser la stratégie de session basée sur la base de données ou pour stocker les comptes utilisateurs, NextAuth.js propose des adaptateurs pour différentes bases de données :

Configuration avec Prisma

1import { PrismaAdapter } from "@auth/prisma-adapter";
2import { PrismaClient } from "@prisma/client";
3
4const prisma = new PrismaClient();
5
6export const authOptions = {
7  adapter: PrismaAdapter(prisma),
8  providers: [
9    // Provider configuration...
10  ],
11  // Other options...
12};

Le schéma Prisma doit contenir les modèles appropriés :

1// prisma/schema.prisma
2model Account {
3  id                String  @id @default(cuid())
4  userId            String
5  type              String
6  provider          String
7  providerAccountId String
8  refresh_token     String? @db.Text
9  access_token      String? @db.Text
10  expires_at        Int?
11  token_type        String?
12  scope             String?
13  id_token          String? @db.Text
14  session_state     String?
15
16  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
17
18  @@unique([provider, providerAccountId])
19}
20
21model Session {
22  id           String   @id @default(cuid())
23  sessionToken String   @unique
24  userId       String
25  expires      DateTime
26  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
27}
28
29model User {
30  id            String    @id @default(cuid())
31  name          String?
32  email         String?   @unique
33  emailVerified DateTime?
34  image         String?
35  password      String?
36  role          String?   @default("user")
37  accounts      Account[]
38  sessions      Session[]
39}
40
41model VerificationToken {
42  identifier String
43  token      String   @unique
44  expires    DateTime
45
46  @@unique([identifier, token])
47}

Autres adaptateurs disponibles

NextAuth.js propose des adaptateurs pour de nombreuses bases de données :

  • Adaptateurs SQL : PostgreSQL, MySQL, SQLite, SQL Server
  • Adaptateurs non relationnels : MongoDB, DynamoDB
  • ORM/ODM : Prisma, Mongoose, Typeorm, Sequelize, DrizzleORM
  • Autres : Firebase, Supabase, Kysely, Planetscale, Xata

Utiliser l'authentification dans l'application

1. Récupérer les données de session côté client

Après avoir configuré NextAuth.js, tu peux utiliser le hook useSession dans les composants client :

1'use client';
2
3import { useSession } from 'next-auth/react';
4import { redirect } from 'next/navigation';
5
6export default function Dashboard() {
7  const { data: session, status } = useSession({
8    required: true,
9    onUnauthenticated() {
10      redirect('/auth/login?callbackUrl=/dashboard');
11    },
12  });
13
14  if (status === 'loading') {
15    return <div>Chargement...</div>;
16  }
17
18  return (
19    <div>
20      <h1>Dashboard</h1>
21      <p>Bonjour, {session.user.name} !</p>
22      <p>Ton e-mail : {session.user.email}</p>
23      {session.user.role === 'admin' && (
24        <div>
25          <h2>Panneau d'administration</h2>
26          {/* Contenu réservé aux administrateurs */}
27        </div>
28      )}
29    </div>
30  );
31}

2. Récupérer les données de session côté serveur

Dans les composants rendus côté serveur, tu peux utiliser getServerSession :

1// app/profile/page.tsx
2import { getServerSession } from 'next-auth/next';
3import { redirect } from 'next/navigation';
4import { authOptions } from '../api/auth/[...nextauth]/route';
5
6export default async function ProfilePage() {
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</h1>
16      <p>Bonjour, {session.user.name} !</p>
17      <p>Ton e-mail : {session.user.email}</p>
18      {/* Reste du contenu du profil */}
19    </div>
20  );
21}

3. Gérer l'état d'authentification

NextAuth.js fournit des fonctions pour gérer l'état d'authentification :

1'use client';
2
3import { signIn, signOut, useSession } from 'next-auth/react';
4
5export function AuthButton() {
6  const { data: session } = useSession();
7
8  if (session) {
9    return (
10      <button
11        onClick={() => signOut({ callbackUrl: '/' })}
12        className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
13      >
14        Se déconnecter
15      </button>
16    );
17  }
18
19  return (
20    <button
21      onClick={() => signIn()}
22      className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
23    >
24      Se connecter
25    </button>
26  );
27}

Adapter l'interface à NextAuth.js

Nous pouvons créer un composant qui affiche différents éléments d'interface selon l'état d'authentification :

1'use client';
2
3import { useSession } from 'next-auth/react';
4import Link from 'next/link';
5import { AuthButton } from './AuthButton';
6
7export function Navbar() {
8  const { data: session, status } = useSession();
9  const isLoading = status === 'loading';
10
11  return (
12    <nav className="bg-white shadow-md p-4">
13      <div className="container mx-auto flex justify-between items-center">
14        <Link href="/" className="text-xl font-bold">
15          MojaAplikacja
16        </Link>
17
18        <div className="flex items-center space-x-4">
19          <Link href="/" className="hover:text-blue-500">
20            Accueil
21          </Link>
22
23          {session ? (
24            <>
25              <Link href="/dashboard" className="hover:text-blue-500">
26                Dashboard
27              </Link>
28              <Link href="/profile" className="hover:text-blue-500">
29                Profil
30              </Link>
31              {session.user.role === 'admin' && (
32                <Link href="/admin" className="hover:text-blue-500">
33                  Admin
34                </Link>
35              )}
36            </>
37          ) : (
38            <Link href="/features" className="hover:text-blue-500">
39              Fonctionnalités
40            </Link>
41          )}
42
43          {!isLoading && (
44            <div className="flex items-center space-x-2">
45              {session && (
46                <div className="flex items-center">
47                  {session.user.image && (
48                    <img
49                      src={session.user.image}
50                      alt={session.user.name || 'User'}
51                      className="w-8 h-8 rounded-full mr-2"
52                    />
53                  )}
54                  <span className="text-sm font-medium">
55                    {session.user.name}
56                  </span>
57                </div>
58              )}
59              <AuthButton />
60            </div>
61          )}
62        </div>
63      </div>
64    </nav>
65  );
66}

Authentification avec les API Routes

NextAuth.js peut aussi servir à sécuriser les points d'API :

1// app/api/protected-data/route.ts
2import { getServerSession } from 'next-auth/next';
3import { NextResponse } from 'next/server';
4import { authOptions } from '../auth/[...nextauth]/route';
5
6export async function GET() {
7  const session = await getServerSession(authOptions);
8
9  if (!session) {
10    return NextResponse.json(
11      { message: 'Unauthorized access' },
12      { status: 401 }
13    );
14  }
15
16  // Check permissions based on role (optional)
17  if (session.user.role !== 'admin') {
18    return NextResponse.json(
19      { message: 'Insufficient permissions' },
20      { status: 403 }
21    );
22  }
23
24  // Data available only to logged-in users (and admins if we check roles)
25  return NextResponse.json({
26    message: 'Succès',
27    data: {
28      secretContent: 'Ces données sont protégées',
29      timestamp: new Date().toISOString()
30    }
31  });
32}

Bonnes pratiques pour la mise en œuvre de NextAuth.js

  1. La sécurité avant tout

    • Utilise HTTPS en production
    • Génère un NEXTAUTH_SECRET solide
    • Ne stocke jamais de données sensibles dans les jetons JWT
  2. Optimisation des performances

    • Utilise des stratégies de session adaptées à tes besoins
    • Envisage la stratégie JWT pour une meilleure évolutivité
  3. Gestion des erreurs

    • Implémente des pages d'erreur personnalisées
    • Journalise les erreurs d'authentification pour les analyser
  4. Expérience utilisateur

    • Conçois des formulaires de connexion conviviaux
    • Permets de basculer facilement entre les fournisseurs
  5. Tests

    • Teste différents scénarios d'authentification
    • Vérifie comment l'application réagit aux sessions expirées

Scénarios avancés

1. Authentification multi-étapes

Dans certains cas, tu voudras peut-être mettre en place une authentification en plusieurs étapes (par ex. mot de passe + code SMS) :

1// Implémente un CredentialsProvider personnalisé
2CredentialsProvider({
3  name: 'TwoFactor',
4  credentials: {
5    email: { label: "Email", type: "email" },
6    password: { label: "Mot de passe", type: "password" },
7    code: { label: "Code de vérification", type: "text" }
8  },
9  async authorize(credentials) {
10    if (!credentials?.email || !credentials?.password) {
11      return null;
12    }
13
14    // First step: verify password
15    const user = await prisma.user.findUnique({
16      where: { email: credentials.email }
17    });
18
19    if (!user || !await compare(credentials.password, user.password)) {
20      return null;
21    }
22
23    // Second step: verify the verification code
24    if (credentials.code) {
25      const isValidCode = await verifyTwoFactorCode(user.id, credentials.code);
26
27      if (!isValidCode) {
28        return null;
29      }
30    } else {
31      // If the code was not provided and is required
32      if (user.twoFactorEnabled) {
33        // Sending the code to email/phone
34        await sendTwoFactorCode(user);
35
36        // Return a special object so App Router knows we need a code
37        throw new Error('TwoFactorRequired');
38      }
39    }
40
41    // Authentication successful
42    return {
43      id: user.id,
44      email: user.email,
45      name: user.name,
46      role: user.role
47    };
48  }
49})

2. Implémenter une logique d'inscription personnalisée

1// app/api/register/route.ts
2import { NextResponse } from 'next/server';
3import { hash } from 'bcrypt';
4import { prisma } from '@/lib/prisma';
5import { z } from 'zod';
6
7// Schéma de validation
8const userSchema = z.object({
9  name: z.string().min(2, 'Le prénom doit comporter au moins 2 caractères'),
10  email: z.string().email('Adresse e-mail invalide'),
11  password: z.string().min(8, 'Le mot de passe doit comporter au moins 8 caractères')
12});
13
14export async function POST(request: Request) {
15  try {
16    const body = await request.json();
17
18    // Validate data
19    const result = userSchema.safeParse(body);
20
21    if (!result.success) {
22      return NextResponse.json(
23        { message: result.error.errors[0].message },
24        { status: 400 }
25      );
26    }
27
28    const { name, email, password } = result.data;
29
30    // Check if user already exists
31    const existingUser = await prisma.user.findUnique({
32      where: { email }
33    });
34
35    if (existingUser) {
36      return NextResponse.json(
37        { message: 'Un utilisateur avec cette adresse e-mail existe déjà' },
38        { status: 409 }
39      );
40    }
41
42    // Hacher le mot de passe
43    const hashedPassword = await hash(password, 10);
44
45    // Créer le nouvel utilisateur
46    const user = await prisma.user.create({
47      data: {
48        name,
49        email,
50        password: hashedPassword,
51        role: 'user' // Rôle par défaut
52      }
53    });
54
55    // Ne pas renvoyer le mot de passe dans la réponse
56    const { password: _, ...userWithoutPassword } = user;
57
58    return NextResponse.json(
59      { message: 'Inscription effectuée avec succès', user: userWithoutPassword },
60      { status: 201 }
61    );
62  } catch (error) {
63    console.error('Registration error:', error);
64    return NextResponse.json(
65      { message: 'Une erreur serveur est survenue' },
66      { status: 500 }
67    );
68  }
69}

Résumé

NextAuth.js (Auth.js) est un outil complet pour mettre en œuvre l'authentification dans les applications Next.js. Il offre :

  1. Flexibilité - prend en charge plusieurs stratégies d'authentification
  2. Intégration - s'intègre facilement à divers fournisseurs
  3. Sécurité - applique les meilleures pratiques de sécurité
  4. Évolutivité - fonctionne avec différentes bases de données et architectures
  5. Extensibilité - peut être adapté à des besoins spécifiques

Grâce à NextAuth.js, tu peux rapidement mettre en place un système d'authentification complet dans ton application Next.js, tout en gagnant du temps et en réduisant le risque d'erreurs liées à la sécurité.

Dans les modules suivants, nous aborderons des aspects plus avancés de l'authentification, notamment les stratégies d'authentification, la gestion des sessions et des jetons, et la mise en place des rôles et des permissions.

Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Démo : NextAuth.js / Auth.js - configuration et providers
4interface AuthConfig {
5  provider: string;
6  callbackUrl: string;
7  strategy: 'jwt' | 'database';
8  maxAge: number;
9}
10
11const defaultProviders: AuthConfig[] = [
12  { provider: 'credentials', callbackUrl: '/dashboard', strategy: 'jwt', maxAge: 3600 },
13  { provider: 'google', callbackUrl: '/dashboard', strategy: 'jwt', maxAge: 86400 },
14  { provider: 'github', callbackUrl: '/dashboard', strategy: 'database', maxAge: 604800 },
15];
16
17function AuthConfigBuilder() {
18  const [configs, setConfigs] = useState<AuthConfig[]>(defaultProviders);
19  const [newProvider, setNewProvider] = useState('');
20  const [strategy, setStrategy] = useState<'jwt' | 'database'>('jwt');
21  const [showCode, setShowCode] = useState(false);
22
23  const addProvider = () => {
24    if (!newProvider.trim()) return;
25    setConfigs(prev => [...prev, {
26      provider: newProvider.trim(),
27      callbackUrl: '/dashboard',
28      strategy,
29      maxAge: strategy === 'jwt' ? 3600 : 86400,
30    }]);
31    setNewProvider('');
32  };
33
34  const removeProvider = (index: number) => {
35    setConfigs(prev => prev.filter((_, i) => i !== index));
36  };
37
38  // Mots-clés dans des variables : l’aperçu de l’éditeur retire les lignes import/export du code, même dans le texte du template
39  const kw = { imp: 'import', exp: 'export' };
40  const generatedCode = `// app/api/auth/[...nextauth]/route.ts
41${kw.imp} NextAuth from 'next-auth';
42${configs.map(c => {
43  if (c.provider === 'credentials') return kw.imp + " CredentialsProvider from 'next-auth/providers/credentials';";
44  return kw.imp + " " + c.provider.charAt(0).toUpperCase() + c.provider.slice(1) + "Provider from 'next-auth/providers/" + c.provider + "';";
45}).join('\n')}
46
47${kw.exp} const authOptions = {
48  providers: [
49${configs.map(c => "    // " + c.provider + " provider\n    // strategy: " + c.strategy).join(',\n')}
50  ],
51  session: {
52    strategy: '${configs[0]?.strategy || 'jwt'}',
53    maxAge: ${configs[0]?.maxAge || 3600},
54  },
55  callbacks: {
56    async jwt({ token, user }) {
57      if (user) token.role = user.role;
58      return token;
59    },
60    async session({ session, token }) {
61      session.user.role = token.role;
62      return session;
63    },
64  },
65};`;
66
67  return (
68    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
69      <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>NextAuth.js / Auth.js Configuration</h1>
70      <p style={{ color: '#8892b0', marginBottom: '24px' }}>Configurateur interactif NextAuth.js - ajoute des providers et génère le code</p>
71
72      <div style={{ display: 'flex', gap: '12px', marginBottom: '24px', flexWrap: 'wrap' }}>
73        <input
74          value={newProvider}
75          onChange={e => setNewProvider(e.target.value)}
76          placeholder="Nom du provider (ex. discord)"
77          style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid #333', background: '#1a1a2e', color: '#fff', flex: 1, minWidth: '200px' }}
78        />
79        <select
80          value={strategy}
81          onChange={e => setStrategy(e.target.value as 'jwt' | 'database')}
82          style={{ padding: '8px 12px', borderRadius: '6px', border: '1px solid #333', background: '#1a1a2e', color: '#fff' }}
83        >
84          <option value="jwt">JWT Strategy</option>
85          <option value="database">Database Strategy</option>
86        </select>
87        <button onClick={addProvider} style={{ padding: '8px 16px', borderRadius: '6px', background: '#64ffda', color: '#0f0f23', border: 'none', cursor: 'pointer', fontWeight: 'bold' }}>
88          + Ajouter un Provider
89        </button>
90      </div>
91
92      <div style={{ display: 'grid', gap: '12px', marginBottom: '24px' }}>
93        {configs.map((config, i) => (
94          <div key={i} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', background: '#1a1a2e', borderRadius: '8px', border: '1px solid #333' }}>
95            <div>
96              <strong style={{ color: '#64ffda' }}>{config.provider}</strong>
97              <span style={{ color: '#8892b0', marginLeft: '12px', fontSize: '14px' }}>
98                Strategy: {config.strategy} | MaxAge: {config.maxAge}s
99              </span>
100            </div>
101            <button onClick={() => removeProvider(i)} style={{ background: '#ef4444', color: '#fff', border: 'none', borderRadius: '4px', padding: '4px 8px', cursor: 'pointer' }}>
102              Supprimer
103            </button>
104          </div>
105        ))}
106      </div>
107
108      <button
109        onClick={() => setShowCode(!showCode)}
110        style={{ padding: '10px 20px', borderRadius: '6px', background: showCode ? '#333' : '#7c3aed', color: '#fff', border: 'none', cursor: 'pointer', marginBottom: '16px' }}
111      >
112        {showCode ? 'Masquer le code' : 'Générer le code de configuration'}
113      </button>
114
115      {showCode && (
116        <pre style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', overflow: 'auto', fontSize: '13px', lineHeight: 1.5, border: '1px solid #333' }}>
117          <code style={{ color: '#e0e0e0' }}>{generatedCode}</code>
118        </pre>
119      )}
120    </div>
121  );
122}
123
124export default AuthConfigBuilder;

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. L'authentification signifie :

  2. 2. L'autorisation signifie :

Tâches pratiques dans le jeu

  • Éditeur de code

    Construis une interface avec des sections protégées nécessitant une connexion

  • Tri vertical

    Place les éléments dans le bon ordre : import 'server-only' → export async → fu

  • Clique dans l'ordre

    Place les éléments dans le bon ordre : import → NextAuth → from

  • Éditeur de code

    Crée un composant de connexion avec authentification JWT

Articles utiles