React
Sessione con cookie nell’App Router: il minimo sicuro
Cookie httpOnly con token firmato o ID opaco, controllo sul server a ogni richiesta: il minimo per una sessione sicura nell’App Router.
In sintesi
- Il cookie porta un token firmato o un ID opaco, sempre `httpOnly`, `secure` e `sameSite=lax`.
- L’autorizzazione sta nel server component o nell’action, non nel middleware.
- Un JWT non si revoca: se ti serve il logout immediato, usa una tabella di sessioni.
Una sessione sicura nell’App Router è un cookie httpOnly, secure, sameSite=lax con un token firmato o un ID opaco, più un controllo sul server a ogni richiesta. Il middleware non è il posto dove autorizzi.
Cosa mette il cookie
Due scelte reali: token firmato autocontenuto, oppure ID opaco legato a una tabella di sessioni. Il primo non richiede database ma non si revoca; il secondo costa una query per richiesta e si revoca subito.
| Approccio | Revoca | Costo per richiesta | Quando serve |
|---|---|---|---|
| JWT firmato | Solo cambiando la chiave | Verifica della firma, nessuna query | Sessione breve, pochi utenti |
| ID opaco più tabella | Immediata, una riga | Una query, eventualmente in cache | Logout, ban, dispositivi |
| Cookie cifrato | Con una versione del token | Decifratura, nessuna query | Dati piccoli nel cookie |
Il codice minimo
// app/actions/login.ts
"use server";
import { cookies } from "next/headers";
import { SignJWT } from "jose";
const chiave = new TextEncoder().encode(process.env.SESSION_SECRET);
export async function login(userId: string) {
const token = await new SignJWT({ sub: userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("7d")
.sign(chiave);
const jar = await cookies();
jar.set("sessione", token, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 7,
});
}
La chiave si genera una volta e resta nell’ambiente: openssl rand -base64 32. Se la chiave finisce nel repository, ogni sessione è compromessa e la rotazione è l’unica risposta sensata.
In Next 15 cookies() è asincrono: const jar = await cookies(). Dimenticare l’await produce un errore di tipo, non un bug silenzioso, ed è il motivo per cui il problema si vede subito.
Il middleware serve, ma non autorizza
Il middleware legge il cookie e può fare un redirect rapido verso /login. Non può validare il token contro il database e non deve diventare l’unico controllo: chi copia il pattern senza questa distinzione lascia passare richieste che sembrano autenticate. Il pattern completo è in middleware per l’autenticazione.
Il catalogo EneUI indicizza 111.985 componenti, 1.556 blocchi e 12.808 icone da 1.472 librerie; il dato è misurato il 1 novembre 2026 su ui.eneawork.it. Con quel numero di pagine, una sola funzione requireUser() letta dal server vale più di venti controlli sparsi nei componenti.
Componenti per il login
Tre record utili: i blocchi di autenticazione con form completi da adattare, il blocco pronto login-01, e i componenti di validazione form per gli errori lato client.
Per capire dove mettere il controllo, action o route, vale Route Handler o Server Action. La validazione dei payload è trattata in validazione delle Server Action, e il confine tra i due mondi in server-only e client-only.
Il limite da mettere in conto
Un JWT non si revoca. Se un utente perde il dispositivo, l’unica leva è accorciare la scadenza o passare a sessioni in tabella. Decidi prima di distribuire il login, non quando succede.
Domande frequenti
Meglio JWT o sessione in database?
JWT se la sessione è breve e non ti serve revocarla. Sessione in tabella se ti servono logout immediato, ban o elenco dispositivi. Il costo è una query per richiesta, spesso cacheabile.
Dove metto il segreto di firma?
In una variabile d’ambiente del server, mai in <code>NEXT_PUBLIC_*</code>. Una variabile pubblica finisce nel bundle del client e chiunque può firmare token validi.
Il middleware può proteggere una pagina?
Può fare un redirect, non può autorizzare. La verifica vera sta nel server component o nell’action, perché il middleware non ha accesso al database e può essere aggirato da un errore di matcher.
Come faccio il logout?
Con una Server Action che cancella il cookie con <code>jar.delete("sessione")</code> e, se usi sessioni in tabella, elimina la riga. Il solo redirect non cancella nulla.
Il cookie funziona in sviluppo senza HTTPS?
Con <code>secure: true</code> il browser lo scarta su <code>http://localhost</code> in alcuni contesti. Imposta <code>secure: process.env.NODE_ENV === "production"</code> e lascia la produzione rigida.