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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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 AuthAuth.js v5JWT Personnalisé
Sécurité des typesInférence TypeScript complètePartielleManuelle
Dépendance au frameworkAucuneCentré Next.jsAucune
Contrôle de la BDDVous la possédezBasé sur des adaptateursVous la possédez
Fournisseurs OAuth20+ intégrés80+ intégrésManuel
2FA / MFAPlugin intégréCommunautaireManuel
RBACPlugin intégréManuelManuel
Vérification emailIntégréeIntégréeManuelle
Stratégie de sessionCookie + BDDJWT ou BDDJWT
Taille du bundle~15KB~30KB~2KB
Courbe d'apprentissageFaibleMoyenneÉ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-demo

Installez Better Auth et ses dépendances :

npm install better-auth
npm install -D @types/better-sqlite3

Pour 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 32

Cré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 passe
  • POST /api/auth/sign-in/email — Connexion par email/mot de passe
  • GET /api/auth/sign-in/social — Redirection OAuth
  • POST /api/auth/sign-out — Déconnexion
  • GET /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 generate

Cela crée un fichier de migration avec les tables requises : user, session, account et verification. Appliquez la migration :

npx drizzle-kit push

Le 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 dev

Testez les flux suivants :

  1. Inscription : Naviguez vers /auth/sign-up, créez un compte avec email et mot de passe
  2. Connexion : Naviguez vers /auth/sign-in, connectez-vous avec vos identifiants
  3. Tableau de bord : Vérifiez que vos données utilisateur apparaissent sur /dashboard
  4. Déconnexion : Cliquez sur le bouton de déconnexion, vérifiez la redirection
  5. OAuth : Testez les boutons de connexion GitHub et Google
  6. Routes protégées : Essayez d'accéder à /dashboard déconnecté — redirection attendue
  7. Routes d'auth : Essayez d'accéder à /auth/sign-in connecté — 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_SECRET fort (au moins 32 caractères)
  • Activer requireEmailVerification
  • Configurer un fournisseur d'email réel (Resend, SendGrid)
  • Définir BETTER_AUTH_URL sur 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 twoFactor pour la 2FA basée sur TOTP
  • Liens magiques : Activez l'authentification sans mot de passe par email
  • Support des organisations : Utilisez le plugin organization pour les apps multi-tenant
  • Limitation de débit : Ajoutez le plugin rateLimit contre 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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Integration d'ALLaM-7B-Instruct-preview avec Ollama.

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·