Dans le boilerplate que j'utilise pour démarrer mes projets, le commit qui intègre le premier fournisseur d'auth et celui qui le remplace par le deuxième sont séparés de deux jours. Deux jours. Le premier ajoutait un middleware pour les routes protégées et un écran de login ; le deuxième l'arrachait entièrement et posait un autre SDK avec ses propres pages de sign-in et de sign-up. Depuis, ce même gabarit est passé par deux fournisseurs de plus et a fini, presque un an après, avec un middleware écrit à la main.
Ce n'est pas de la malchance. C'est un motif, et je l'ai répété dans au moins quatre dépôts.
Je commence toujours avec un fournisseur géré, et toujours pour la même raison
Le jour un d'un projet, je ne veux pas écrire le hachage des mots de passe, la vérification d'email, ni le flux OAuth avec Google. J'installe un SDK, je pose deux variables d'environnement, et en une après-midi il y a un login. C'est la bonne décision ce jour-là : le projet ne sait pas encore s'il existera dans un mois.
Le problème, c'est que ce SDK ne reste pas sagement au bord de l'application. Il s'installe dans le layout, dans le contexte utilisateur, dans chaque server action qui a besoin de savoir qui appelle, et dans le schéma de base de données parce que le user_id de sa table est celui qui finit en clé étrangère dans toutes les tiennes.
Les quatre migrations et ce qui a déclenché chacune
- Fournisseur un vers fournisseur deux, en deux jours. Le premier marchait, mais le modèle tarifaire et le couplage avec son propre tableau de bord ne collaient pas. Aucune ligne d'auth n'a survécu au changement.
- Fournisseur deux, six mois de cohabitation. Là, il y a eu de tout : des notes dans le README sur son comportement bizarre avec Suspense, des refactors du middleware pour lire les domaines protégés depuis un fichier de config, et un passage de
useProfileàuseUserpour cesser de maintenir ma propre couche par-dessus la sienne. - Fournisseur deux vers fournisseur trois. Une PR entière appelée "migrate from" suivi du nom du précédent. Trente fichiers touchés. On supprime
stack.tset la route catch-allapp/handler/[...stack]/page.tsx, et apparaissentauth.tscôté client etapp/api/auth/[...path]/route.tscôté serveur. - Fournisseur trois vers middleware maison. Le commit dit littéralement "replace neonAuth with custom auth middleware and update session handling". Du SDK géré je garde le client ; le reste, c'est moi qui le contrôle.
Ce qui casse toujours : les URLs
Dans la deuxième migration, il y a deux commits le même jour, l'un après l'autre. Le premier laisse le fichier de configuration comme ça :
export const stackServerApp = new StackServerApp({
tokenStore: "nextjs-cookie",
urls: {
signIn: "/ass",
signUp: "/asdds",
},
});
Voilà ce qui sort quand tu testes à la main où chaque chose redirige et que tu laisses ta frappe de test en place. Le second commit remet /auth dans les deux. C'est ridicule et c'est exactement la faute que tu vas te manger dans toute migration d'auth : le fournisseur ne sait pas où vit ton écran de login, il y a donc une URL déclarée dans sa config qui doit coïncider avec une route réelle du router, et aucun type ne le vérifie. Ça redirige vers un 404 et le symptôme n'est pas « URL fausse » : c'est « le login ne marche pas ».
L'autre moitié de ce qui casse, c'est la forme de la session. Au passage au middleware maison, getSession est passé de renvoyer l'objet du SDK à renvoyer quelque chose d'explicite :
type SessionData = NonNullable<Awaited<ReturnType<typeof auth.getSession>>["data"]>;
export type Session = SessionData | { session: null; user: null };
Ce | { session: null; user: null } est la concession à faire. Le SDK précédent renvoyait un nullable tout nu et la moitié du code faisait session?.user. En le normalisant, tous les endroits qui supposaient que la session pouvait être undefined ont cessé de compiler. C'est ça, la partie chère de la migration, pas le login.
Ce que je gagne et ce que je perds en l'écrivant moi-même
Ce que je gagne est concret : le contrôle sur l'objet de session, sur l'endroit où le cookie est stocké et sur ce qui se passe exactement quand il n'y a pas de session. Et une chose que je n'avais pas su apprécier avant d'en avoir besoin : pouvoir mettre les règles d'autorisation au même endroit.
Dans un outil de BI pour une grande entreprise, l'autorisation a fini par être une liste d'adresses mail dans le middleware lui-même :
const authorizedEmails = [ /* ... */ ];
const hasFullAccess = userEmail && authorizedEmails.includes(userEmail);
Avec deux redirections différentes : le non authentifié va au login, l'authentifié sans droit va à l'accueil. Avec un fournisseur géré, ça se fait en bagarrant avec ses claims ou son panneau. Là, je l'ai écrit en dix lignes. J'ai aussi laissé traîner un console.log("User email:", userEmail) qu'il a fallu retirer dans un commit ultérieur, et cette app a vu son auth réécrite trois fois en un mois.
Ce que je perds, c'est tout ce que le fournisseur faisait sans que tu le voies : rotation des tokens, invalidation des sessions, le mail de récupération, le rate limit du login. Rien de tout ça n'est impossible, mais ça devient mon code, sans tests que je n'aurais pas écrits moi-même.
Mon critère aujourd'hui
Ce n'est pas « écris ta propre auth ». C'est plus ennuyeux : pars du principe dès le jour un que tu changeras de fournisseur. Ce que je fais aujourd'hui avant d'installer quoi que ce soit :
- Une seule fonction
getSession()dans mon code. Aucun composant n'importe le SDK. Le type de retour, c'est moi qui le définis, je ne l'infère pas du fournisseur. C'est la différence entre migrer trente fichiers et en migrer un. - Le
user_idde ma base de données est le mien. Celui du fournisseur va dans une colonne à part. Sinon, la migration cesse d'être un changement de bibliothèque et devient une migration de données. - Les URLs de login dans une constante, pas dans la config du fournisseur. Que le
/assn'ait nulle part où se cacher. - Jamais d'autorisation à l'intérieur du fournisseur. Qui peut entrer, c'est l'affaire du fournisseur ; ce qu'il peut faire, c'est l'affaire de mon code.
Récemment, j'ai démarré un nouveau projet et j'ai recommencé avec un fournisseur géré. Le premier jour, impossible d'entrer : l'URL de base de l'auth pointait sur une valeur de remplissage du boilerplate. Le cycle recommence, et cette fois je sais exactement où ça va faire mal.
