Better Auth avec Next.js 15 : Le Guide Complet d'Authentification pour 2026

L'authentification bien faite. Better Auth est la bibliothèque d'authentification indépendante des frameworks et orientée TypeScript qui a conquis l'écosystème en 2026. Elle gère les sessions, OAuth, la vérification d'email, l'authentification à deux facteurs et bien plus — sans aucun verrouillage fournisseur et avec une sécurité de types complète. Dans ce tutoriel, vous construirez un système d'authentification complet avec Next.js 15.
Ce que vous apprendrez
À la fin de ce tutoriel, vous saurez :
- Configurer Better Auth dans un projet Next.js 15 App Router
- Implémenter l'inscription et la connexion par email/mot de passe
- Ajouter les fournisseurs GitHub et Google OAuth
- Protéger les routes avec le middleware et les vérifications de session côté serveur
- Construire un système de contrôle d'accès basé sur les rôles (RBAC)
- Gérer la vérification d'email et la réinitialisation de mot de passe
- Administrer les sessions utilisateur avec des tokens sécurisés par cookies
- Déployer un système d'authentification prêt pour la production
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (
node --version) - De l'expérience en TypeScript (types, génériques, async/await)
- Une connaissance de Next.js 15 (App Router, Server Components, Server Actions)
- PostgreSQL en local ou une base de données cloud (Neon, Supabase ou similaire)
- Un éditeur de code — VS Code ou Cursor recommandé
- Des applications OAuth GitHub et Google créées (nous vous guiderons)
Pourquoi Better Auth ?
Le paysage de l'authentification JavaScript a considérablement évolué. Lucia Auth a été abandonné, NextAuth (Auth.js) a ses complexités, et beaucoup de développeurs voulaient quelque chose de plus simple mais plus puissant. Better Auth comble ce vide :
| Fonctionnalité | Better Auth | Auth.js v5 | JWT Personnalisé |
|---|---|---|---|
| Sécurité des types | Inférence TypeScript complète | Partielle | Manuelle |
| Dépendance au framework | Aucune | Centré Next.js | Aucune |
| Contrôle de la BDD | Vous la possédez | Basé sur des adaptateurs | Vous la possédez |
| Fournisseurs OAuth | 20+ intégrés | 80+ intégrés | Manuel |
| 2FA / MFA | Plugin intégré | Communautaire | Manuel |
| RBAC | Plugin intégré | Manuel | Manuel |
| Vérification email | Intégrée | Intégrée | Manuelle |
| Stratégie de session | Cookie + BDD | JWT ou BDD | JWT |
| Taille du bundle | ~15KB | ~30KB | ~2KB |
| Courbe d'apprentissage | Faible | Moyenne | Élevée |
Better Auth vous donne un contrôle total sur votre base de données, zéro verrouillage fournisseur et une architecture de plugins qui vous permet d'ajouter des fonctionnalités progressivement.
Étape 1 : Créer un projet Next.js
Commencez par créer une application Next.js 15 :
npx create-next-app@latest better-auth-demo --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd better-auth-demoInstallez Better Auth et ses dépendances :
npm install better-auth
npm install -D @types/better-sqlite3Pour ce tutoriel, nous utiliserons PostgreSQL avec Drizzle ORM. Installez les dépendances de la base de données :
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kitÉtape 2 : Configurer la base de données
Créez un fichier .env.local à la racine du projet :
DATABASE_URL="postgresql://user:password@localhost:5432/better_auth_demo"
BETTER_AUTH_SECRET="votre-cle-secrete-au-moins-32-caracteres"
BETTER_AUTH_URL="http://localhost:3000"
GITHUB_CLIENT_ID="votre-github-client-id"
GITHUB_CLIENT_SECRET="votre-github-client-secret"
GOOGLE_CLIENT_ID="votre-google-client-id"
GOOGLE_CLIENT_SECRET="votre-google-client-secret"Générez une clé secrète sécurisée :
openssl rand -base64 32Créez la configuration de la base de données dans src/db/index.ts :
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);Étape 3 : Configurer le serveur Better Auth
Créez la configuration principale dans src/lib/auth.ts :
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: false, // Activez en production
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 jours
updateAge: 60 * 60 * 24, // Mise à jour toutes les 24h
cookieCache: {
enabled: true,
maxAge: 5 * 60, // Cache de 5 minutes
},
},
});Étape 4 : Créer le gestionnaire de routes API
Better Auth a besoin d'une route API pour gérer toutes les requêtes d'authentification. Créez src/app/api/auth/[...all]/route.ts :
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);C'est tout — Better Auth gère maintenant automatiquement toutes les routes sous /api/auth/*, notamment :
POST /api/auth/sign-up/email— Inscription par email/mot de passePOST /api/auth/sign-in/email— Connexion par email/mot de passeGET /api/auth/sign-in/social— Redirection OAuthPOST /api/auth/sign-out— DéconnexionGET /api/auth/session— Obtenir la session courante
Étape 5 : Générer les tables de la base de données
Better Auth peut générer le schéma de la base de données automatiquement. Exécutez :
npx better-auth generateCela crée un fichier de migration avec les tables requises : user, session, account et verification. Appliquez la migration :
npx drizzle-kit pushLe schéma généré comprend :
CREATE TABLE "user" (
"id" TEXT PRIMARY KEY,
"name" TEXT NOT NULL,
"email" TEXT UNIQUE NOT NULL,
"emailVerified" BOOLEAN DEFAULT FALSE,
"image" TEXT,
"createdAt" TIMESTAMP DEFAULT NOW(),
"updatedAt" TIMESTAMP DEFAULT NOW()
);
CREATE TABLE "session" (
"id" TEXT PRIMARY KEY,
"userId" TEXT NOT NULL REFERENCES "user"("id"),
"token" TEXT UNIQUE NOT NULL,
"expiresAt" TIMESTAMP NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT
);
CREATE TABLE "account" (
"id" TEXT PRIMARY KEY,
"userId" TEXT NOT NULL REFERENCES "user"("id"),
"accountId" TEXT NOT NULL,
"providerId" TEXT NOT NULL,
"accessToken" TEXT,
"refreshToken" TEXT,
"expiresAt" TIMESTAMP,
"password" TEXT
);
CREATE TABLE "verification" (
"id" TEXT PRIMARY KEY,
"identifier" TEXT NOT NULL,
"value" TEXT NOT NULL,
"expiresAt" TIMESTAMP NOT NULL
);Étape 6 : Créer le client d'authentification
Créez un helper d'authentification côté client dans src/lib/auth-client.ts :
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
});
export const {
signIn,
signUp,
signOut,
useSession,
} = authClient;Ajoutez NEXT_PUBLIC_APP_URL à votre .env.local :
NEXT_PUBLIC_APP_URL="http://localhost:3000"Étape 7 : Construire la page d'inscription
Créez un formulaire d'inscription dans src/app/auth/sign-up/page.tsx :
"use client";
import { useState } from "react";
import { signUp } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
import Link from "next/link";
export default function SignUpPage() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
const { error } = await signUp.email({
email,
password,
name,
});
if (error) {
setError(error.message || "Une erreur est survenue");
setLoading(false);
return;
}
router.push("/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">
<h2 className="text-3xl font-bold text-gray-900">Créer un compte</h2>
<p className="mt-2 text-gray-600">Commencez votre aventure</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
Nom complet
</label>
<input
id="name"
type="text"
required
value={name}
onChange={(e) => setName(e.target.value)}
className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Jean Dupont"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="jean@exemple.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Mot de passe
</label>
<input
id="password"
type="password"
required
minLength={8}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Au moins 8 caractères"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
>
{loading ? "Création en cours..." : "Créer un compte"}
</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</span>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => signIn.social({ provider: "github" })}
className="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
>
GitHub
</button>
<button
onClick={() => signIn.social({ provider: "google" })}
className="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
>
Google
</button>
</div>
<p className="text-center text-sm text-gray-600">
Vous avez déjà un compte ?{" "}
<Link href="/auth/sign-in" className="text-blue-600 hover:underline">
Se connecter
</Link>
</p>
</div>
</div>
);
}Étape 8 : Construire la page de connexion
Créez le formulaire de connexion dans src/app/auth/sign-in/page.tsx :
"use client";
import { useState } from "react";
import { signIn } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
import Link from "next/link";
export default function SignInPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
const { error } = await signIn.email({
email,
password,
});
if (error) {
setError(error.message || "Identifiants invalides");
setLoading(false);
return;
}
router.push("/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">
<h2 className="text-3xl font-bold text-gray-900">Bon retour</h2>
<p className="mt-2 text-gray-600">Connectez-vous à votre compte</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Mot de passe
</label>
<input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex items-center justify-between">
<label className="flex items-center">
<input type="checkbox" className="rounded border-gray-300" />
<span className="ml-2 text-sm text-gray-600">Se souvenir de moi</span>
</label>
<Link href="/auth/forgot-password" className="text-sm text-blue-600 hover:underline">
Mot de passe oublié ?
</Link>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
>
{loading ? "Connexion en cours..." : "Se connecter"}
</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</span>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<button
onClick={() => signIn.social({ provider: "github" })}
className="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
>
GitHub
</button>
<button
onClick={() => signIn.social({ provider: "google" })}
className="flex items-center justify-center px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition"
>
Google
</button>
</div>
<p className="text-center text-sm text-gray-600">
Pas encore de compte ?{" "}
<Link href="/auth/sign-up" className="text-blue-600 hover:underline">
Créer un compte
</Link>
</p>
</div>
</div>
);
}Étape 9 : Protéger les routes avec le middleware
Créez src/middleware.ts pour protéger les routes :
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
const protectedRoutes = ["/dashboard", "/settings", "/admin"];
const authRoutes = ["/auth/sign-in", "/auth/sign-up"];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isProtectedRoute = protectedRoutes.some((route) =>
pathname.startsWith(route)
);
const isAuthRoute = authRoutes.some((route) =>
pathname.startsWith(route)
);
const session = await auth.api.getSession({
headers: await headers(),
});
// Rediriger les utilisateurs non authentifiés vers la connexion
if (isProtectedRoute && !session) {
const signInUrl = new URL("/auth/sign-in", request.url);
signInUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(signInUrl);
}
// Rediriger les utilisateurs authentifiés loin des pages d'auth
if (isAuthRoute && session) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/admin/:path*", "/auth/:path*"],
};Étape 10 : Construire le tableau de bord avec les données de session
Créez un tableau de bord protégé dans src/app/dashboard/page.tsx :
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { SignOutButton } from "@/components/sign-out-button";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
redirect("/auth/sign-in");
}
const { user } = session;
return (
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16 items-center">
<h1 className="text-xl font-semibold">Tableau de bord</h1>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">{user.email}</span>
<SignOutButton />
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto py-12 px-4 sm:px-6 lg:px-8">
<div className="bg-white rounded-xl shadow-sm p-8">
<h2 className="text-2xl font-bold mb-6">
Bienvenue, {user.name} !
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="p-6 bg-blue-50 rounded-lg">
<h3 className="font-semibold text-blue-900">Profil</h3>
<p className="text-sm text-blue-700 mt-1">{user.email}</p>
<p className="text-xs text-blue-600 mt-2">
Vérifié : {user.emailVerified ? "Oui" : "Non"}
</p>
</div>
<div className="p-6 bg-green-50 rounded-lg">
<h3 className="font-semibold text-green-900">Session</h3>
<p className="text-sm text-green-700 mt-1">Active</p>
<p className="text-xs text-green-600 mt-2">
Expire : {new Date(session.session.expiresAt).toLocaleDateString("fr")}
</p>
</div>
<div className="p-6 bg-purple-50 rounded-lg">
<h3 className="font-semibold text-purple-900">Compte</h3>
<p className="text-sm text-purple-700 mt-1">
ID : {user.id.slice(0, 8)}...
</p>
<p className="text-xs text-purple-600 mt-2">
Inscrit : {new Date(user.createdAt).toLocaleDateString("fr")}
</p>
</div>
</div>
</div>
</main>
</div>
);
}Créez le composant de déconnexion dans src/components/sign-out-button.tsx :
"use client";
import { signOut } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
export function SignOutButton() {
const router = useRouter();
return (
<button
onClick={async () => {
await signOut();
router.push("/auth/sign-in");
}}
className="px-4 py-2 text-sm bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition"
>
Déconnexion
</button>
);
}Étape 11 : Ajouter le contrôle d'accès basé sur les rôles
Better Auth dispose d'un plugin RBAC intégré. Mettez à jour src/lib/auth.ts :
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin } from "better-auth/plugins";
import { db } from "@/db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
emailAndPassword: {
enabled: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
plugins: [
admin(), // Ajoute les champs role et banned à l'utilisateur
],
session: {
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60 * 24,
cookieCache: {
enabled: true,
maxAge: 5 * 60,
},
},
});Mettez à jour le client d'auth pour inclure le plugin admin dans src/lib/auth-client.ts :
import { createAuthClient } from "better-auth/react";
import { adminClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
plugins: [adminClient()],
});
export const { signIn, signUp, signOut, useSession } = authClient;Vous pouvez maintenant vérifier les rôles dans vos composants :
// Server Component
const session = await auth.api.getSession({
headers: await headers(),
});
if (session?.user.role !== "admin") {
redirect("/dashboard");
}
// Client Component
const { data: session } = useSession();
if (session?.user.role === "admin") {
// Afficher les contrôles admin
}Créez une page admin dans src/app/admin/page.tsx :
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function AdminPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session || session.user.role !== "admin") {
redirect("/dashboard");
}
const users = await auth.api.listUsers({
headers: await headers(),
});
return (
<div className="min-h-screen bg-gray-50 p-8">
<h1 className="text-3xl font-bold mb-8">Panneau d'administration</h1>
<div className="bg-white rounded-xl shadow-sm overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Nom
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Rôle
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Inscription
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{users?.users?.map((user) => (
<tr key={user.id}>
<td className="px-6 py-4 text-sm">{user.name}</td>
<td className="px-6 py-4 text-sm text-gray-600">{user.email}</td>
<td className="px-6 py-4">
<span className={`text-xs px-2 py-1 rounded-full ${
user.role === "admin"
? "bg-purple-100 text-purple-800"
: "bg-gray-100 text-gray-800"
}`}>
{user.role || "utilisateur"}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-600">
{new Date(user.createdAt).toLocaleDateString("fr")}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}Étape 12 : Ajouter la vérification d'email
Mettez à jour la configuration pour activer la vérification d'email :
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin } from "better-auth/plugins";
import { db } from "@/db";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
sendVerificationEmail: async ({ user, url }) => {
// En production, utilisez Resend, SendGrid ou AWS SES
console.log(`Email de vérification pour ${user.email}: ${url}`);
// Exemple avec Resend :
// await resend.emails.send({
// from: "auth@votredomaine.com",
// to: user.email,
// subject: "Vérifiez votre email",
// html: `<a href="${url}">Vérifier l'email</a>`,
// });
},
sendResetPassword: async ({ user, url }) => {
console.log(`Réinitialisation pour ${user.email}: ${url}`);
},
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
plugins: [admin()],
session: {
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60 * 24,
cookieCache: {
enabled: true,
maxAge: 5 * 60,
},
},
});Étape 13 : Construire le flux de mot de passe oublié
Créez src/app/auth/forgot-password/page.tsx :
"use client";
import { useState } from "react";
import { authClient } from "@/lib/auth-client";
import Link from "next/link";
export default function ForgotPasswordPage() {
const [email, setEmail] = useState("");
const [sent, setSent] = useState(false);
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
await authClient.forgetPassword({
email,
redirectTo: "/auth/reset-password",
});
setSent(true);
setLoading(false);
};
if (sent) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full p-8 bg-white rounded-xl shadow-lg text-center">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Vérifiez votre email</h2>
<p className="text-gray-600 mb-6">
Si un compte existe avec {email}, vous recevrez un lien de réinitialisation.
</p>
<Link href="/auth/sign-in" className="text-blue-600 hover:underline">
Retour à la connexion
</Link>
</div>
</div>
);
}
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">
<h2 className="text-3xl font-bold text-gray-900">Mot de passe oublié</h2>
<p className="mt-2 text-gray-600">
Entrez votre email et nous vous enverrons un lien de réinitialisation
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email
</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 px-4 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition disabled:opacity-50"
>
{loading ? "Envoi en cours..." : "Envoyer le lien"}
</button>
</form>
<p className="text-center text-sm text-gray-600">
<Link href="/auth/sign-in" className="text-blue-600 hover:underline">
Retour à la connexion
</Link>
</p>
</div>
</div>
);
}Étape 14 : Utiliser le hook useSession dans les composants client
Better Auth fournit un hook React prêt à l'emploi. Voici comment l'utiliser dans n'importe quel composant client :
"use client";
import { useSession } from "@/lib/auth-client";
export function UserAvatar() {
const { data: session, isPending } = useSession();
if (isPending) {
return <div className="w-8 h-8 rounded-full bg-gray-200 animate-pulse" />;
}
if (!session) {
return null;
}
return (
<div className="flex items-center gap-2">
{session.user.image ? (
<img
src={session.user.image}
alt={session.user.name}
className="w-8 h-8 rounded-full"
/>
) : (
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-sm font-bold">
{session.user.name.charAt(0).toUpperCase()}
</div>
)}
<span className="text-sm font-medium">{session.user.name}</span>
</div>
);
}Tester votre implémentation
Démarrez le serveur de développement :
npm run devTestez les flux suivants :
- Inscription : Naviguez vers
/auth/sign-up, créez un compte avec email et mot de passe - Connexion : Naviguez vers
/auth/sign-in, connectez-vous avec vos identifiants - Tableau de bord : Vérifiez que vos données utilisateur apparaissent sur
/dashboard - Déconnexion : Cliquez sur le bouton de déconnexion, vérifiez la redirection
- OAuth : Testez les boutons de connexion GitHub et Google
- Routes protégées : Essayez d'accéder à
/dashboarddéconnecté — redirection attendue - Routes d'auth : Essayez d'accéder à
/auth/sign-inconnecté — redirection vers le tableau de bord
Dépannage
Erreurs "session invalide"
Assurez-vous que BETTER_AUTH_SECRET est défini et cohérent entre les redémarrages. Si vous le changez, toutes les sessions existantes deviennent invalides.
Erreurs de callback OAuth
Vérifiez que vos URLs de callback dans les paramètres OAuth correspondent à :
- GitHub :
http://localhost:3000/api/auth/callback/github - Google :
http://localhost:3000/api/auth/callback/google
Problèmes de connexion à la base de données
Assurez-vous que votre DATABASE_URL est correcte et que la base est accessible. Exécutez npx drizzle-kit push pour vérifier que les tables existent.
Session non persistante
Vérifiez que les cookies sont correctement définis. Better Auth utilise des cookies préfixés __Secure- en production — assurez-vous que votre domaine supporte HTTPS.
Checklist de production
Avant le déploiement en production :
- Définir un
BETTER_AUTH_SECRETfort (au moins 32 caractères) - Activer
requireEmailVerification - Configurer un fournisseur d'email réel (Resend, SendGrid)
- Définir
BETTER_AUTH_URLsur votre domaine de production - Activer HTTPS (requis pour les cookies sécurisés)
- Configurer la limitation de débit sur les endpoints d'auth
- Configurer les en-têtes CORS si frontend séparé
- Tester toutes les URLs OAuth avec les domaines de production
- Activer le cache de cookies pour les performances
Prochaines étapes
Maintenant que vous avez un système d'authentification solide, envisagez :
- Authentification à deux facteurs : Ajoutez le plugin
twoFactorpour la 2FA basée sur TOTP - Liens magiques : Activez l'authentification sans mot de passe par email
- Support des organisations : Utilisez le plugin
organizationpour les apps multi-tenant - Limitation de débit : Ajoutez le plugin
rateLimitcontre les attaques par force brute - Passkeys : Activez l'authentification WebAuthn pour la connexion sans mot de passe
Conclusion
Better Auth offre une solution d'authentification puissante, type-safe et flexible pour les applications Next.js. Contrairement aux solutions rigides, elle vous donne un contrôle total sur votre schéma de base de données, la gestion des sessions et l'expérience utilisateur tout en gérant les détails de sécurité complexes pour vous.
Dans ce tutoriel, vous avez construit un système d'authentification complet avec email/mot de passe, OAuth, protection des routes, RBAC et vérification d'email. L'architecture de plugins signifie que vous pouvez ajouter progressivement des fonctionnalités comme la 2FA, les liens magiques et les organisations au fur et à mesure que votre application grandit.
Better Auth est rapidement devenu le choix de référence en 2026 pour les développeurs qui veulent une authentification simple à mettre en place et suffisamment puissante pour la production — sans verrouillage fournisseur.
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

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.

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 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.