Cours Next.js · Module 7: Authentification et autorisation

Connecter des fournisseurs (Google, GitHub, Facebook)

16 min de lecture
Dans cette leçon6

Dans les leçons précédentes, nous avons vu les bases de l'authentification dans les applications Next.js et différentes stratégies d'autorisation. Nous allons maintenant nous concentrer sur la mise en œuvre concrète de l'authentification avec des fournisseurs externes populaires - Google, GitHub et Facebook. Cette méthode est très répandue, car elle offre aux utilisateurs une connexion rapide et pratique, sans avoir à créer un nouveau compte ni à retenir un mot de passe de plus.

Pourquoi ajouter des fournisseurs d'authentification externes ?

Se connecter avec un compte Google, GitHub ou Facebook apporte plusieurs avantages importants, à la fois pour les utilisateurs et pour nous, les développeurs de l'application :

  1. Meilleure expérience utilisateur - processus de connexion plus rapide et plus pratique, sans avoir à inventer et mémoriser de nouveaux identifiants
  2. Taux d'inscription plus élevé - moins d'inscriptions abandonnées grâce au processus simplifié
  3. Plus de sécurité - des fournisseurs comme Google ou Facebook utilisent des mécanismes avancés de protection des comptes
  4. Informations supplémentaires - possibilité d'obtenir des données complémentaires sur l'utilisateur (avec son consentement), comme la photo de profil ou les coordonnées
  5. Authentification multifacteur - de nombreux fournisseurs proposent déjà des mécanismes 2FA/MFA intégrés

Passons donc à la mise en œuvre concrète de la connexion avec ces fournisseurs populaires dans une application Next.js.

Configuration OAuth pour les fournisseurs

Avant de commencer à coder, nous devons créer les applications OAuth correspondantes chez chaque fournisseur afin d'obtenir les clés nécessaires.

Google OAuth

  1. Va sur Google Cloud Console
  2. Crée un nouveau projet ou sélectionne-en un existant
  3. Va dans « APIs & Services » > « Credentials »
  4. Clique sur « Create Credentials » > « OAuth client ID »
  5. Choisis le type d'application (Web application)
  6. Ajoute un nom et les URI de redirection autorisés :
    • http://localhost:3000/api/auth/callback/google (pour l'environnement de développement)
    • https://ton-domaine.com/api/auth/callback/google (pour la production)
  7. Note l'identifiant client (Client ID) et la clé secrète client (Client Secret)

GitHub OAuth

  1. Connecte-toi sur GitHub
  2. Va dans Settings > Developer settings > OAuth Apps
  3. Clique sur « New OAuth App »
  4. Remplis le formulaire :
    • Application name : le nom de ton application
    • Homepage URL : par ex. http://localhost:3000
    • Authorization callback URL : http://localhost:3000/api/auth/callback/github
  5. Enregistre l'application
  6. Génère un nouveau Client Secret et conserve-le avec le Client ID

