Negli ultimi anni, l’aggiunta dell’autenticazione a un’applicazione è passata da qualcosa di oscuro e complicato a qualcosa per cui basta letteralmente usare un’API.

Non mancano repository di esempio e tutorial su come implementare specifici schemi di autenticazione in Next.js, ma ce ne sono meno sui perché di quali schemi, strumenti e compromessi scegliere.

Questo post illustrerà cosa considerare nell’approccio all’autenticazione in Next.js, dalla scelta di un provider alla creazione di route per l’accesso e alla scelta tra lato server e lato client.

Scegliere un metodo/fornitore di autenticazione

Esistono praticamente 1.000 modi per integrare l’autenticazione in un’applicazione. Piuttosto che concentrarci su particolari provider (argomento per un altro post del blog), esaminiamo i tipi di soluzioni di autenticazione e alcuni esempi di ciascuna. In termini di implementazione, next-auth sta rapidamente diventando un’opzione popolare per integrare l’applicazione Next.js con più provider, aggiungere SSO, ecc.

Database tradizionale

Questa soluzione è semplicissima: si memorizzano nomi utente e password in un database relazionale. Quando un utente si iscrive per la prima volta, si inserisce una nuova riga nella tabella `users` con le informazioni fornite. Quando l’utente accede, si verifica che le sue credenziali corrispondano a quelle memorizzate nella tabella. Quando un utente vuole cambiare la propria password, si aggiorna il valore nella tabella.

L’autenticazione tradizionale su database è sicuramente lo schema di autenticazione più diffuso, se si considera la totalità delle applicazioni esistenti, ed esiste praticamente da sempre. È molto flessibile, economico e non vincola a nessun fornitore in particolare. Ma è necessario costruirlo da soli e, in particolare, preoccuparsi della crittografia e assicurarsi che quelle preziosissime password non finiscano nelle mani sbagliate.

Soluzioni di autenticazione dal fornitore di database

Negli ultimi anni (e, per quanto riguarda Firebase, già da qualche anno), i fornitori di database gestiti sono diventati relativamente standard nell’offrire una sorta di soluzione di autenticazione gestita. Firebase, Supabase e AWS offrono sia un database gestito che un’autenticazione gestita come servizio attraverso una suite di API che astrae facilmente dalla creazione di utenti e dalla gestione delle sessioni (per saperne di più, proseguite nella lettura).

L’accesso di un utente con l’autenticazione Supabase è semplice:

async function signInWithEmail() {
  const { data, error } = await supabase.auth.signInWithPassword({
    email: '[email protected]',
    password: 'example-password',
  })
}

Soluzioni di autenticazione che non provengono dal proprio fornitore di database

Forse ancora più comune dell’autenticazione come servizio del DBaaS è l’autenticazione come servizio di un’intera azienda o prodotto. Auth0 esiste dal 2013 (ora è di proprietà di Okta) e le recenti aggiunte come Stytch hanno dato priorità all’esperienza degli sviluppatori e hanno guadagnato un po’ di spazio.

L'interfaccia utente che si ottiene utilizzando Auth0 per l'autenticazione
Auth0 per l’autenticazione

Single Sign On

L’SSO permette di “esternalizzare” l’identità a un fornitore esterno, che può spaziare da un fornitore aziendale focalizzato sulla sicurezza come Okta a qualcosa di più diffuso come Google o GitHub. Google SSO è onnipresente nel mondo SaaS, mentre alcuni strumenti dedicati agli sviluppatori effettuano l’autenticazione solo tramite GitHub.

Qualunque fornitore si scelga, SSO è generalmente un’aggiunta agli altri tipi di autenticazione di cui sopra e comporta le sue peculiarità per quanto riguarda l’integrazione con piattaforme esterne (attenzione: SAML utilizza XML).

Ok, allora quale scegliere?

