Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

L'authentification est le gardien de toute application web sérieuse. Que vous construisiez un tableau de bord SaaS, une plateforme e-commerce ou un outil interne, vous avez besoin d'un système d'authentification sécurisé et maintenable. Auth.js v5 (anciennement NextAuth.js) est le standard de facto pour l'authentification dans l'écosystème Next.js — et avec sa réécriture en v5, il est plus rapide, plus simple et plus puissant que jamais.

Dans ce tutoriel, vous construirez un système d'authentification complet de zéro, couvrant :

  • La connexion via Google OAuth
  • Les identifiants email/mot de passe
  • La gestion des sessions avec JWT
  • La protection des routes via middleware
  • Le contrôle d'accès basé sur les rôles (RBAC)

Pourquoi Auth.js v5 ? La version 5 apporte un support natif de l'App Router, la compatibilité Edge Runtime, une surface API simplifiée et une protection CSRF intégrée. Si vous démarrez un nouveau projet Next.js en 2026, Auth.js v5 est le choix recommandé.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 18+ installé
  • Un projet Next.js 15 (App Router)
  • Un compte Google Cloud Console (pour OAuth)
  • Des connaissances de base en TypeScript et React Server Components

Ce que vous allez construire

Une application Next.js 15 avec :

  1. Une page de connexion supportant Google OAuth et email/mot de passe
  2. Un tableau de bord protégé accessible uniquement aux utilisateurs authentifiés
  3. Un panneau d'administration restreint aux utilisateurs avec le rôle admin
  4. Un middleware qui redirige automatiquement les utilisateurs non authentifiés

Étape 1 : Configurer votre projet Next.js

Si vous n'avez pas encore de projet, créez-en un :

npx create-next-app@latest my-auth-app --typescript --tailwind --app --src-dir
cd my-auth-app

Installez les dépendances requises :

npm install next-auth@beta @auth/prisma-adapter @prisma/client prisma bcryptjs zod
npm install -D @types/bcryptjs

Voici le rôle de chaque package :

PackageRôle
next-auth@betaAuth.js v5 pour Next.js
@auth/prisma-adapterStockage des utilisateurs/sessions en base de données
prismaORM pour la base de données
bcryptjsHachage des mots de passe
zodValidation des schémas pour les identifiants

Étape 2 : Configurer Prisma

Initialisez Prisma avec votre base de données préférée (nous utiliserons PostgreSQL) :

npx prisma init --datasource-provider postgresql

Mettez à jour votre prisma/schema.prisma avec les modèles Auth.js :

generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  passwordHash  String?
  role          String    @default("user")
  accounts      Account[]
  sessions      Session[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}
 
model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.Text
  session_state     String?
  user              User    @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  @@unique([provider, providerAccountId])
}
 
model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}
 
model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime
 
  @@unique([identifier, token])
}

Notez les champs passwordHash et role sur User — ce sont des ajouts personnalisés pour la connexion par identifiants et le RBAC.

Exécutez la migration :

npx prisma migrate dev --name init
npx prisma generate

Créez un singleton Prisma dans src/lib/prisma.ts :

import { PrismaClient } from "@prisma/client"
 
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}
 
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

Étape 3 : Configurer Auth.js

Créez le fichier de configuration principal d'Auth.js dans src/auth.ts :

import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import bcrypt from "bcryptjs"
import { z } from "zod"
 
const signInSchema = z.object({
  email: z.string().email("Adresse email invalide"),
  password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères"),
})
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID!,
      clientSecret: process.env.AUTH_GOOGLE_SECRET!,
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          email: profile.email,
          image: profile.picture,
          role: "user",
        }
      },
    }),
    Credentials({
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const parsed = signInSchema.safeParse(credentials)
        if (!parsed.success) return null
 
        const { email, password } = parsed.data
 
        const user = await prisma.user.findUnique({
          where: { email },
        })
 
        if (!user || !user.passwordHash) return null
 
        const isValid = await bcrypt.compare(password, user.passwordHash)
        if (!isValid) return null
 
        return {
          id: user.id,
          name: user.name,
          email: user.email,
          image: user.image,
          role: user.role,
        }
      },
    }),
  ],
  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60,
  },
  pages: {
    signIn: "/auth/signin",
    error: "/auth/error",
  },
  callbacks: {
    jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.role = (user as any).role ?? "user"
      }
      return token
    },
    session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        ;(session.user as any).role = token.role as string
      }
      return session
    },
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user
      const isOnDashboard = nextUrl.pathname.startsWith("/dashboard")
      const isOnAdmin = nextUrl.pathname.startsWith("/admin")
 
      if (isOnDashboard || isOnAdmin) {
        if (isLoggedIn) return true
        return false
      }
      return true
    },
  },
})

