Développement Web

Auth.js v5 et Clerk pour Next.js 15 : tutoriel d’integration

12 دقائق للقراءة

L’authentification est rarement le sujet le plus excitant d’un projet, mais c’est presque toujours le premier qui ralentit l’équipe. Deux approches dominent aujourd’hui dans l’écosystème Next.js 15 : Auth.js v5 (l’évolution open-source de NextAuth) pour ceux qui veulent garder la maîtrise totale, et Clerk pour ceux qui préfèrent déléguer la couche identité à un service managé. Ce tutoriel monte les deux pas-à-pas sur la même app de démo, puis donne des critères concrets pour trancher.

Les deux approches arrivent au même résultat fonctionnel — un utilisateur connecté, une session, des pages protégées — mais avec des compromis très différents. Auth.js v5 demande deux à trois heures de setup initial mais ne coûte rien en exploitation et garde tout chez vous. Clerk se déploie en quinze minutes avec une UI prête mais devient payant au-delà de 10 000 utilisateurs actifs mensuels.

Prérequis

  • Un projet Next.js 15.x avec App Router
  • Une base PostgreSQL accessible (locale ou cloud type Neon, Supabase, Railway)
  • Prisma installé pour la voie Auth.js (pnpm add prisma @prisma/client)
  • Comprendre les Server Components et l’App Router
  • Niveau : intermédiaire
  • Temps estimé : 90 à 120 minutes pour suivre les deux options

Voie A — Auth.js v5

Étape 1 — Installation et fichier de configuration

Au moment de la rédaction, Auth.js v5 est en version 5.0.0-beta.x. La beta est utilisée en production par des milliers d’apps, l’API est stabilisée, mais le tag beta persiste jusqu’à la dernière vague de polish. L’installation se fait avec le tag @beta.

pnpm add next-auth@beta @auth/prisma-adapter
pnpm add -D @types/bcrypt
pnpm add bcrypt zod

On crée ensuite le fichier de configuration central. Convention v5 : un seul auth.ts à la racine du dossier src/ qui exporte handlers, auth, signIn, signOut.

// src/auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Credentials from 'next-auth/providers/credentials';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcrypt';
import { z } from 'zod';

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: 'jwt' },
  pages: { signIn: '/login' },
  providers: [
    GitHub,
    Credentials({
      credentials: { email: {}, password: {} },
      async authorize(creds) {
        const schema = z.object({ email: z.string().email(), password: z.string().min(6) });
        const parsed = schema.safeParse(creds);
        if (!parsed.success) return null;
        const user = await prisma.user.findUnique({ where: { email: parsed.data.email } });
        if (!user || !user.passwordHash) return null;
        const ok = await bcrypt.compare(parsed.data.password, user.passwordHash);
        return ok ? { id: user.id, email: user.email, name: user.name } : null;
      },
    }),
  ],
});

Trois choses à noter. Le strategy: 'jwt' stocke la session dans un cookie signé côté client plutôt qu’en base — plus rapide, et nécessaire si on veut consulter la session depuis un middleware Edge. Le PrismaAdapter n’est utilisé que pour les comptes OAuth (GitHub) et la gestion des utilisateurs ; les sessions JWT n’écrivent rien en base. Le provider Credentials permet l’auth email/mot de passe classique, à valider et hacher soi-même.

Étape 2 — Variables d’environnement et schéma Prisma

Trois variables sont obligatoires.

# .env.local
AUTH_SECRET=...                    # generer avec: openssl rand -base64 32
AUTH_GITHUB_ID=...                 # GitHub OAuth App
AUTH_GITHUB_SECRET=...
DATABASE_URL=postgres://...

Le schéma Prisma doit inclure les modèles attendus par l’adapter Auth.js (User, Account, Session, VerificationToken) plus le champ passwordHash pour les credentials.

// prisma/schema.prisma — extrait
model User {
  id            String    @id @default(cuid())
  email         String    @unique
  name          String?
  image         String?
  passwordHash  String?
  emailVerified DateTime?
  accounts      Account[]
  sessions      Session[]
}

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  // ...autres champs standards de l'adapter
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  @@unique([provider, providerAccountId])
}

Lancer pnpm prisma migrate dev --name init-auth pour appliquer le schéma. Le détail exact des modèles requis est disponible dans la documentation de l’adapter Prisma — copier-coller depuis la doc évite les surprises.

Étape 3 — Route handler et middleware

Auth.js v5 expose ses endpoints via un Route Handler unique à app/api/auth/[...nextauth]/route.ts.

// src/app/api/auth/[...nextauth]/route.ts
export { GET, POST } from '@/auth';

Pour protéger des routes côté Edge (avant même que la page rende), on utilise un middleware qui appelle auth.

// src/middleware.ts
import { auth } from '@/auth';
import { NextResponse } from 'next/server';