Non esiste una scelta “corretta”: ciò che è giusto per il proprio progetto dipende dalle proprie priorità. Se volete fare le cose in fretta senza un sacco di configurazioni iniziali, l’outsourcing dell’autenticazione ha senso (anche esternalizzarla completamente, UI inclusa, a qualcuno come Auth0). Se invece prevedete una configurazione più complessa, è opportuno costruire il proprio backend di autenticazione. E se pensate di supportare clienti più grandi, prima o poi dovrete aggiungere il SSO.

Next.js è così popolare che la maggior parte di questi fornitori di autenticazione hanno documenti e guide all’integrazione specifiche per Next.js.

Creazione di route per l’iscrizione e l’accesso e suggerimenti per raggiungere un livello di autenticazione extra

Alcuni fornitori di autenticazione, come Auth0, forniscono intere pagine web ospitate per la registrazione e l’accesso. Ma se state costruendo queste pagine da zero, trovo che sia utile crearle all’inizio del processo, perché vi serviranno come reindirizzamento quando implementerete l’autenticazione.

Quindi ha senso creare la struttura di queste pagine e poi aggiungere le richieste al backend in un secondo momento. Il modo più semplice per implementare l’autenticazione è avere due di queste route:

  • Una per l’iscrizione
  • Un’altra per l’accesso quando l’utente ha già un account

Oltre alle basi, dovrete occuparvi di casi limite, come quando un utente dimentica la password. Alcuni team preferiscono inserire il processo di reimpostazione della password in un percorso separato, mentre altri aggiungono elementi dinamici dell’interfaccia utente alla normale pagina di accesso.

Una bella pagina di iscrizione potrebbe non fare la differenza tra successo e fallimento, ma piccoli accorgimenti possono lasciare una buona impressione e fornire una migliore UX. Eccone alcuni raccolti da siti di tutto il web che hanno messo un po’ di cura in più nei loro processi di autenticazione.

1. Barra di navigazione aggiornata quando c’è una sessione attiva

La call to action nella navbar di Stripe cambia in base alla presenza o meno di una sessione autenticata. Ecco come appare il sito di marketing se non si è autenticati. Notate la call to action per accedere:

Homepage di Stripe
La homepage di Stripe cambia la CTA se l’autenticazione è avvenuta o meno

Ecco come appare se siete autenticati. Notate che la call to action cambia per portare l’utente alla sua dashboard invece che all’accesso:

Differenze nella homepage di Stripe
Differenze nella homepage di Stripe

Non cambia poi molto la mia esperienza con Stripe, ma è utile.

Un interessante inciso tecnico: c’è una buona ragione per cui la maggior parte delle aziende non fa “dipendere” la navbar del proprio sito di marketing dall’autenticazione: significherebbe una richiesta API aggiuntiva per verificare lo stato di autenticazione su ogni singolo caricamento di pagina, la maggior parte dei quali riguarda visitatori che probabilmente non sono autenticati.

2. Contenuti utili aggiunti al modulo di iscrizione

Negli ultimi anni, soprattutto nel settore SaaS, le aziende hanno iniziato ad aggiungere contenuti alla pagina di iscrizione per “incoraggiare” l’utente a completare l’iscrizione. Questo può aiutare a migliorare la conversione della pagina, almeno in modo incrementale.

Ecco una pagina di iscrizione di Retool, con un’animazione e alcuni loghi laterali:

Pagina di iscrizione di Retool
Se avete intenzione di farlo, cercate di fare in modo che i font di ogni lato coincidano.

Anche noi di Kinsta facciamo così per la nostra pagina di iscrizione:

Pagina di iscrizione di MyKinsta
Pagina di iscrizione di Kinsta

Un piccolo contenuto extra può aiutare a ricordare all’utente per cosa si sta iscrivendo e perché ne ha bisogno.

3. Se si usa una password: suggerirne o imporne una forte

