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.

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.

ApproccioRevocaCosto per richiestaQuando serve
JWT firmatoSolo cambiando la chiaveVerifica della firma, nessuna querySessione breve, pochi utenti
ID opaco più tabellaImmediata, una rigaUna query, eventualmente in cacheLogout, ban, dispositivi
Cookie cifratoCon una versione del tokenDecifratura, nessuna queryDati 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.