Facebook OAuth

  1. Va sur Facebook Developers
  2. Crée une nouvelle application (choisis « For Everything Else » comme type)
  3. Dans le panneau de l'application, ajoute « Facebook Login » et choisis « Web »
  4. Dans les paramètres, ajoute les domaines autorisés :
    • localhost (pour l'environnement de développement)
    • ton-domaine.com (pour la production)
  5. Dans les paramètres d'authentification, ajoute la redirection OAuth :
    • http://localhost:3000/api/auth/callback/facebook
  6. Note l'App ID et l'App Secret

Mise en œuvre de l'authentification avec NextAuth/Auth.js

Maintenant que nous avons les clés OAuth, nous pouvons mettre en place l'authentification dans notre application Next.js. Nous allons utiliser la bibliothèque NextAuth.js (désormais aussi appelée Auth.js).

1. Configuration des variables d'environnement

Commençons par ajouter nos clés OAuth dans le fichier .env.local :

1# Configuration de base de NextAuth.js
2NEXTAUTH_URL=http://localhost:3000
3NEXTAUTH_SECRET=ta_cle_secrete_de_chiffrement
4
5# Google Provider
6GOOGLE_CLIENT_ID=ton_google_client_id
7GOOGLE_CLIENT_SECRET=ton_google_client_secret
8
9# GitHub Provider
10GITHUB_ID=ton_github_client_id
11GITHUB_SECRET=ton_github_client_secret
12
13# Facebook Provider
14FACEBOOK_CLIENT_ID=ton_facebook_app_id
15FACEBOOK_CLIENT_SECRET=ton_facebook_app_secret

2. Configuration des fournisseurs dans NextAuth

Créons ou mettons maintenant à jour le fichier de configuration de NextAuth.js :

1// app/api/auth/[...nextauth]/route.ts
2import NextAuth from "next-auth";
3import GoogleProvider from "next-auth/providers/google";
4import GitHubProvider from "next-auth/providers/github";
5import FacebookProvider from "next-auth/providers/facebook";
6import { PrismaAdapter } from "@auth/prisma-adapter";
7import { PrismaClient } from "@prisma/client";
8
9const prisma = new PrismaClient();
10
11export const authOptions = {
12  adapter: PrismaAdapter(prisma),
13  providers: [
14    GoogleProvider({
15      clientId: process.env.GOOGLE_CLIENT_ID!,
16      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
17      // We can define what profile data we want to receive
18      profile(profile) {
19        return {
20          id: profile.sub,
21          name: profile.name,
22          email: profile.email,
23          image: profile.picture,
24          // We can add additional fields specific to our application
25          emailVerified: profile.email_verified,
26        };
27      },
28    }),
29    GitHubProvider({
30      clientId: process.env.GITHUB_ID!,
31      clientSecret: process.env.GITHUB_SECRET!,
32      profile(profile) {
33        return {
34          id: profile.id.toString(),
35          name: profile.name || profile.login,
36          email: profile.email,
37          image: profile.avatar_url,
38          // Champ supplémentaire spécifique à GitHub
39          username: profile.login,
40        };
41      },
42    }),
43    FacebookProvider({
44      clientId: process.env.FACEBOOK_CLIENT_ID!,
45      clientSecret: process.env.FACEBOOK_CLIENT_SECRET!,
46      profile(profile) {
47        return {
48          id: profile.id,
49          name: profile.name,
50          email: profile.email,
51          image: profile.picture.data.url,
52        };
53      },
54    }),
55    // We can also add email/password authentication
56    // for users who prefer traditional login
57  ],
58  callbacks: {
59    async session({ session, user }) {
60      // Add user ID to the session to easily identify the user
61      if (session.user) {
62        session.user.id = user.id;
63      }
64      return session;
65    },
66    async jwt({ token, user, account, profile }) {
67      // If we have a user (e.g. during login), add their ID to the token
68      if (user) {
69        token.uid = user.id;
70      }
71      // We can also add information about the authentication provider
72      if (account) {
73        token.provider = account.provider;
74      }
75      return token;
76    },
77  },
78  pages: {
79    signIn: '/auth/login',  // Page de connexion personnalisée
80    signOut: '/auth/logout', // Page de déconnexion personnalisée
81    error: '/auth/error',    // Page d'erreur d'authentification
82  },
83  session: {
84    strategy: 'database',  // Nous utilisons la base de données pour stocker les sessions
85    maxAge: 30 * 24 * 60 * 60, // 30 jours
86  },
87  debug: process.env.NODE_ENV === 'development',
88};
89
90const handler = NextAuth(authOptions);
91export { handler as GET, handler as POST };

3. Créer une interface utilisateur pour la connexion

Créons maintenant une interface de connexion conviviale qui proposera toutes les méthodes d'authentification configurées :

1// app/auth/login/page.tsx
2'use client';
3
4import { signIn, useSession } from 'next-auth/react';
5import { useRouter, useSearchParams } from 'next/navigation';
6import { useEffect, useState } 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 [isLoading, setIsLoading] = useState(false);
17  const [error, setError] = useState('');
18
19  // If the user is already logged in, redirect them
20  useEffect(() => {
21    if (status === 'authenticated') {
22      router.push(callbackUrl);
23    }
24  }, [status, router, callbackUrl]);
25
26  // Email/password login handling (if configured)
27  const handleEmailLogin = async (e: React.FormEvent) => {
28    e.preventDefault();
29    setIsLoading(true);
30    setError('');
31
32    try {
33      const result = await signIn('credentials', {
34        redirect: false,
35        email,
36        password,
37      });
38
39      if (result?.error) {
40        setError('E-mail ou mot de passe invalide');
41      } else {
42        router.push(callbackUrl);
43      }
44    } catch (error) {
45      setError('Une erreur est survenue lors de la connexion');
46    } finally {
47      setIsLoading(false);
48    }
49  };
50
51  if (status === 'loading') {
52    return (
53      <div className="flex justify-center items-center min-h-screen">
54        <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500" />
55      </div>
56    );
57  }
58
59  return (
60    <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-lg">
61      <h1 className="text-2xl font-bold mb-6 text-center">Connexion à l'application</h1>
62
63      {error && (
64        <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
65          {error}
66        </div>
67      )}
68
69      <div className="space-y-4 mb-6">
70        <button
71          onClick={() => signIn('google', { callbackUrl })}
72          className="w-full flex items-center justify-center gap-3 p-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
73          disabled={isLoading}
74        >
75          <svg viewBox="0 0 24 24" width="20" height="20">
76            {/* Logo Google */}
77            <path
78              d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
79              fill="#4285F4"
80            />
81            <path
82              d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
83              fill="#34A853"
84            />
85            <path
86              d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
87              fill="#FBBC05"
88            />
89            <path
90              d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
91              fill="#EA4335"
92            />
93          </svg>
94          Continuer avec Google
95        </button>
96
97        <button
98          onClick={() => signIn('github', { callbackUrl })}
99          className="w-full flex items-center justify-center gap-3 p-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
100          disabled={isLoading}
101        >
102          <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
103            {/* Logo GitHub */}
104            <path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
105          </svg>
106          Continuer avec GitHub
107        </button>
108
109        <button
110          onClick={() => signIn('facebook', { callbackUrl })}
111          className="w-full flex items-center justify-center gap-3 p-3 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
112          disabled={isLoading}
113        >
114          <svg viewBox="0 0 24 24" width="20" height="20" fill="#1877F2">
115            {/* Logo Facebook */}
116            <path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
117          </svg>
118          Continuer avec Facebook
119        </button>
120      </div>
121
122      {/* Optionnel : formulaire de connexion e-mail/mot de passe */}
123      <div className="relative my-6">
124        <div className="absolute inset-0 flex items-center">
125          <div className="w-full border-t border-gray-300"></div>
126        </div>
127        <div className="relative flex justify-center text-sm">
128          <span className="px-2 bg-white text-gray-500">
129            ou utilise ton e-mail
130          </span>
131        </div>
132      </div>
133
134      <form onSubmit={handleEmailLogin} className="space-y-4">
135        <div>
136          <label htmlFor="email" className="block text-sm font-medium mb-1">
137            Email
138          </label>
139          <input
140            id="email"
141            type="email"
142            value={email}
143            onChange={(e) => setEmail(e.target.value)}
144            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
145            required
146          />
147        </div>
148
149        <div>
150          <label htmlFor="password" className="block text-sm font-medium mb-1">
151            Mot de passe
152          </label>
153          <input
154            id="password"
155            type="password"
156            value={password}
157            onChange={(e) => setPassword(e.target.value)}
158            className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
159            required
160          />
161        </div>
162
163        <button
164          type="submit"
165          className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
166          disabled={isLoading}
167        >
168          {isLoading ? 'Connexion en cours...' : 'Se connecter'}
169        </button>
170      </form>
171
172      <div className="mt-6 text-center">
173        <p className="text-sm text-gray-600">
174          Tu n'as pas encore de compte ?{' '}
175          <a href="/auth/register" className="text-blue-600 hover:underline">
176            S'inscrire
177          </a>
178        </p>
179      </div>
180    </div>
181  );
182}

