logo logo Dove iniziare Linguaggi Aiuto Blog
Home Dove iniziare Linguaggi Aiuto e Supporto Biadets Blog

Autenticazione e sessioni in Node.js

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.

Autenticazione e autorizzazione

Prima di iniziare, è importante capire la differenza tra due concetti che spesso vengono confusi:

  • Autenticazione: verifica chi sei. È il processo che conferma l'identità di un utente, tipicamente attraverso username e password.
  • Autorizzazione: verifica cosa puoi fare. Dopo che l'utente è stato autenticato, l'autorizzazione determina a quali risorse può accedere.

Ad esempio, quando accedi al tuo account email (autenticazione), il sistema verifica se puoi leggere le email di altri utenti (autorizzazione).

Strategie di autenticazione

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.

Autenticazione con sessioni e cookie

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.

Come funzionano i cookie

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:

  1. L'utente invia username e password al server.
  2. Il server verifica le credenziali e crea una sessione.
  3. Il server invia un cookie al browser contenente l'ID della sessione.
  4. Il browser conserva il cookie e lo invia automaticamente con ogni richiesta.
  5. Il server legge il cookie e recupera i dati della sessione.

Creare una sessione con express-session

Per gestire le sessioni in Express, utilizziamo il modulo express-session. Prima di tutto, installiamolo:

npm install express-session

Ora 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:

  • Configuriamo 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).
  • Impostiamo 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).
  • Nella pagina /login, verifichiamo le credenziali e salviamo l'ID dell'utente in req.session.userId.
  • Nella pagina /profilo, controlliamo se esiste req.session.userId per sapere se l'utente è autenticato.

Salvare l'ID utente nella sessione

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" });
  }
});

Logout e scadenza delle sessioni

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
  }
}));

Autenticazione con JWT

I JSON Web Token (JWT) sono un'alternativa alle sessioni, particolarmente utile per le API e le applicazioni.

Cos'è un JWT e come funziona

Un JWT è una stringa codificata composta da tre parti separate da punti:

header.payload.signature

In dettaglio:

  • Header: contiene informazioni sul tipo di token e sull’algoritmo di firma.
  • Payload: contiene i dati dell’utente, detti “claims”.
  • Signature: garantisce che il token non sia stato modificato.

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ì:

  1. L’utente invia username e password.
  2. Il server verifica le credenziali e genera un JWT.
  3. Il server invia il JWT al client.
  4. Il client conserva il JWT (in localStorage o in un cookie).
  5. Ad ogni richiesta, il client invia il JWT nell’header Authorization.
  6. Il server verifica la firma del JWT e autorizza la richiesta.

Generare token con jsonwebtoken

Per usare i JWT in Node.js, installiamo il modulo jsonwebtoken:

npm install jsonwebtoken

Ecco 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:

  • Usiamo jwt.sign() per creare il token. Il primo parametro è un oggetto con i dati che vogliamo includere nel token (payload).
  • Il secondo parametro è la chiave segreta usata per firmare il token.
  • Il terzo parametro è un oggetto con opzioni, come expiresIn che imposta la scadenza del token.

Verificare i token lato server

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.

Conservare e inviare il token dal client

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" });
});

Sicurezza e buone pratiche

L’autenticazione deve essere implementata con attenzione per evitare vulnerabilità.

Salvare password con bcrypt

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 bcrypt

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

Impostare la scadenza dei token

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" });
  }
});

HTTPS e cookie sicuri

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.

Prova!Completa gli spazi vuoti con il testo appropriato.
// Crea un token JWT con Node.js
const token = jwt.({ userId: 1 }, "chiave-segreta", { : "24h" });

Prova!Scegli l'opzione corretta tra quelle elencate.
// È necessario verificare il token JWT tramite jsonwebtoken
const decoded = jwt.____(token, SECRET_KEY);