Mi sento di affermare che gli sviluppatori sanno bene che le password sono intrinsecamente insicure, ma non è così per tutte le persone che si iscriveranno al prodotto. Incoraggiare gli utenti a creare password sicure è un bene per voi e per loro.

Coinbase è molto severo per quanto riguarda l’iscrizione e richiede l’utilizzo di una password sicura e più complicata del semplice nome di battesimo:

Creare un account con Coinbase
Password debole su Coinbase

Dopo averne generata una dal mio gestore di password, ero pronto a partire:

Password forte su Coinbase
Password forte su Coinbase

L’interfaccia utente, però, non mi diceva perché la password non era abbastanza sicura, né i requisiti oltre alla presenza di un numero. Includere questi requisiti nel copy del prodotto renderà le cose più semplici per l’utente e aiuterà a evitare la frustrazione da ripetizione della password.

4. Etichettare gli input in modo da renderli compatibili con un gestore di password

Un americano su tre usa un gestore di password come 1Password, eppure molti moduli sul web continuano a ignorare il `type=` negli input HTML. Fate in modo che i moduli giochino con i gestori di password:

  • Racchiudete gli elementi di input in un elemento del modulo
  • Assegnate agli input un tipo e un’etichetta
  • Aggiungete funzionalità di completamento automatico agli input
  • Non aggiungete campi dinamicamente (parlo proprio di te, Delta)

Può fare la differenza tra un’iscrizione di 10 secondi, incredibilmente fluida, e una fastidiosa iscrizione manuale, soprattutto su mobile.

Scegliere tra sessioni e JWT

Una volta avvenuta l’autenticazione dell’utente, è il momento di scegliere una strategia per mantenere lo stato durante le richieste successive. L’HTTP è stateless e non vogliamo certo chiedere all’utente la sua password a ogni singola richiesta. Esistono due metodi popolari per gestire questo aspetto: le sessioni (o cookie) e i JWT (JSON web token), che si differenziano per il fatto che il lavoro venga svolto dal server o dal client.

Sessioni, ovvero cookie

Nell’autenticazione basata sulle sessioni, la logica e il lavoro per mantenere l’autenticazione sono gestiti dal server. Ecco il flusso di base:

  1. L’utente si autentica tramite la pagina di accesso.
  2. Il server crea un record che rappresenta questa particolare “sessione” di navigazione. Questo viene solitamente inserito in un database con un identificatore casuale e dettagli sulla sessione, come la data di inizio e la data di scadenza.
  3. Questo identificatore casuale – qualcosa come `6982e583b1874abf9078e1d1dd5442f1` – viene inviato al browser e memorizzato come cookie.
  4. Nelle richieste successive del client, l’identificatore viene incluso e controllato nella tabella delle sessioni del database.

Per quanto riguarda la durata delle sessioni, il momento in cui devono essere revocate, ecc., è tutto piuttosto semplice e modificabile. L’aspetto negativo è la latenza su scala significativa, viste tutte le scritture e le letture del database, ma questo potrebbe non essere un aspetto importante per la maggior parte dei lettori.

Gettoni web JSON (JWT)

Invece di gestire l’autenticazione per le richieste successive sul server, i JWT permettono di gestirle (per lo più) sul lato client. Ecco come funziona:

  1. L’utente si autentica tramite la pagina di accesso.
  2. Il server genera un JWT che contiene l’identità dell’utente, i permessi che gli sono stati concessi e una data di scadenza (oltre a potenziali altre cose).
  3. Il server firma il token, ne cripta il contenuto e lo invia al cliente.
  4. Per ogni richiesta, il client può decifrare il token e verificare che l’utente abbia il permesso di effettuare la richiesta (senza dover comunicare con il server).

Con tutto il lavoro successivo all’autenticazione iniziale scaricato sul client, l’applicazione può essere caricata e funzionare molto più velocemente. Ma c’è un problema principale: non c’è modo di invalidare un JWT dal server. Se l’utente vuole disconnettersi da un dispositivo o se l’ambito della sua autorizzazione cambia, dovrete aspettare che il JWT scada.