export default auth((req) => {
  const isProtected = req.nextUrl.pathname.startsWith('/compte');
  if (isProtected && !req.auth) {
    return NextResponse.redirect(new URL('/login', req.url));
  }
});

export const config = { matcher: ['/compte/:path*'] };

Côté Server Component, on lit la session via la fonction auth().

// src/app/compte/page.tsx
import { auth } from '@/auth';

export default async function Compte() {
  const session = await auth();
  return <p>Bonjour {session?.user?.name}</p>;
}

Pour le bouton de logout, on appelle signOut depuis une Server Action ou un Client Component. C’est l’intégration la plus propre avec le reste du framework.

Voie B — Clerk

Étape 4 — Installation et ClerkProvider

Clerk a fait du chemin depuis ses débuts : le SDK @clerk/nextjs est aujourd’hui en série 7.x avec support natif et complet de l’App Router et React 19. L’installation prend une commande.

pnpm add @clerk/nextjs

On crée un compte sur clerk.com (gratuit jusqu’à 50 000 MAU), on crée une application, et on récupère deux clés à mettre dans .env.local.

# .env.local
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...

On enveloppe le layout racine dans <ClerkProvider>.

// src/app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="fr">
        <body>{children}</body>
      </html>
    </ClerkProvider>
  );
}

À ce stade, Clerk est opérationnel. Aucune base, aucun schéma, aucun provider à configurer — tout est géré côté Clerk Dashboard.

Étape 5 — Composants prêts à l’emploi

Clerk fournit des composants React qui rendent les UIs de sign-in, sign-up, et user profile. Ils s’utilisent comme n’importe quel composant.

// src/app/login/[[...sign-in]]/page.tsx
import { SignIn } from '@clerk/nextjs';

export default function LoginPage() {
  return <SignIn />;
}

Le double [[...sign-in]] est un segment catch-all optionnel — Clerk l’utilise pour gérer ses sous-routes internes (mot de passe oublié, MFA, etc.). Identique pour /signup/[[...sign-up]]/page.tsx avec <SignUp />.

Pour le menu utilisateur (avatar + dropdown logout + profil), il suffit de placer <UserButton /> dans le header.

// src/app/layout.tsx — header
import { SignedIn, SignedOut, UserButton, SignInButton } from '@clerk/nextjs';

<header className="flex justify-between p-4 border-b">
  <a href="/">Boutique</a>
  <div>
    <SignedIn><UserButton /></SignedIn>
    <SignedOut><SignInButton /></SignedOut>
  </div>
</header>

Les composants <SignedIn> et <SignedOut> rendent leur contenu conditionnellement. Zéro logique d’état à écrire.

Étape 6 — Middleware et auth() côté serveur

Clerk fournit son propre middleware qui protège les routes en une ligne.

// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';

const isProtected = createRouteMatcher(['/compte(.*)', '/admin(.*)']);

export default clerkMiddleware(async (auth, req) => {
  if (isProtected(req)) await auth.protect();
});

export const config = {
  matcher: ['/((?!_next|[^?]*\\.(?:html?|css|js|jpg|png|svg|gif|webp|ico)).*)', '/(api|trpc)(.*)', '/__clerk/(.*)'],
};

Côté Server Component, on lit la session via la fonction auth() exposée par Clerk.

// src/app/compte/page.tsx
import { auth, currentUser } from '@clerk/nextjs/server';

export default async function Compte() {
  const { userId } = await auth();
  if (!userId) return <p>Non connecte</p>;
  const user = await currentUser();
  return <p>Bonjour {user?.firstName}</p>;
}

L’API auth() est intentionnellement très proche de celle d’Auth.js pour faciliter une migration éventuelle dans un sens ou l’autre.

Étape 7 — Comment trancher entre les deux

La grille de décision se résume à six questions concrètes.

Question Auth.js Clerk
Budget mensuel pour l’auth ? 0 € Gratuit jusqu’à 10k MAU, ~25 $/mois ensuite
Volume utilisateurs estimé ? Idéal > 50k Idéal < 50k
Besoin UI prête (sign-in, MFA, organisations) ? À coder soi-même Fourni
Souveraineté des données utilisateur ? Tout chez vous Hébergé chez Clerk (USA principalement)
Temps disponible pour le setup ? 2 à 4 heures + maintenance 15 minutes + zéro maintenance
Besoin de personnaliser le flow d’auth ? Total Limité (mais croissant)

Cas typiques : B2C grand public à fort volume → Auth.js. B2B SaaS en early stage → Clerk. Plateforme avec utilisateurs sensibles (santé, finance) où la souveraineté importe → Auth.js. Side-project ou MVP qu’on veut lancer ce week-end → Clerk.

Erreurs fréquentes

