EnglishBac à sable

Authentification

Trois façons de connecter quelqu'un, et elles partagent presque tout. Se tromper de choix coûte une seule fonction, pas l'application.

        comment des identifiants deviennent un token   <- seul ceci change
                        |
        @fluixi/session      token, utilisateur, statut, rafraîchissement, rôles, garde 401
                        |
        @fluixi/auth/client  useUser, protectedRoute, Can
                        |
                    votre interface

@fluixi/session possède la machine à états de la session. @fluixi/auth/client possède les guards et les lectures. Ni l'un ni l'autre ne sait comment vous vous êtes authentifié, et c'est pour cela que la couche du dessus reste identique quel que soit le chemin choisi.

Lequel est le vôtre

La question qui tranche : qui détient les comptes utilisateurs.

Vous avez Utilisez Page
Votre propre page de connexion et votre base @fluixi/session Sessions
La page de connexion d'un fournisseur : Keycloak, Auth0, Okta, Curity @fluixi/oauth2 OAuth2 et OIDC
Une application qui est le système d'identité better-auth avec @fluixi/auth Better Auth

Ce que chacun coûte

@fluixi/session. Vous écrivez la vérification du mot de passe, la table des utilisateurs, les routes de connexion et leur rate limiting. En échange vous gardez votre schéma et ne prenez aucune dépendance au-delà du framework. @fluixi/session/server fournit le cookie de session, donc vous n'écrivez pas de cryptographie.

@fluixi/oauth2. Pas de table utilisateurs, pas de mots de passe à manipuler : le fournisseur s'en charge. Fonctionne avec n'importe quel fournisseur OAuth2 ou OIDC, et en mode navigateur ne demande aucun serveur, donc un déploiement statique suffit. La contrepartie est que la page de connexion ne vous appartient pas.

better-auth avec @fluixi/auth. Liaison de comptes, double facteur, organisations, et mot de passe à côté du SSO, tout est fourni. Cela demande une base de données et un serveur, car cette bibliothèque crée ses propres comptes et ses propres sessions même quand la connexion est déléguée à un fournisseur.

Une précision sur ce dernier point, facile à mal lire : 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, stockée dans sa base. Si le fournisseur doit être le seul système d'identité, @fluixi/oauth2 est la réponse plus légère.

Ce qui ne change pas

Quel que soit le choix, la couche d'interface est identique :

import { createAuthHooks } from '@fluixi/auth/client';
import { useNavigate } from '@fluixi/start/router';

export const { protectedRoute, Can, useUser } = createAuthHooks({
  session: auth,
  navigate: { handler: useNavigate },
  loginPath: '/login',
});
// une page entière, derrière un rôle
export default protectedRoute(AdminPage, { role: 'admin' });

// un seul contrôle, derrière une permission
<Can perform={(p) => p.invoices.refund} fallback={null}>
  <button onClick={refund}>Rembourser</button>
</Can>

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

createAuthHooks accepte n'importe quel objet exposant un accesseur user(), donc les trois chemins l'alimentent. @fluixi/oauth2 va plus loin et renvoie les guards depuis le service lui-même : une application qui l'utilise déclare son authentification dans un seul fichier.

Protect, protectedRoute et Can

Trois guards qui se ressemblent et qui ne font pas la même chose.

Can Protect protectedRoute
Demande ce que vous pouvez faire qui vous êtes qui vous êtes
Portée un contrôle une section de page la page entière
Si refusé rend fallback rend fallback redirige

La conséquence à retenir : avec protectedRoute le composant ne s'exécute jamais, donc une page qui charge des données au montage ne les charge pas pour quelqu'un qui ne doit pas les voir. Protect laisse le reste de la page intact et remplace une seule zone.

Tout ce que renvoient les hooks

Hook Ce qu'il donne
useUser() l'utilisateur connecté, ou null, en signal
useSession() l'enregistrement de session à côté de l'utilisateur : son expiration, et ce que le backend a envoyé d'autre
useAuth() user, session, isAuthenticated, roles, can, signIn, signOut en un objet
useSignIn() démarre une connexion ; des credentials pour Better Auth, sinon ce que prend le login de la session
useSignOut() termine la session
usePermissions() l'ensemble des permissions des rôles courants
useCan() can(query) : l'utilisateur courant peut-il faire ceci. false quand aucun rôle n'est configuré, donc une configuration manquante cache un contrôle au lieu de le montrer
refresh() relit la session, pour un changement fait hors de cet onglet
store le store sous-jacent, pour un test qui injecte un utilisateur

usePermissions et useCan décident de ce qui s'affiche et de rien d'autre. Un client peut revendiquer n'importe quelle permission : c'est le serveur qui décide de ce qui est permis, à chaque requête.

const can = useCan();

<button disabled={() => !can((p) => p.users.delete)}>Delete</button>

Côté serveur

Une fonction serveur ou une route d'API interroge la requête, pas les hooks :

import { requireUser, requireRole, requirePermission, optionalUser } from '@fluixi/auth/server';

export async function deleteUser(id: string) {
  "use server";
  const admin = await requireRole('admin');
  // ...
}
Helper Renvoie Refuse avec
optionalUser() l'utilisateur, ou null jamais
requireUser({ redirectTo? }) l'utilisateur 401, redirectTo valant /login par défaut
requireRole(role, { redirectTo? }) l'utilisateur 401 si déconnecté, 403 si le rôle diffère
requirePermission(check, { redirectTo? }) rien 401 si déconnecté, 403 quand check (un booléen ou une fonction) est faux

Ils lèvent une AuthError qui porte le statut, et une fonction serveur ou une route d'API y répond avec ce statut et { error, redirectTo } en corps, pas avec un 500.

Ils lisent l'utilisateur dans les locals de la requête, il faut donc que quelque chose l'y ait mis. Avec Better Auth, c'est authMiddleware ; avec OAuth2 en mode serveur, c'est sessionLocals, que monte authServerModule. En mode navigateur d'OAuth2, le serveur n'a pas de session à lire : vérifiez plutôt l'access token, avec Vérification des tokens.

La limite qui n'est pas optionnelle

Tout ce qui précède décide de ce qu'il faut afficher. Rien de tout cela ne décide de ce qui est autorisé.

Un client peut revendiquer n'importe quel rôle, puisque c'est du code sur la machine de quelqu'un d'autre. Lisez can(...) comme « faut-il montrer ce bouton », jamais comme « cette personne a-t-elle le droit ». Le serveur revérifie à chaque requête, et c'est cette vérification qui protège réellement. Voir Vérification des tokens pour l'autre côté.

Suite : Sessions.