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

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 :
- Une page de connexion supportant Google OAuth et email/mot de passe
- Un tableau de bord protégé accessible uniquement aux utilisateurs authentifiés
- Un panneau d'administration restreint aux utilisateurs avec le rôle
admin - 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-appInstallez les dépendances requises :
npm install next-auth@beta @auth/prisma-adapter @prisma/client prisma bcryptjs zod
npm install -D @types/bcryptjsVoici le rôle de chaque package :
| Package | Rôle |
|---|---|
next-auth@beta | Auth.js v5 pour Next.js |
@auth/prisma-adapter | Stockage des utilisateurs/sessions en base de données |
prisma | ORM pour la base de données |
bcryptjs | Hachage des mots de passe |
zod | Validation 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 postgresqlMettez à 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 generateCré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 :
- Le fournisseur Google gère l'OAuth avec un mapping automatique du profil
- Le fournisseur Credentials valide l'email/mot de passe avec Zod et bcrypt
- Les callbacks JWT persistent le rôle de l'utilisateur dans le token
- Le callback session expose le rôle aux composants client
- Le callback authorized protège les routes
/dashboardet/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 } = handlersC'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
- Allez sur Google Cloud Console
- Créez un nouveau projet ou sélectionnez un projet existant
- Naviguez vers APIs & Services > Credentials
- Cliquez sur Create Credentials > OAuth client ID
- Sélectionnez Web application
- Ajoutez l'URI de redirection autorisée :
http://localhost:3000/api/auth/callback/google - Copiez le Client ID et le Client Secret dans votre
.env.local
Générez un secret d'authentification :
npx auth secretTester votre implémentation
Démarrez le serveur de développement :
npm run devTestez les flux suivants :
- Google OAuth : Cliquez sur "Continuer avec Google" sur la page de connexion
- Inscription : Créez un nouveau compte via
/auth/register - Connexion par identifiants : Connectez-vous avec l'email/mot de passe enregistrés
- Routes protégées : Essayez d'accéder à
/dashboardsans vous connecter - Accès admin : Changez manuellement le rôle d'un utilisateur en
admindans 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.
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.

Construire une application temps réel avec Supabase et Next.js 15 : guide complet
Apprenez à construire une application full-stack en temps réel avec Supabase et Next.js 15 App Router. Ce guide couvre l'authentification, la base de données, Row Level Security et les abonnements temps réel.

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.