Erreur Cause Solution
« AUTH_SECRET is required » au démarrage Auth.js Variable manquante Générer avec openssl rand -base64 32 et l’ajouter à .env.local
Le bouton GitHub renvoie une page d’erreur 400 sur le callback URL de callback mal configurée dans l’OAuth App GitHub Vérifier que l’URL exacte https://votre-domaine/api/auth/callback/github est enregistrée
Session toujours null après login Cookie pas envoyé en HTTPS, ou domaine mal configuré En prod, vérifier que le cookie est en Secure, le domaine match, et NEXTAUTH_URL est correct
Clerk : « Publishable key required » Variable manquante ou pas préfixée NEXT_PUBLIC_ Toujours préfixer la clé publique avec NEXT_PUBLIC_ pour qu’elle soit injectée côté client
Middleware Clerk qui bloque les assets statiques Matcher trop large Reprendre le matcher exact de la doc officielle, qui exclut _next et les extensions communes
Variable NEXTAUTH_URL oubliée en production Vercel Auth.js en mode prod a besoin de l’URL exacte Définir AUTH_URL (v5 utilise AUTH_URL, plus NEXTAUTH_URL de la v4) dans les variables d’environnement Vercel ou Coolify

Étape 8 — Rôles et permissions (RBAC) avec Auth.js

Au-delà du simple connecté/déconnecté, presque toute app sérieuse a besoin de rôles : admin, modérateur, utilisateur standard. Auth.js v5 facilite cela via les callbacks JWT et session, qui permettent d’enrichir l’objet session avec n’importe quel champ.

// src/auth.ts — extension
export const { handlers, auth, signIn, signOut } = NextAuth({
  // ...config précédente
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        const dbUser = await prisma.user.findUnique({
          where: { id: user.id },
          select: { role: true },
        });
        token.role = dbUser?.role ?? 'user';
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) session.user.role = token.role as string;
      return session;
    },
  },
});

Il faut étendre les types TypeScript pour que session.user.role soit reconnu. On crée src/types/next-auth.d.ts qui augmente le module next-auth avec le champ role. La doc officielle fournit l’exemple exact à copier-coller. Une fois en place, const session = await auth() donne accès à session.user.role partout dans l’app.

Pour les contrôles d’accès fins (un éditeur ne peut modifier que ses propres articles), on combine le rôle avec une vérification de propriété dans la Server Action elle-même. Le rôle filtre l’accès grossier (admin/non-admin), la vérification d’ownership protège le détail.

Étape 9 — Organisations multi-tenant avec Clerk

Clerk brille particulièrement sur le multi-tenant. Activer les Organizations dans le dashboard Clerk donne en un clic : invitations par email, rôles par organisation (admin, member), facturation par organisation, et changement d’organisation actif depuis l’UI.

// src/app/admin/page.tsx
import { auth } from '@clerk/nextjs/server';

export default async function Admin() {
  const { userId, orgId, orgRole } = await auth();
  if (!userId) return <p>Non connecte</p>;
  if (orgRole !== 'admin') return <p>Reserve aux admins de l'organisation</p>;
  return <p>Dashboard admin de l'organisation {orgId}</p>;
}

Le composant <OrganizationSwitcher /> permet à l’utilisateur de basculer entre ses organisations sans recharger la page. Le composant <OrganizationProfile /> fournit l’UI de gestion (membres, invitations, settings) en quelques lignes. Pour un SaaS B2B où chaque client est une organisation, ces composants couvrent 80 % des besoins UI dès le premier jour.

Étape 10 — Stratégie de migration entre les deux

Démarrer sur Clerk pour valider rapidement le marché, puis migrer vers Auth.js si le volume justifie le travail. La migration est faisable mais demande un plan en quatre étapes : exporter les utilisateurs via l’API Clerk (incluant emails et identifiants externes OAuth), créer les enregistrements correspondants en base via le PrismaAdapter d’Auth.js, mettre en place un script de re-vérification d’email (les hash de mot de passe Clerk ne sont pas exportables, donc reset obligatoire pour les comptes Credentials), et basculer le code applicatif progressivement en gardant les deux systèmes actifs deux à quatre semaines pour absorber les sessions en cours.

L’inverse (Auth.js → Clerk) est plus simple côté code mais demande d’inviter chaque utilisateur à se ré-enregistrer côté Clerk, ce qui se traduit par une perte d’utilisateurs en pourcentage (typiquement 10 à 30 % qui ne reviennent pas). À planifier en parallèle d’une campagne emailing explicite.

Pour aller plus loin

Cet article fait partie d’une série sur React 19 et Next.js 15. Pour la vue d’ensemble et le parcours conseillé, consultez React 19 + Next.js 15 : architecture full-stack moderne.

Tutoriels frères :

Ressources officielles :

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité