02Arquitectura5 min

He migrat de proveïdor d'auth quatre vegades

Quatre proveïdors gestionats en any i mig, i el mateix destí en tots: middleware propi. El que trenca cada transició i què miro ara abans d'escriure una línia d'auth.

Per Salva Castells

Imatge de portada: Shixart1985, CC BY 2.0.
En aquest post

Al boilerplate que faig servir per començar projectes, el commit que integra el primer proveïdor d'auth i el que el substitueix pel segon estan separats per dos dies. Dos dies. El primer ficava middleware per a rutes protegides i una pantalla de login; el segon l'arrencava sencer i posava un altre SDK amb les seves pròpies pàgines de sign-in i sign-up. Des de llavors aquella mateixa plantilla ha passat per dos proveïdors més i ha acabat, gairebé un any després, amb un middleware escrit a mà.

No és mala sort. És un patró, i l'he repetit en almenys quatre repos.

Sempre començo amb un proveïdor gestionat, i sempre pel mateix

El dia u d'un projecte no vull escriure hash de contrasenyes, verificació de correu, ni el flux d'OAuth amb Google. Instal·lo un SDK, poso dues variables d'entorn, i en una tarda hi ha login. És la decisió correcta aquell dia: el projecte encara no sap si existirà d'aquí a un mes.

El problema és que aquell SDK no es queda quiet a la vora de l'aplicació. Es fica al layout, al context d'usuari, a cada server action que necessita saber qui truca, i a l'esquema de base de dades perquè el user_id de la seva taula és el que acaba sent clau forana a totes les teves.

Les quatre migracions i què va disparar cadascuna

  • Proveïdor u a proveïdor dos, en dos dies. El primer funcionava, però el model de preus i l'acoblament amb el seu propi dashboard no encaixaven. Cap línia d'auth no va sobreviure al canvi.
  • Proveïdor dos, sis mesos de convivència. Aquí hi va haver de tot: notes al README sobre el seu comportament estrany amb Suspense, refactoritzacions del middleware per llegir els dominis protegits d'un fitxer de config, i un canvi de useProfile a useUser per deixar de mantenir la meva pròpia capa a sobre de la seva.
  • Proveïdor dos a proveïdor tres. Una PR sencera anomenada "migrate from" el nom de l'anterior. Trenta fitxers tocats. S'esborren stack.ts i la ruta catch-all app/handler/[...stack]/page.tsx, i apareixen auth.ts al client i app/api/auth/[...path]/route.ts al servidor.
  • Proveïdor tres a middleware propi. El commit diu literalment "replace neonAuth with custom auth middleware and update session handling". De l'SDK gestionat em quedo amb el client; la resta la controlo jo.

El que es trenca sempre: les URL

A la segona migració hi ha dos commits el mateix dia, l'un darrere l'altre. El primer deixa el fitxer de configuració així:

export const stackServerApp = new StackServerApp({
  tokenStore: "nextjs-cookie",
  urls: {
    signIn: "/ass",
    signUp: "/asdds",
  },
});

Això és el que surt quan estàs provant a mà a on redirigeix cada cosa i et deixes el tecleig de prova posat. El segon commit ho torna a /auth a totes dues. És ridícul i és exactament la fallada que et menjaràs en tota migració d'auth: el proveïdor no sap on viu la teva pantalla de login, així que hi ha una URL declarada a la seva config que ha de coincidir amb una ruta real del router, i no hi ha tipus que ho comprovin. Redirigeix a un 404 i el símptoma no és "URL malament": és "el login no funciona".

L'altra meitat del que trenca és la forma de la sessió. En el pas al middleware propi, getSession va passar de retornar l'objecte de l'SDK a retornar una cosa explícita:

type SessionData = NonNullable<Awaited<ReturnType<typeof auth.getSession>>["data"]>;
export type Session = SessionData | { session: null; user: null };

Aquell | { session: null; user: null } és la cessió que cal fer. L'SDK anterior retornava un nullable pelat i mig codi feia session?.user. En normalitzar-ho, tots els llocs que assumien que la sessió podia ser undefined van deixar de compilar. Aquesta és la part cara de la migració, no el login.

Què hi guanyo i què hi perdo escrivint-ho jo

El que hi guanyo és concret: control sobre l'objecte de sessió, sobre on es desa la galeta i sobre què passa exactament quan no hi ha sessió. I una cosa que no havia valorat fins que la vaig necessitar: poder ficar regles d'autorització al mateix lloc.

En una eina de BI per a una empresa gran, l'autorització va acabar sent una llista de correus al mateix middleware:

const authorizedEmails = [ /* ... */ ];
const hasFullAccess = userEmail && authorizedEmails.includes(userEmail);

Amb dues redireccions diferents: el no autenticat va al login, l'autenticat sense permís va a la portada. Això amb un proveïdor gestionat es fa barallant-te amb els seus claims o el seu panell. Allà ho vaig escriure en deu línies. També hi vaig deixar un console.log("User email:", userEmail) posat que es va haver de treure en un commit posterior, i aquella app es va reescriure l'auth tres vegades en un mes.

El que hi perdo és tot el que el proveïdor feia i no veies: rotació de tokens, invalidació de sessions, el correu de recuperació, el rate limit del login. Res d'això no és impossible, però passa a ser codi meu, sense tests que no hagi escrit jo.

El meu criteri ara

No és "escriu la teva pròpia auth". És més avorrit: assumeix des del dia u que canviaràs de proveïdor. El que faig avui abans d'instal·lar res:

  • Una sola funció getSession() al meu codi. Cap component no importa l'SDK. El tipus de retorn el defineixo jo, no l'infereixo del proveïdor. És la diferència entre migrar trenta fitxers i migrar-ne un.
  • El user_id de la meva base de dades és meu. El del proveïdor va en una columna a part. Si no, la migració deixa de ser un canvi de llibreria i passa a ser una migració de dades.
  • Les URL de login en una constant, no a la config del proveïdor. Que el /ass no tingui on amagar-se.
  • Autorització mai dins del proveïdor. Qui pot entrar és del proveïdor; què pot fer és del meu codi.

Fa poc vaig arrencar un projecte nou i vaig tornar a començar amb un proveïdor gestionat. El primer dia no es va poder entrar: la URL base d'auth apuntava a un valor de farciment del boilerplate. El cicle comença un altre cop, i aquesta vegada sé exactament on farà mal.