4. Préparer le schéma de base de données Prisma

Si nous utilisons l'adaptateur Prisma, nous devons fournir un schéma de base de données approprié :

1// prisma/schema.prisma
2datasource db {
3  provider = "postgresql" // ou un autre fournisseur de base de données
4  url      = env("DATABASE_URL")
5}
6
7generator client {
8  provider = "prisma-client-js"
9}
10
11model Account {
12  id                String  @id @default(cuid())
13  userId            String
14  type              String
15  provider          String
16  providerAccountId String
17  refresh_token     String? @db.Text
18  access_token      String? @db.Text
19  expires_at        Int?
20  token_type        String?
21  scope             String?
22  id_token          String? @db.Text
23  session_state     String?
24
25  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
26
27  @@unique([provider, providerAccountId])
28}
29
30model Session {
31  id           String   @id @default(cuid())
32  sessionToken String   @unique
33  userId       String
34  expires      DateTime
35  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
36}
37
38model User {
39  id            String    @id @default(cuid())
40  name          String?
41  email         String?   @unique
42  emailVerified DateTime?
43  image         String?
44  accounts      Account[]
45  sessions      Session[]
46  // Champs supplémentaires spécifiques à ton application
47  username      String?   @unique
48  role          String?   @default("user")
49  createdAt     DateTime  @default(now())
50  updatedAt     DateTime  @updatedAt
51}
52
53model VerificationToken {
54  identifier String
55  token      String   @unique
56  expires    DateTime
57
58  @@unique([identifier, token])
59}