Voici ce qui se passe :

  1. Le fournisseur Google gère l'OAuth avec un mapping automatique du profil
  2. Le fournisseur Credentials valide l'email/mot de passe avec Zod et bcrypt
  3. Les callbacks JWT persistent le rôle de l'utilisateur dans le token
  4. Le callback session expose le rôle aux composants client
  5. Le callback authorized protège les routes /dashboard et /admin

Étape 4 : Configurer la route API

Créez la route API d'Auth.js dans src/app/api/auth/[...nextauth]/route.ts :

import { handlers } from "@/auth"
 
export const { GET, POST } = handlers

C'est tout — Auth.js v5 rend cela incroyablement propre.

Étape 5 : Ajouter le Middleware pour la protection des routes

Créez src/middleware.ts :

export { auth as default } from "@/auth"
 
export const config = {
  matcher: [
    "/dashboard/:path*",
    "/admin/:path*",
    "/api/protected/:path*",
  ],
}

Ce middleware exécute le callback authorized de votre configuration Auth.js sur chaque route correspondante. Les utilisateurs non authentifiés sont automatiquement redirigés vers votre page de connexion.

Étape 6 : Étendre les types TypeScript

Pour obtenir un typage correct pour session.user.role, créez src/types/next-auth.d.ts :

import { DefaultSession } from "next-auth"
 
declare module "next-auth" {
  interface Session {
    user: {
      id: string
      role: string
    } & DefaultSession["user"]
  }
 
  interface User {
    role?: string
  }
}
 
declare module "next-auth/jwt" {
  interface JWT {
    id: string
    role: string
  }
}

Étape 7 : Construire la page de connexion

Créez une page de connexion personnalisée dans src/app/auth/signin/page.tsx :

import { signIn, auth } from "@/auth"
import { redirect } from "next/navigation"
 
export default async function SignInPage() {
  const session = await auth()
  if (session) redirect("/dashboard")
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
        <div className="text-center">
          <h1 className="text-3xl font-bold text-gray-900">Bon retour</h1>
          <p className="mt-2 text-gray-600">Connectez-vous à votre compte</p>
        </div>
 
        {/* Google OAuth */}
        <form
          action={async () => {
            "use server"
            await signIn("google", { redirectTo: "/dashboard" })
          }}
        >
          <button
            type="submit"
            className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
          >
            Continuer avec Google
          </button>
        </form>
 
        <div className="relative">
          <div className="absolute inset-0 flex items-center">
            <div className="w-full border-t border-gray-300" />
          </div>
          <div className="relative flex justify-center text-sm">
            <span className="px-2 bg-white text-gray-500">Ou continuer avec email</span>
          </div>
        </div>
 
        {/* Email/Mot de passe */}
        <form
          action={async (formData) => {
            "use server"
            await signIn("credentials", {
              email: formData.get("email"),
              password: formData.get("password"),
              redirectTo: "/dashboard",
            })
          }}
          className="space-y-4"
        >
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              Email
            </label>
            <input
              id="email"
              name="email"
              type="email"
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
              placeholder="vous@exemple.com"
            />
          </div>
          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              Mot de passe
            </label>
            <input
              id="password"
              name="password"
              type="password"
              required
              minLength={8}
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            />
          </div>
          <button
            type="submit"
            className="w-full py-3 px-4 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
          >
            Se connecter
          </button>
        </form>
 
        <p className="text-center text-sm text-gray-600">
          Pas encore de compte ?{" "}
          <a href="/auth/register" className="text-blue-600 hover:underline">
            Créez-en un
          </a>
        </p>
      </div>
    </div>
  )
}

Étape 8 : Créer un endpoint d'inscription

