EnglishBac à sable

Better Auth

@fluixi/auth intègre Better Auth, pour une application qui est le système d'identité : sa propre table d'utilisateurs, la liaison de comptes, le double facteur, les organisations, et le mot de passe à côté de la connexion sociale.

Better Auth possède les utilisateurs, les sessions et les routes. @fluixi/auth le relie à Fluixi : un middleware qui résout la session par requête, un handler à monter, et les mêmes hooks que les deux autres approches.

C'est la plus lourde des trois, et la bonne quand vous voulez une table d'utilisateurs que vous contrôlez. Si un fournisseur doit être le seul système d'identité, OAuth2 est plus léger. Si vous voulez écrire la connexion vous-même, Sessions l'est encore plus.

Installation

better-auth est une peer dependency, vous choisissez donc la version :

npm i @fluixi/auth better-auth

Elle demande une base de données. Suivez la documentation de Better Auth pour l'adaptateur et le schéma ; cette page ne couvre que le côté Fluixi.

Le serveur

Créez l'instance Better Auth comme sa documentation l'indique, puis passez-la une fois à defineAuth :

// src/lib/auth.server.ts
import { betterAuth } from 'better-auth';
import { defineAuth } from '@fluixi/auth/server';

export const auth = defineAuth(
  betterAuth({
    database: /* votre adaptateur */,
    emailAndPassword: { enabled: true },
    socialProviders: {
      github: { clientId: process.env.GITHUB_ID!, clientSecret: process.env.GITHUB_SECRET! },
    },
  }),
);

defineAuth enregistre l'instance pour que le middleware et le handler de routes la trouvent, et renvoie un petit wrapper :

auth.raw          // l'instance Better Auth, pour ce que le wrapper ne couvre pas
auth.handler      // à monter sur /api/auth/*
auth.getSession   // résout la session d'une requête

Monter les routes

Better Auth sert ses propres endpoints pour la connexion, l'inscription, la déconnexion, les callbacks OAuth et le reste. Donnez-lui la route catch-all :

// src/api/auth/[...all].ts
import { mountAuthRoutes } from '@fluixi/auth/server';

const handler = mountAuthRoutes();

export const GET = handler;
export const POST = handler;

Un serveur construit ne distribue vers src/api que si l'entrée serveur réexporte les gestionnaires, donc vérifiez la présence de cette ligne :

// src/entry-server.ts
export { isApiRequest, handleApiRequest } from '@fluixi/start/api-routes';

Les applications générées la contiennent, et une compilation échoue en nommant la ligne si elle manque.

Le middleware

// src/middleware.ts
import { defineMiddleware } from '@fluixi/start';
import { authMiddleware } from '@fluixi/auth/server';
import './lib/auth.server.js';   // pour que defineAuth se soit exécuté

export default defineMiddleware([authMiddleware()]);

authMiddleware résout la session et la place dans les locals de la requête, sous la clé que lisent les hooks côté client. C'est ce qui fait répondre useUser() pendant un rendu serveur au lieu de se remplir après l'hydratation, et ce qui permet à un guard de refuser une page avant qu'aucun HTML ne soit produit.

Importer le module qui appelle defineAuth compte. Sans cela le middleware n'a aucune instance à interroger.

Le client

// src/lib/auth.ts
import { createAuthClient } from 'better-auth/client';
import { createAuthHooks } from '@fluixi/auth/client';
import { useNavigate } from '@fluixi/start/router';

const client = createAuthClient({ baseURL: '/api/auth' });

export const { protectedRoute, Can, Protect, useUser, useAuth, useSignIn, useSignOut } =
  createAuthHooks({
    client,
    navigate: { handler: useNavigate },
    loginPath: '/login',
    homePath: '/dashboard',
  });

Notez client et non session. createAuthHooks accepte les deux : un client Better Auth, ou n'importe quelle session réactive comme celles que produisent @fluixi/session et @fluixi/oauth2. C'est pourquoi les pages voisines décrivent les mêmes guards.

Se connecter

export default function Login() {
  const signIn = useSignIn();
  const email = signal('');
  const password = signal('');

  return (
    <form onSubmit={(e) => { e.preventDefault(); void signIn({ email: email(), password: password() }); }}>
      <input type="email" value={email()} onInput={(e) => email.set(e.currentTarget.value)} />
      <input type="password" value={password()} onInput={(e) => password.set(e.currentTarget.value)} />
      <button type="submit">Se connecter</button>
    </form>
  );
}

Contrairement au flux OAuth2, le formulaire est le vôtre et les identifiants vont à votre propre serveur. Better Auth les vérifie, crée la session et pose son cookie.

Lire et protéger

Identique aux autres approches :

export default protectedRoute(Dashboard);
export default protectedRoute(AdminPage, { role: 'admin' });

const user = useUser();
<span>{() => user()?.email ?? 'Déconnecté'}</span>

const signOut = useSignOut();
<button onClick={() => signOut()}>Se déconnecter</button>

Connexion sociale

Configurez le fournisseur sur l'instance Better Auth, puis démarrez le flux via son client. Better Auth gère la redirection et le callback sur ses propres routes : il n'y a donc aucune page de callback à écrire.

await client.signIn.social({ provider: 'github', callbackURL: '/dashboard' });

La session qui en résulte est celle de Better Auth, stockée dans votre base, avec un compte lié au compte GitHub. C'est la différence avec @fluixi/oauth2, où le fournisseur reste le seul système d'identité et où rien n'est stocké localement.

Les rôles

Better Auth place un role sur l'utilisateur quand vous en configurez un. Les hooks le lisent sans configuration supplémentaire :

useAuth().roles();   // ['admin'], depuis user.role

Pour un modèle de permissions plus riche, donnez à createAuthHooks une session dont vous contrôlez le can, ou dérivez les permissions dans votre propre code. Les aides RBAC de Sessions en décrivent la forme.

Comme partout, un rôle côté client décide de ce qu'il faut afficher. Ce sont les contrôles serveur de Better Auth qui décident de ce qui est autorisé.

Lectures côté serveur

Dans une fonction serveur ou une route d'API, interrogez l'instance directement plutôt que les hooks :

import { getAuth } from '@fluixi/auth/server';

export async function GET(request: Request) {
  const { user } = await getAuth().getSession(request);
  if (!user) return new Response(null, { status: 401 });

  return Response.json(await ordersFor(user.id));
}

Quand authMiddleware s'est déjà exécuté, la même session est dans les locals de la requête, donc un rendu la lit sans seconde recherche.

Pour refuser plutôt que lire, requireUser, requireRole et requirePermission répondent 401 ou 403 à votre place ; voir Authentification.

Choisir entre celle-ci et les autres

Better Auth @fluixi/oauth2 @fluixi/session
Comptes utilisateurs à vous, dans votre base aucun, le fournisseur les détient à vous
Sessions celles de Better Auth les vôtres les vôtres
Page de connexion la vôtre celle du fournisseur la vôtre
Base de données requise oui non pour le cookie
Serveur requis oui non, en mode navigateur pour le cookie
Liaison de comptes, 2FA, orgs fourni non à écrire

Better Auth sait déléguer la connexion à Keycloak via son plugin OAuth générique, mais la session qu'il émet reste la sienne. Le faire tourner uniquement pour relayer la connexion d'un fournisseur revient à tenir deux stockages d'identité là où un seul suffirait.

Suite : Service à service.