5. Créer un SessionProvider pour toute l'application

Pour que l'authentification fonctionne correctement dans toute l'application, ajoutons SessionProvider 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";
13import "./globals.css";
14
15export const metadata = {
16  title: "Mon Application",
17  description: "Description de mon application",
18};
19
20export default function RootLayout({
21  children,
22}: {
23  children: React.ReactNode;
24}) {
25  return (
26    <html lang="pl">
27      <body>
28        <Providers>{children}</Providers>
29      </body>
30    </html>
31  );
32}

6. Ajouter un composant de navigation prenant en compte l'état d'authentification

Créons maintenant un composant de navigation qui affichera différentes options selon que l'utilisateur est connecté ou non :

1// components/Navigation.tsx
2'use client';
3
4import { signOut, useSession } from 'next-auth/react';
5import Link from 'next/link';
6import Image from 'next/image';
7
8export default function Navigation() {
9  const { data: session, status } = useSession();
10  const isLoading = status === 'loading';
11
12  return (
13    <nav className="bg-white shadow-sm">
14      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
15        <div className="flex justify-between h-16">
16          <div className="flex items-center">
17            <Link href="/" className="shrink-0 flex items-center">
18              <span className="text-xl font-bold">MojaAplikacja</span>
19            </Link>
20            <div className="hidden sm:ml-6 sm:flex sm:space-x-8">
21              <Link
22                href="/"
23                className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
24              >
25                Accueil
26              </Link>
27              <Link
28                href="/features"
29                className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
30              >
31                Fonctionnalités
32              </Link>
33              <Link
34                href="/pricing"
35                className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
36              >
37                Tarifs
38              </Link>
39            </div>
40          </div>
41
42          <div className="flex items-center">
43            {isLoading ? (
44              <div className="h-8 w-8 animate-pulse rounded-full bg-gray-200" />
45            ) : session ? (
46              <div className="relative ml-3">
47                <div className="flex items-center space-x-4">
48                  <Link
49                    href="/dashboard"
50                    className="text-sm font-medium text-gray-700 hover:text-gray-800"
51                  >
52                    Dashboard
53                  </Link>
54
55                  <div className="flex items-center space-x-2">
56                    {session.user?.image ? (
57                      <Image
58                        className="h-8 w-8 rounded-full"
59                        src={session.user.image}
60                        alt={session.user.name || "Avatar"}
61                        width={32}
62                        height={32}
63                      />
64                    ) : (
65                      <div className="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center text-white text-sm">
66                        {session.user?.name?.charAt(0) || "U"}
67                      </div>
68                    )}
69
70                    <span className="text-sm font-medium text-gray-700">
71                      {session.user?.name || session.user?.email?.split('@')[0]}
72                    </span>
73                  </div>
74
75                  <button
76                    onClick={() => signOut({ callbackUrl: '/' })}
77                    className="text-sm font-medium text-gray-700 hover:text-gray-800"
78                  >
79                    Se déconnecter
80                  </button>
81                </div>
82              </div>
83            ) : (
84              <div className="space-x-4">
85                <Link
86                  href="/auth/login"
87                  className="text-gray-700 hover:text-gray-900 font-medium"
88                >
89                  Se connecter
90                </Link>
91                <Link
92                  href="/auth/register"
93                  className="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 font-medium"
94                >
95                  S'inscrire
96                </Link>
97              </div>
98            )}
99          </div>
100        </div>
101      </div>
102    </nav>
103  );
104}