Scegliere tra autorizzazione lato server e lato client

Parte di ciò che rende grande Next.js è il rendering statico integrato: se la pagina è statica, cioè non deve effettuare chiamate API esterne, Next.js la memorizza automaticamente nella cache e può servirla in modo estremamente veloce tramite un CDN. Le versioni precedenti a Next.js 13 sanno se una pagina è statica se non include alcun `getServerSideProps` o `getInitialProps` nel file, mentre le versioni successive a Next.js 13 utilizzano React Server Components per farlo.

Per quanto riguarda l’autenticazione, avete la possibilità di scegliere tra il rendering di una pagina statica di “caricamento” e l’esecuzione del fetching sul lato client o l’esecuzione di tutto sul lato server. Per le pagine che richiedono l’autenticazione[1], potete eseguire il rendering di uno “scheletro” statico e poi effettuare le richieste di autenticazione sul lato client. In teoria, questo significa che la pagina si carica più velocemente, anche se il contenuto iniziale non è completamente pronto.

Ecco un esempio semplificato tratto dai documenti che rende uno stato di caricamento finché l’oggetto utente non è pronto:

import useUser from '../lib/useUser'
 
const Profile = () => {
  // Fetch the user client-side
  const { user } = useUser({ redirectTo: '/login' })
 
  // Server-render loading state
  if (!user || user.isLoggedIn === false) {
    // Build some sort of loading page here
    return <div>Loading...</div>
  }
 
  // Once the user request finishes, show the user
  return (
    <div>
      <h1>Your Account</h1>
      <p>Username: {JSON.stringify(user.username,null)}</p>
      <p>Email: {JSON.stringify(user.email,null)}</p>
      <p>Address: {JSON.stringify(user.address,null)}</p>
    </div>
  )
}
 
export default Profile

Si noti che è necessario creare una sorta di UI di caricamento per occupare lo spazio mentre il client effettua le richieste dopo il caricamento.

Se volete semplificare le cose ed eseguire l’autenticazione lato server, potete aggiungere la richiesta di autenticazione alla funzione `getServerSideProps` e Next aspetterà il rendering della pagina fino al completamento della richiesta. Al posto della logica condizionale dello snippet qui sopra, potreste eseguire qualcosa di più semplice come questa versione semplificata dei documenti di Next:

import withSession from '../lib/session'
 
export const getServerSideProps = withSession(async function ({ req, res }) {
  const { user } = req.session
 
  if (!user) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    }
  }
 
  return {
    props: { user },
  }
})
 
const Profile = ({ user }) => {
  // Show the user. No loading state is required
  return (
    <div>
      <h1>Your Account</h1>
      <p>Username: {JSON.stringify(user.username,null)}</p>
      <p>Email: {JSON.stringify(user.email,null)}</p>
      <p>Address: {JSON.stringify(user.address,null)}</p>
    </div>
  )
}
 
export default Profile

Qui c’è ancora la logica per gestire il fallimento dell’autenticazione, ma si reindirizza al login invece di eseguire il rendering dello stato di caricamento.

Riepilogo

Qual è la soluzione giusta per il vostro progetto? Iniziate valutando quanto siete sicuri della velocità dello schema di autenticazione. Se le richieste non richiedono tempo, potete eseguirle lato server ed evitare lo stato di caricamento. Se volete dare priorità al rendering immediato e poi attendere la richiesta, saltate `getServerSideProps` ed eseguite l’autenticazione altrove.

[1] Quando si usa Next, questo è un buon motivo per non richiedere l’autenticazione a tappeto per ogni singola pagina. È più semplice farlo, ma significa perdere i vantaggi in termini di prestazioni del framework web.

Justin Gage

Justin is a technical writer and author of the popular Technically newsletter. He did his B.S. in Data Science before a stint in full-stack engineering and now focuses on making complex technical concepts accessible to everyone.