Système d’Authentification
Overview - better-auth
Wagoo utilise better-auth, un framework d’authentification moderne et sécurisé.
Plugins Activés
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins/two-factor";
import { passkey } from "better-auth/plugins/passkey";
import { magicLink } from "better-auth/plugins/magic-link";
import { emailVerification } from "better-auth/plugins/email-verification";
import { socialProviders } from "better-auth/plugins/social-providers";
export const auth = betterAuth({
database: prisma,
secret: process.env.BETTER_AUTH_SECRET,
plugins: [
twoFactor(),
passkey(),
magicLink(),
emailVerification(),
socialProviders({
providers: {
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 }
}
})
]
});Flux d’Authentification
Sign Up (Inscription)
1. User → Remplit email + password
2. Form validation (client + serveur)
3. Vérifie l'unicité de l'email
4. Hash le password avec bcrypt
5. Crée l'user en DB
6. Envoie email de vérification
7. Session créée
8. Redirection vers dashboard/verify-emailSign In (Connexion)
1. User → Remplit email + password
2. Trouve l'user par email
3. Compare password avec bcrypt
4. Crée une session
5. Set cookie de session
6. Redirection vers dashboardSign Out (Déconnexion)
1. Supprime la session en DB
2. Efface le cookie
3. Redirection vers homeMéthodes d’authentification
1. Email/Password
Prérequis : Email + Password
// Sign Up
const { user, session } = await auth.signUp({
email: "user@example.com",
password: "SecurePassword123"
});
// Sign In
const { user, session } = await auth.signIn({
email: "user@example.com",
password: "SecurePassword123"
});2. Magic Link
Prérequis : Juste l’email
// Envoyer un lien magique
await auth.sendMagicLink({
email: "user@example.com"
});
// User clique le lien → Connexion automatique3. OAuth (GitHub)
Prérequis : Compte GitHub
// Redirection vers GitHub
window.location.href = auth.getOAuthRedirectURL("github");
// Après authentification GitHub
// → Session créée automatiquement4. OAuth (Google)
Prérequis : Compte Google
// Redirection vers Google
window.location.href = auth.getOAuthRedirectURL("google");
// Après authentification Google
// → Session créée automatiquement5. Passkeys (Biométrique)
Prérequis : Appareil avec support passkey
// Enregistrer un passkey
await auth.registerPasskey();
// Sign in avec passkey
await auth.signInWithPasskey();6. 2FA (TOTP)
Prérequis : Email/Password + Authenticator app
// Activer 2FA
const { secret, qrCode } = await auth.enableTwoFactor();
// Vérifier le code TOTP
await auth.verifyTwoFactor({
code: "123456"
});
// Sign In avec 2FA
const { user, session } = await auth.signIn({
email: "user@example.com",
password: "SecurePassword123",
totpCode: "123456"
});Tables d’authentification
better-auth crée automatiquement ces tables :
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refreshToken String?
accessToken String?
expiresAt Int?
tokenType String?
scope String?
idToken String?
sessionToken String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation("accounts", fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
userId String
expiresAt DateTime
token String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation("sessions", fields: [userId], references: [id], onDelete: Cascade)
}
model Verification {
id String @id @default(cuid())
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([identifier, token])
}Middleware d’authentification
Protéger les routes
// app/api/projects/route.ts
import { auth } from "@/lib/auth";
export async function GET(request: Request) {
// Récupérer la session
const session = await auth.getSession(request);
if (!session) {
return Response.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
// Session valide, continuer...
const projects = await prisma.project.findMany({
where: { ownerId: session.user.id }
});
return Response.json(projects);
}Protéger les pages
// app/dashboard/page.tsx
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
export default async function DashboardPage() {
const session = await auth.getSession();
if (!session) {
redirect("/auth/sign-in");
}
return <div>Welcome {session.user.email}</div>;
}Sécurité
Hachage des mots de passe
✅ Utilise bcrypt avec salt aléatoire ✅ Les mots de passe ne sont jamais stockés en clair ✅ Difficile à craquer (millions d’années en brute force)
Tokens de session
✅ Tokens générés aléatoirement (256 bits) ✅ Stockés en httpOnly cookies (inaccessibles en JavaScript) ✅ Expiration configurable (défaut : 7 jours)
Protection CSRF
✅ Intégré dans Next.js ✅ Cookies SameSite=Strict ✅ CSRF tokens en POST requests
Email verification
✅ Tokens d’vérification uniques ✅ Expiration après 24h ✅ Invalide après utilisation
Rate limiting (recommandé)
// Limiter les tentatives de sign-in
import { Ratelimit } from "@upstash/ratelimit";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, "10 m"),
});
const { success } = await ratelimit.limit(email);
if (!success) return Response.json({ error: "Too many requests" }, { status: 429 });Configuration des OAuth
GitHub
- Settings → Developer settings → New OAuth AppÂ
- Authorization callback URL:
http://localhost:3000/api/auth/callback/github - Copier
Client IDetClient Secretdans.env.local
- Google Cloud Console → New ProjectÂ
- APIs & Services → Credentials
- Authorized redirect URI:
http://localhost:3000/api/auth/callback/google - Copier
Client IDetSecretdans.env.local
Session & Cookies
better-auth gère automatiquement :
✅ Création de sessions ✅ Stockage sécurisé des cookies ✅ Refresh tokens ✅ Expiration des sessions
Exemple complet : Sign Up
// app/auth/sign-up/page.tsx
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
export default function SignUpPage() {
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError("");
try {
const response = await fetch("/api/auth/sign-up", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
const data = await response.json();
setError(data.error || "Une erreur est survenue");
return;
}
// Redirection vers verify email
router.push("/auth/verify-email");
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit" disabled={loading}>
{loading ? "Inscription..." : "S'inscrire"}
</button>
</form>
);
}Voir Sécurité pour les bonnes pratiques de sécurité supplémentaires.