7. Créer une page Dashboard réservée aux utilisateurs connectés

Créons une page Dashboard qui ne sera accessible qu'aux utilisateurs connectés :

1// app/dashboard/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 DashboardPage() {
7  const session = await getServerSession(authOptions);
8
9  if (!session) {
10    redirect("/auth/login?callbackUrl=/dashboard");
11  }
12
13  return (
14    <div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
15      <div className="px-4 py-6 sm:px-0">
16        <h1 className="text-2xl font-bold mb-4">Dashboard</h1>
17
18        <div className="bg-white shadow overflow-hidden sm:rounded-lg">
19          <div className="px-4 py-5 sm:px-6">
20            <h3 className="text-lg leading-6 font-medium text-gray-900">
21              Informations du profil
22            </h3>
23            <p className="mt-1 max-w-2xl text-sm text-gray-500">
24              Les détails et paramètres de ton compte.
25            </p>
26          </div>
27          <div className="border-t border-gray-200">
28            <dl>
29              <div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
30                <dt className="text-sm font-medium text-gray-500">Nom complet</dt>
31                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
32                  {session.user?.name || "Non renseigné"}
33                </dd>
34              </div>
35              <div className="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
36                <dt className="text-sm font-medium text-gray-500">Adresse e-mail</dt>
37                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
38                  {session.user?.email}
39                </dd>
40              </div>
41              <div className="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
42                <dt className="text-sm font-medium text-gray-500">Méthode de connexion</dt>
43                <dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
44                  {/* Ce champ nécessite d'enrichir le jeton avec l'information du fournisseur */}
45                  Fournisseur d'authentification externe
46                </dd>
47              </div>
48            </dl>
49          </div>
50        </div>
51      </div>
52    </div>
53  );
54}

8. Créer un middleware pour protéger les chemins

Sécurisons certains chemins de notre application à l'aide du middleware de Next.js :

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  const isAuthPage = request.nextUrl.pathname.startsWith('/auth');
10
11  if (isAuthPage) {
12    if (token) {
13      return NextResponse.redirect(new URL('/dashboard', request.url));
14    }
15    return NextResponse.next();
16  }
17
18  if (!token) {
19    const url = new URL('/auth/login', request.url);
20    url.searchParams.set('callbackUrl', request.nextUrl.pathname);
21    return NextResponse.redirect(url);
22  }
23
24  return NextResponse.next();
25}
26
27// Indique les chemins auxquels le middleware doit s'appliquer
28export const config = {
29  matcher: [
30    '/dashboard/:path*',
31    '/profile/:path*',
32    '/settings/:path*',
33    '/auth/:path*'
34  ],
35};

Configuration avancée

Modifier les données de profil de l'utilisateur

Nous pouvons modifier les données de profil provenant des fournisseurs externes avant de les enregistrer dans notre base de données :

1GoogleProvider({
2  clientId: process.env.GOOGLE_CLIENT_ID!,
3  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
4  profile(profile) {
5    // Transformation des données du profil
6    return {
7      id: profile.sub,
8      name: profile.name,
9      email: profile.email,
10      image: profile.picture,
11      // Champs supplémentaires spécifiques à l'application
12      role: profile.email.endsWith('@tonentreprise.com') ? 'admin' : 'user',
13      emailVerified: profile.email_verified ? new Date() : null,
14    };
15  },
16});

Lier des comptes provenant de différents fournisseurs

NextAuth.js gère automatiquement la liaison des comptes provenant de différents fournisseurs s'ils utilisent la même adresse e-mail. Si nous voulons personnaliser ce comportement, nous pouvons modifier la fonction linkAccount dans les callbacks :

1callbacks: {
2  // ...autres callbacks
3  async linkAccount({ user, account, profile }) {
4    // Ici, nous pouvons exécuter une logique de liaison de compte personnalisée
5    console.log(`Liaison du compte : ${account.provider} pour l'utilisateur ${user.id}`);
6    return true;
7  },
8}

Ajouter une logique personnalisée au processus d'authentification

Nous pouvons ajouter des étapes supplémentaires dans le processus d'authentification à l'aide du callback signIn :

1callbacks: {
2  // ...autres callbacks
3  async signIn({ user, account, profile, email, credentials }) {
4    // Vérifie si l'utilisateur peut se connecter
5    // Par exemple : vérifier si l'e-mail appartient au domaine de l'entreprise
6    const allowedDomains = ['tonentreprise.com', 'partner.com'];
7    const emailDomain = user.email?.split('@')[1];
8
9    if (emailDomain && !allowedDomains.includes(emailDomain)) {
10      return false; // Refuser l'accès
11    }
12
13    // Nous pouvons aussi mettre à jour les données de l'utilisateur à chaque connexion
14    if (user.id) {
15      await prisma.user.update({
16        where: { id: user.id },
17        data: { lastLogin: new Date() }
18      });
19    }
20
21    return true; // Autoriser l'accès
22  },
23}

Gestion des erreurs d'authentification

Créons une page d'erreur d'authentification personnalisée :

1// app/auth/error/page.tsx
2'use client';
3
4import { useSearchParams } from "next/navigation";
5import Link from "next/link";
6
7export default function AuthErrorPage() {
8  const searchParams = useSearchParams();
9  const error = searchParams.get('error');
10
11  let errorMessage = "Une erreur est survenue lors de la tentative d'authentification.";
12  let errorDescription = "Réessaie ou contacte l'administration.";
13
14  // Associe les codes d'erreur à des messages compréhensibles
15  switch (error) {
16    case "Configuration":
17      errorMessage = "Une erreur de configuration du serveur est survenue.";
18      errorDescription = "Contacte l'administrateur système.";
19      break;
20    case "AccessDenied":
21      errorMessage = "Accès refusé.";
22      errorDescription = "Tu n'as pas les permissions suffisantes pour te connecter.";
23      break;
24    case "Verification":
25      errorMessage = "Le lien de vérification a expiré ou est invalide.";
26      errorDescription = "Essaie de renvoyer le lien de vérification.";
27      break;
28    case "OAuthSignin":
29    case "OAuthCallback":
30    case "OAuthCreateAccount":
31    case "EmailCreateAccount":
32    case "Callback":
33      errorMessage = "Une erreur est survenue lors de la communication avec le fournisseur d'authentification.";
34      errorDescription = "Réessaie dans un instant ou choisis une autre méthode de connexion.";
35      break;
36    case "OAuthAccountNotLinked":
37      errorMessage = "Un compte avec cette adresse e-mail existe déjà.";
38      errorDescription = "Connecte-toi avec la méthode que tu avais utilisée précédemment.";
39      break;
40    case "EmailSignin":
41      errorMessage = "Impossible d'envoyer l'e-mail contenant le lien de connexion.";
42      errorDescription = "Vérifie que l'adresse e-mail fournie est correcte.";
43      break;
44    case "CredentialsSignin":
45      errorMessage = "Identifiants de connexion invalides.";
46      errorDescription = "Vérifie que l'e-mail et le mot de passe fournis sont corrects.";
47      break;
48    case "SessionRequired":
49      errorMessage = "Connexion requise.";
50      errorDescription = "Connecte-toi pour accéder à cette page.";
51      break;
52    default:
53      // Message par défaut déjà défini
54      break;
55  }
56
57  return (
58    <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
59      <div className="max-w-md w-full space-y-8">
60        <div>
61          <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
62            {errorMessage}
63          </h2>
64          <p className="mt-2 text-center text-sm text-gray-600">
65            {errorDescription}
66          </p>
67        </div>
68        <div className="flex flex-col items-center space-y-4">
69          <Link
70            href="/auth/login"
71            className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none"
72          >
73            Retour à la page de connexion
74          </Link>
75          <Link
76            href="/"
77            className="text-sm text-blue-600 hover:text-blue-500"
78          >
79            Retour à la page d'accueil
80          </Link>
81        </div>
82      </div>
83    </div>
84  );
85}