Créez une server action pour l'inscription dans src/app/auth/register/actions.ts :

"use server"
 
import { prisma } from "@/lib/prisma"
import bcrypt from "bcryptjs"
import { z } from "zod"
 
const registerSchema = z.object({
  name: z.string().min(2, "Le nom doit contenir au moins 2 caractères"),
  email: z.string().email("Adresse email invalide"),
  password: z.string().min(8, "Le mot de passe doit contenir au moins 8 caractères"),
})
 
export async function register(formData: FormData) {
  const parsed = registerSchema.safeParse({
    name: formData.get("name"),
    email: formData.get("email"),
    password: formData.get("password"),
  })
 
  if (!parsed.success) {
    return { error: parsed.error.errors[0].message }
  }
 
  const { name, email, password } = parsed.data
 
  const existingUser = await prisma.user.findUnique({
    where: { email },
  })
 
  if (existingUser) {
    return { error: "Un compte avec cet email existe déjà" }
  }
 
  const passwordHash = await bcrypt.hash(password, 12)
 
  await prisma.user.create({
    data: {
      name,
      email,
      passwordHash,
      role: "user",
    },
  })
 
  return { success: true }
}

Étape 9 : Construire un tableau de bord protégé

Créez un tableau de bord dans src/app/dashboard/page.tsx :

import { auth, signOut } from "@/auth"
import { redirect } from "next/navigation"
 
