L'autenticazione è uno degli aspetti più importanti nella costruzione di applicazioni web sicure. Permette di verificare l'identità degli utenti che accedono al nostro sistema, proteggendo le informazioni sensibili e le funzionalità riservate.
In Node.js possiamo implementare l'autenticazione in diversi modi, principalmente attraverso sessioni con cookie o token JWT. In questa lezione vedremo entrambi gli approcci, quando usarli e come implementarli in modo sicuro.
Prima di iniziare, è importante capire la differenza tra due concetti che spesso vengono confusi:
Ad esempio, quando accedi al tuo account email (autenticazione), il sistema verifica se puoi leggere le email di altri utenti (autorizzazione).
Esistono due approcci principali per gestire l'autenticazione in Node.js.
Sessioni con cookie: il server memorizza i dati della sessione e invia un cookie al browser dell'utente. A ogni richiesta successiva, il browser invia automaticamente il cookie, permettendo al server di riconoscere l'utente.
Token JWT: il server genera un token firmato digitalmente che contiene le informazioni dell'utente. Il client conserva questo token e lo invia con ogni richiesta. Il server verifica la firma per confermare l'autenticità del token.
Le sessioni sono un meccanismo tradizionale per mantenere lo stato di autenticazione. Quando un utente effettua il login, il server crea una sessione e invia un cookie al browser che la identifica.
I cookie sono piccoli file di testo che il server invia al browser dell'utente. Il browser li conserva e li reinvia automaticamente al server con ogni richiesta successiva allo stesso dominio.
Quando utilizziamo le sessioni:
Per gestire le sessioni in Express, utilizziamo il modulo express-session. Prima di tutto, installiamolo:
npm install express-sessionOra creiamo un sistema di login base con sessioni:
const express = require("express");
const session = require("express-session");
const app = express();
// Middleware per leggere i dati JSON
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Configurazione della sessione
app.use(session({
secret: "chiave-segreta-molto-lunga-e-casuale",
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 60 * 24, // 24 ore
httpOnly: true
}
}));
// Dati utenti (in produzione useresti un database)
const utenti = [
{ id: 1, username: "mario", password: "password123" },
{ id: 2, username: "luigi", password: "pass456" }
];
// Route di login
app.post("/login", (req, res) => {
const { username, password } = req.body;
// Cerca l'utente nel database
const utente = utenti.find(u =>
u.username === username && u.password === password
);
if (utente) {
// Salva l'ID utente nella sessione
req.session.userId = utente.id;
req.session.username = utente.username;
res.json({ message: "Login effettuato con successo" });
} else {
res.status(401).json({ error: "Credenziali non valide" });
}
});
// Route protetta
app.get("/profilo", (req, res) => {
if (req.session.userId) {
res.json({
message: `Benvenuto ${req.session.username}`,
userId: req.session.userId
});
} else {
res.status(401).json({ error: "Devi effettuare il login" });
}
});
app.listen(3000);Nell'esempio:
express-session con un secret (una stringa casuale per "firmare" i cookie), resave: false (non salva la sessione a ogni richiesta, ma solo è stata modificata) e saveUninitialized: false (non crea sessioni vuote).maxAge per far scadere la sessione dopo 24 ore e httpOnly per impedire l'accesso al cookie tramite JavaScript (così si ottiene una protezione da attacchi XSS).req.session.userId.req.session.userId per sapere se l'utente è autenticato.La proprietà req.session è un oggetto dove possiamo salvare qualsiasi dato vogliamo conservare tra le richieste. È importante salvare solo l'ID dell'utente e non tutti i suoi dati, per questioni di sicurezza e performance.
Ecco un esempio più completo che recupera i dati dell'utente dal database:
app.get("/dashboard", (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: "Non autenticato" });
}
// Recupera i dati completi dell'utente dal database
const utente = utenti.find(u => u.id === req.session.userId);
if (utente) {
res.json({
username: utente.username,
// Altri dati dell'utente (senza la password!)
});
} else {
res.status(404).json({ error: "Utente non trovato" });
}
});Per effettuare il logout, dobbiamo distruggere la sessione usando req.session.destroy():
app.post("/logout", (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: "Errore durante il logout" });
}
res.json({ message: "Logout effettuato con successo" });
});
});Le sessioni possono anche scadere automaticamente dopo un periodo di inattività. Possiamo configurarlo modificando maxAge nel cookie:
app.use(session({
secret: "chiave-segreta",
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 1000 * 60 * 15, // 15 minuti
httpOnly: true
}
}));I JSON Web Token (JWT) sono un'alternativa alle sessioni, particolarmente utile per le API e le applicazioni.
Un JWT è una stringa codificata composta da tre parti separate da punti:
header.payload.signature
In dettaglio:
A differenza delle sessioni, con i JWT non è necessario memorizzare nulla sul server. Il token stesso contiene tutte le informazioni necessarie e può essere verificato tramite la firma digitale.
Il flusso di autenticazione con JWT funziona così:
localStorage o in un cookie).Authorization.Per usare i JWT in Node.js, installiamo il modulo jsonwebtoken:
npm install jsonwebtokenEcco come creare un token dopo il login:
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();
app.use(express.json());
// Chiave segreta per firmare i token (conservala in un file .env!)
const SECRET_KEY = "chiave-super-segreta-da-non-condividere";
const utenti = [
{ id: 1, username: "mario", password: "password123" },
{ id: 2, username: "luigi", password: "pass456" }
];
app.post("/login", (req, res) => {
const { username, password } = req.body;
const utente = utenti.find(u =>
u.username === username && u.password === password
);
if (utente) {
// Genera il token con i dati dell'utente
const token = jwt.sign(
{ userId: utente.id, username: utente.username },
SECRET_KEY,
{ expiresIn: "24h" }
);
res.json({ token });
} else {
res.status(401).json({ error: "Credenziali non valide" });
}
});
app.listen(3000);Nel codice:
jwt.sign() per creare il token. Il primo parametro è un oggetto con i dati che vogliamo includere nel token (payload).expiresIn che imposta la scadenza del token.Una volta che il client ha ricevuto il token, lo invierà con ogni richiesta successiva. Il server deve verificare che il token sia valido e non sia stato modificato:
app.get("/profilo", (req, res) => {
// Legge il token dall'header Authorization
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: "Token mancante" });
}
// Il formato dell'header è: "Bearer TOKEN"
const token = authHeader.split(" ")[1];
try {
// Verifica il token
const decoded = jwt.verify(token, SECRET_KEY);
// Il token è valido, possiamo usare i dati dell'utente
res.json({
message: `Benvenuto ${decoded.username}`,
userId: decoded.userId
});
} catch (error) {
res.status(401).json({ error: "Token non valido" });
}
});Il metodo jwt.verify() controlla la firma del token e restituisce il payload se è valido. Se il token è scaduto o è stato modificato, viene lanciato un errore.
Il client (browser) deve conservare il token e inviarlo con ogni richiesta. Ci sono due approcci principali.
Il primo è LocalStorage, che è più comune per applicazioni più piccole:
localStorage.setItem("token", tokenRicevutoDalServer);
// Invia il token con ogni richiesta
fetch("/profilo", {
headers: {
"Authorization": `Bearer ${localStorage.getItem("token")}`
}
});Il secondo è tramite cookie, che è generalmente più sicuro se configurato correttamente:
app.post("/login", (req, res) => {
// ... verifica credenziali ...
const token = jwt.sign({ userId: utente.id }, SECRET_KEY, { expiresIn: "24h" });
res.cookie("token", token, {
httpOnly: true, // Non accessibile da JavaScript
secure: true, // Solo HTTPS
maxAge: 1000 * 60 * 60 * 24
});
res.json({ message: "Login effettuato" });
});L’autenticazione deve essere implementata con attenzione per evitare vulnerabilità.
Non salvare mai le password in chiaro. È essenziale utilizzare un algoritmo di hashing sicuro, come bcrypt, in modo che, anche in caso di violazione del database, le password rimangano protette.
Per installare bcrypt:
npm install bcryptVediamo un esempio di hashing delle password tramite bcrypt:
const bcrypt = require("bcrypt");
// Durante la registrazione
app.post("/registrazione", async (req, res) => {
const { username, password } = req.body;
// Hash della password con 10 "salt rounds"
const passwordHash = await bcrypt.hash(password, 10);
// Salva l'utente nel database con la password hashata
const nuovoUtente = {
id: utenti.length + 1,
username,
password: passwordHash
};
utenti.push(nuovoUtente);
res.json({ message: "Registrazione completata" });
});
// Durante il login
app.post("/login", async (req, res) => {
const { username, password } = req.body;
const utente = utenti.find(u => u.username === username);
if (!utente) {
return res.status(401).json({ error: "Credenziali non valide" });
}
// Confronta la password inserita con quella hashata
const passwordValida = await bcrypt.compare(password, utente.password);
if (passwordValida) {
const token = jwt.sign({ userId: utente.id }, SECRET_KEY, { expiresIn: "24h" });
res.json({ token });
} else {
res.status(401).json({ error: "Credenziali non valide" });
}
});Il metodo bcrypt.hash() genera un hash della password che non può essere decodificato. Durante il login, bcrypt.compare() verifica se la password inserita corrisponde all'hash salvato.
I token JWT devono sempre avere una scadenza. Token che non scadono mai rappresentano un rischio di sicurezza:
// Token con scadenza breve
const accessToken = jwt.sign(
{ userId: utente.id },
SECRET_KEY,
{ expiresIn: "15m" } // 15 minuti
);
// Token di refresh con scadenza lunga
const refreshToken = jwt.sign(
{ userId: utente.id },
REFRESH_SECRET_KEY,
{ expiresIn: "7d" } // 7 giorni
);
res.json({ accessToken, refreshToken });Quando accessToken scade, il client può usare refreshToken per ottenerne uno nuovo senza richiedere nuovamente le credenziali:
app.post("/refresh", (req, res) => {
const { refreshToken } = req.body;
try {
const decoded = jwt.verify(refreshToken, REFRESH_SECRET_KEY);
// Genera un nuovo access token
const nuovoAccessToken = jwt.sign(
{ userId: decoded.userId },
SECRET_KEY,
{ expiresIn: "15m" }
);
res.json({ accessToken: nuovoAccessToken });
} catch (error) {
res.status(401).json({ error: "Refresh token non valido" });
}
});In una vera applicazione web, è essenziale usare HTTPS per proteggere le credenziali e i token durante la trasmissione.
Per i cookie, impostiamo sempre questi flag di sicurezza:
app.use(session({
secret: "chiave-segreta",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Protegge da XSS
secure: true, // Solo HTTPS (in produzione)
sameSite: "strict" // Protegge da CSRF
}
}));Nell'esempio:
httpOnly: true impedisce l'accesso al cookie tramite JavaScript, proteggendo da attacchi XSS.secure: true garantisce che il cookie venga inviato solo su connessioni HTTPS.sameSite: impedisce l'invio del cookie in richieste ad altri siti web, proteggendo da attacchi CSRF.