Résumé

Dans ce module, nous avons vu en détail comment mettre en œuvre l'authentification avec des fournisseurs externes populaires (Google, GitHub, Facebook) dans une application Next.js. Nous avons appris :

  1. Le processus d'enregistrement de l'application auprès des fournisseurs OAuth et l'obtention des clés
  2. La configuration de NextAuth.js / Auth.js avec différents fournisseurs
  3. La création d'une interface utilisateur pour la connexion et l'inscription
  4. La protection des routes à l'aide d'un middleware
  5. L'adaptation du processus d'authentification à nos besoins
  6. La gestion des erreurs d'authentification

Grâce à cette implémentation, notre application Next.js dispose d'un système d'authentification professionnel et multi-plateforme qui améliore l'expérience utilisateur tout en renforçant la sécurité de notre application.

Dans le prochain module, nous traiterons en détail des sessions utilisateur et de la gestion des jetons, ce qui nous permettra de mieux comprendre le fonctionnement des mécanismes d'authentification après la connexion de l'utilisateur.

Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3// Demo: OAuth Providers - Google, GitHub, Facebook
4interface OAuthProvider {
5  id: string;
6  name: string;
7  icon: string;
8  color: string;
9  scopes: string[];
10  requiredEnvVars: string[];
11}
12
13const providers: OAuthProvider[] = [
14  { id: 'google', name: 'Google', icon: 'G', color: '#4285f4', scopes: ['openid', 'profile', 'email'], requiredEnvVars: ['GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET'] },
15  { id: 'github', name: 'GitHub', icon: 'GH', color: '#333', scopes: ['read:user', 'user:email'], requiredEnvVars: ['GITHUB_ID', 'GITHUB_SECRET'] },
16  { id: 'facebook', name: 'Facebook', icon: 'FB', color: '#1877f2', scopes: ['public_profile', 'email'], requiredEnvVars: ['FACEBOOK_CLIENT_ID', 'FACEBOOK_CLIENT_SECRET'] },
17];
18
19type FlowStep = 'idle' | 'redirect' | 'callback' | 'token' | 'profile' | 'done';
20
21function OAuthFlowDemo() {
22  const [selectedProvider, setSelectedProvider] = useState<OAuthProvider | null>(null);
23  const [currentStep, setCurrentStep] = useState<FlowStep>('idle');
24  const [flowLog, setFlowLog] = useState<string[]>([]);
25
26  const steps: { step: FlowStep; label: string; desc: string }[] = [
27    { step: 'redirect', label: '1. Redirect', desc: 'Redirection vers le provider' },
28    { step: 'callback', label: '2. Callback', desc: 'Le provider retourne le code' },
29    { step: 'token', label: '3. Token', desc: 'Échange du code contre un token' },
30    { step: 'profile', label: '4. Profile', desc: 'Récupération du profil' },
31    { step: 'done', label: '5. Done', desc: 'Session créée' },
32  ];
33
34  const simulateFlow = async (provider: OAuthProvider) => {
35    setSelectedProvider(provider);
36    setFlowLog([]);
37    setCurrentStep('idle');
38
39    const addLog = (msg: string) => setFlowLog(prev => [...prev, msg]);
40
41    await new Promise(r => setTimeout(r, 400));
42    setCurrentStep('redirect');
43    addLog('[Client] Clic sur "Se connecter avec ' + provider.name + '"');
44
45    await new Promise(r => setTimeout(r, 1000));
46    setCurrentStep('callback');
47    addLog('[' + provider.name + '] L\'utilisateur a approuvé l\'accès');
48    addLog('[Callback] GET /api/auth/callback?code=abc123');
49
50    await new Promise(r => setTimeout(r, 800));
51    setCurrentStep('token');
52    addLog('[Server] Échange de l\'authorization code contre un access_token');
53
54    await new Promise(r => setTimeout(r, 800));
55    setCurrentStep('profile');
56    addLog('[Server] Récupération du profil utilisateur depuis ' + provider.name);
57
58    await new Promise(r => setTimeout(r, 600));
59    setCurrentStep('done');
60    addLog('[Server] Session créée, utilisateur connecté');
61  };
62
63  return (
64    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
65      <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>OAuth Providers Flow</h1>
66      <p style={{ color: '#8892b0', marginBottom: '24px' }}>Visualisation du processus de connexion OAuth</p>
67
68      <div style={{ display: 'flex', gap: '16px', marginBottom: '32px', flexWrap: 'wrap' }}>
69        {providers.map(p => (
70          <button key={p.id} onClick={() => simulateFlow(p)} style={{
71            padding: '16px 24px', borderRadius: '12px', border: '2px solid',
72            borderColor: selectedProvider?.id === p.id ? p.color : '#333',
73            background: selectedProvider?.id === p.id ? p.color + '20' : '#1a1a2e',
74            color: '#fff', cursor: 'pointer', fontSize: '16px',
75          }}>
76            <div style={{ fontSize: '20px', fontWeight: 'bold' }}>{p.icon}</div>
77            <div style={{ marginTop: '4px' }}>{p.name}</div>
78          </button>
79        ))}
80      </div>
81
82      {selectedProvider && (
83        <>
84          <div style={{ display: 'flex', gap: '8px', marginBottom: '24px', flexWrap: 'wrap' }}>
85            {steps.map(s => {
86              const stepIdx = steps.findIndex(x => x.step === currentStep);
87              const thisIdx = steps.findIndex(x => x.step === s.step);
88              return (
89                <div key={s.step} style={{
90                  padding: '8px 16px', borderRadius: '8px', fontSize: '13px',
91                  background: currentStep === s.step ? '#64ffda20' : thisIdx < stepIdx ? '#1a3a2e' : '#1a1a2e',
92                  border: currentStep === s.step ? '1px solid #64ffda' : '1px solid #333',
93                  color: currentStep === s.step ? '#64ffda' : '#8892b0',
94                }}>
95                  <strong>{s.label}</strong>
96                  <div style={{ fontSize: '11px', marginTop: '2px' }}>{s.desc}</div>
97                </div>
98              );
99            })}
100          </div>
101
102          <div style={{ background: '#1a1a2e', borderRadius: '8px', padding: '16px', border: '1px solid #333', maxHeight: '250px', overflow: 'auto' }}>
103            <h3 style={{ color: '#64ffda', margin: '0 0 12px 0', fontSize: '14px' }}>Flow Log:</h3>
104            {flowLog.map((log, i) => (
105              <div key={i} style={{ fontSize: '13px', padding: '4px 0', borderBottom: '1px solid #222', color: log.includes('[Server]') ? '#f59e0b' : log.includes('[Client]') ? '#64ffda' : '#8892b0' }}>
106                {log}
107              </div>
108            ))}
109          </div>
110
111          <div style={{ marginTop: '16px', background: '#1a1a2e', padding: '12px', borderRadius: '8px', border: '1px solid #333' }}>
112            <strong style={{ color: '#f59e0b' }}>Variables .env requises :</strong>
113            <div style={{ marginTop: '8px', display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
114              {selectedProvider.requiredEnvVars.map(v => (
115                <code key={v} style={{ background: '#0f0f23', padding: '4px 8px', borderRadius: '4px', fontSize: '13px', color: '#64ffda' }}>{v}</code>
116              ))}
117            </div>
118          </div>
119        </>
120      )}
121    </div>
122  );
123}
124
125export default OAuthFlowDemo;

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 par session stocke les données de session :

  2. 2. L'authentification par jeton utilise :

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée le fichier [...nextauth].ts avec une configuration de base pour providers, session et callbacks

  • Tri vertical

    Place les éléments dans le bon ordre : import 'client-only' → export function →

  • Clique dans l'ordre

    Place la syntaxe d'import de getServerSession depuis NextAuth

  • Éditeur de code

    Configure le fournisseur Google dans NextAuth avec l'ID client, le secret et les scopes appropriés

Articles utiles