# 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.

> Pagina: https://ui.eneawork.it/blog/nextjs-auth-session-cookie
> Markdown: https://ui.eneawork.it/blog/nextjs-auth-session-cookie.md

*2026-11-01*

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.

## 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.

Cookie httpOnly per il token, requireUser() sul server per l’autorizzazione, middleware solo per i redirect. Nessuna eccezione senza un test che la copra.

## FAQ

### 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 NEXT_PUBLIC_*. 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 jar.delete("sessione") e, se usi sessioni in tabella, elimina la riga. Il solo redirect non cancella nulla.

### Il cookie funziona in sviluppo senza HTTPS?

Con secure: true il browser lo scarta su http://localhost in alcuni contesti. Imposta secure: process.env.NODE_ENV === "production" e lascia la produzione rigida.