export default async function DashboardPage() {
  const session = await auth()
 
  if (!session?.user) redirect("/auth/signin")
 
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-4xl mx-auto">
        <div className="flex items-center justify-between mb-8">
          <h1 className="text-3xl font-bold text-gray-900">Tableau de bord</h1>
          <form
            action={async () => {
              "use server"
              await signOut({ redirectTo: "/" })
            }}
          >
            <button
              type="submit"
              className="px-4 py-2 text-sm text-red-600 border border-red-300 rounded-lg hover:bg-red-50 transition-colors"
            >
              Se déconnecter
            </button>
          </form>
        </div>
 
        <div className="bg-white rounded-xl shadow-sm p-6">
          <div className="flex items-center gap-4">
            {session.user.image && (
              <img
                src={session.user.image}
                alt=""
                className="w-16 h-16 rounded-full"
              />
            )}
            <div>
              <h2 className="text-xl font-semibold">{session.user.name}</h2>
              <p className="text-gray-600">{session.user.email}</p>
              <span className="inline-block mt-1 px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">
                {(session.user as any).role}
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

Étape 10 : Implémenter le contrôle d'accès basé sur les rôles

Créez une page réservée aux administrateurs dans src/app/admin/page.tsx :

import { auth } from "@/auth"
import { redirect } from "next/navigation"
 
export default async function AdminPage() {
  const session = await auth()
 
  if (!session?.user) redirect("/auth/signin")
  if ((session.user as any).role !== "admin") redirect("/dashboard")
 
  return (
    <div className="min-h-screen bg-gray-50 p-8">
      <div className="max-w-4xl mx-auto">
        <div className="flex items-center gap-3 mb-8">
          <span className="px-3 py-1 text-sm font-medium bg-red-100 text-red-800 rounded-full">
            Admins uniquement
          </span>
          <h1 className="text-3xl font-bold text-gray-900">Panneau d'administration</h1>
        </div>
 
        <div className="bg-white rounded-xl shadow-sm p-6">
          <h2 className="text-lg font-semibold mb-4">Gestion des utilisateurs</h2>
          <p className="text-gray-600">
            Cette page est accessible uniquement aux utilisateurs ayant le rôle administrateur.
            Vous pouvez ajouter des fonctionnalités de gestion des utilisateurs ici.
          </p>
        </div>
      </div>
    </div>
  )
}

Pour appliquer cela au niveau du middleware également, mettez à jour votre callback authorized dans src/auth.ts :

authorized({ auth, request: { nextUrl } }) {
  const isLoggedIn = !!auth?.user
  const isOnAdmin = nextUrl.pathname.startsWith("/admin")
 
  if (isOnAdmin) {
    if (!isLoggedIn) return false
    if ((auth?.user as any)?.role !== "admin") {
      return Response.redirect(new URL("/dashboard", nextUrl))
    }
    return true
  }
 
  if (nextUrl.pathname.startsWith("/dashboard")) {
    return isLoggedIn
  }
 
  return true
},

Étape 11 : Configurer les variables d'environnement

Créez votre fichier .env.local :

# Base de données
DATABASE_URL="postgresql://user:password@localhost:5432/myauthdb"
 
# Auth.js
AUTH_SECRET="votre-secret-aléatoire-ici"
AUTH_URL="http://localhost:3000"
 
# Google OAuth
AUTH_GOOGLE_ID="votre-google-client-id"
AUTH_GOOGLE_SECRET="votre-google-client-secret"

Obtenir les identifiants Google OAuth

  1. Allez sur Google Cloud Console
  2. Créez un nouveau projet ou sélectionnez un projet existant
  3. Naviguez vers APIs & Services > Credentials
  4. Cliquez sur Create Credentials > OAuth client ID
  5. Sélectionnez Web application
  6. Ajoutez l'URI de redirection autorisée : http://localhost:3000/api/auth/callback/google
  7. Copiez le Client ID et le Client Secret dans votre .env.local

Générez un secret d'authentification :

npx auth secret

Tester votre implémentation

Démarrez le serveur de développement :

npm run dev

Testez les flux suivants :

  1. Google OAuth : Cliquez sur "Continuer avec Google" sur la page de connexion
  2. Inscription : Créez un nouveau compte via /auth/register
  3. Connexion par identifiants : Connectez-vous avec l'email/mot de passe enregistrés
  4. Routes protégées : Essayez d'accéder à /dashboard sans vous connecter
  5. Accès admin : Changez manuellement le rôle d'un utilisateur en admin dans la base de données

Dépannage

Erreur "CSRF token mismatch"

Assurez-vous que AUTH_URL dans votre .env.local correspond à l'URL réelle que vous utilisez.

Google OAuth redirige vers la mauvaise URL

Vérifiez que l'URI de redirection dans Google Cloud Console correspond exactement à : http://localhost:3000/api/auth/callback/google

La session est null dans les composants client

Utilisez le wrapper SessionProvider dans votre layout si vous avez besoin d'accéder à la session dans les composants client.

La connexion par identifiants retourne null

Assurez-vous que l'utilisateur a un champ passwordHash. Les utilisateurs inscrits via Google OAuth n'auront pas de mot de passe.

Prochaines étapes

Maintenant que vous avez une base d'authentification solide, envisagez :

  • Vérification d'email : Utilisez le fournisseur email d'Auth.js avec Resend ou Nodemailer
  • Authentification à deux facteurs : Ajoutez la 2FA basée sur TOTP
  • Fournisseurs sociaux : Ajoutez GitHub, Discord ou Apple en plus de Google
  • Limitation de débit : Protégez votre endpoint de connexion contre les attaques par force brute
  • Journal d'audit : Suivez les événements de connexion pour la surveillance de sécurité

Conclusion

Vous avez construit un système d'authentification prêt pour la production avec Auth.js v5 et Next.js 15 qui comprend :

  • Double authentification : Google OAuth et identifiants email/mot de passe
  • Utilisateurs en base de données : Prisma avec PostgreSQL pour le stockage persistant
  • Sessions JWT : Gestion de sessions rapide et sans état
  • Protection des routes : Contrôle d'accès basé sur le middleware
  • RBAC : Autorisation basée sur les rôles au niveau des pages et du middleware

L'API simplifiée d'Auth.js v5 rend remarquablement facile l'ajout d'une authentification de niveau entreprise à vos applications Next.js. La combinaison des Server Components, des Server Actions et du middleware vous offre plusieurs couches de sécurité sans sacrifier l'expérience développeur.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur 11 Les Bases de Laravel 11 : Generation d'URL.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire un Starter Kit SaaS avec Next.js 15, Stripe et Auth.js v5

Apprenez a construire une application SaaS prete pour la production avec Next.js 15, Stripe pour la facturation par abonnement, et Auth.js v5 pour l'authentification. Ce tutoriel pas a pas couvre la configuration du projet, la connexion OAuth, les plans tarifaires, la gestion des webhooks et les routes protegees.

35 min read·

Construire un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·