Introduzione a JSON Web Token (JWT)
Nel moderno sviluppo di applicazioni web, specialmente con l'avvento delle Single Page Application (SPA) e delle architetture a microservizi, la gestione della sessione utente è cambiata drasticamente. Il metodo tradizionale basato sui cookie e sulle sessioni lato server (stateful) presenta limiti di scalabilità: ogni richiesta richiede che il server interroghi un database o una cache (come Redis) per verificare l'identità dell'utente.
Il JSON Web Token (JWT) nasce per risolvere questo problema introducendo un approccio stateless. Un JWT è un token compatto e autonomo che contiene tutte le informazioni necessarie per identificare l'utente, firmate digitalmente dal server. In questo modo, il server non ha bisogno di memorizzare lo stato della sessione; gli basta validare la firma del token ricevuto per sapere chi è l'utente e quali permessi possiede.
Com'è strutturato un JWT?
Un token JWT è composto da tre parti separate da un punto (.):
- Header: Contiene il tipo di token (JWT) e l'algoritmo di hashing utilizzato (es. HS256).
- Payload: Contiene i cosiddetti claims, ovvero i dati dell'utente (ID, ruolo, scadenza). Attenzione: questi dati sono codificati in Base64, non criptati. Chiunque intercetti il token può leggerne il contenuto.
- Signature: È la parte cruciale. Viene creata combinando l'header, il payload e una chiave segreta nota solo al server. Se un singolo bit del payload viene modificato, la firma non corrisponderà più, rendendo il token invalido.
Architettura del Flusso di Autenticazione
Per implementare correttamente JWT in Node.js, dobbiamo comprendere il ciclo di vita di una richiesta autenticata. Il flusso tipico segue questi passaggi:
- Login: L'utente invia le proprie credenziali (email e password) tramite una richiesta POST.
- Verifica: Il server verifica le credenziali nel database. Se corrette, genera un JWT utilizzando una chiave segreta.
- Invio: Il server risponde al client includendo il token (solitamente nell'header o nel corpo della risposta).
- Storage: Il client salva il token (es. in LocalStorage o, preferibilmente, in un cookie HttpOnly).
- Richieste Protette: Per ogni richiesta a endpoint riservati, il client include il token nell'header
Authorization: Bearer <token>. - Validazione: Il server riceve il token, ne verifica la firma e la scadenza, ed estrae i dati dell'utente per autorizzare l'operazione.
Implementazione Pratica in Node.js
Per questo tutorial utilizzeremo express come framework web e jsonwebtoken per la gestione dei token. Assicuriamoci di installare le dipendenze necessarie:
npm install express jsonwebtoken bcryptjs dotenv
1. Configurazione e Generazione del Token
Iniziamo creando un sistema di login. Utilizzeremo bcryptjs per gestire le password in modo sicuro, poiché non dovremmo mai salvare password in chiaro nel database.
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
require('dotenv').config();
const app = express();
app.use(express.json());
const SECRET_KEY = process.env.JWT_SECRET || 'la_mia_chiave_segretissima';
// Database simulato
const users = [];
app.post('/register', async (req, res) => {
const { username, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
users.push({ username, password: hashedPassword });
res.status(201).send('Utente registrato con successo');
});
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username);
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ message: 'Credenziali non valide' });
}
// Creazione del token
// Il payload contiene l'username e una data di scadenza (1 ora)
const token = jwt.sign(
{ username: user.username, role: 'user' },
SECRET_KEY,
{ expiresIn: '1h' }
);
res.json({ token });
});
app.listen(3000, () => console.log('Server in ascolto su porta 3000'));
In questo codice, jwt.sign prende tre parametri: il payload (i dati che vogliamo trasportare), la chiave segreta per la firma e un oggetto opzionale per definire la durata del token. L'uso di dotenv è fondamentale per evitare di scrivere la chiave segreta direttamente nel codice sorgente.
2. Creazione del Middleware di Autenticazione
Per proteggere le rotte, non vogliamo riscrivere la logica di verifica in ogni endpoint. Creeremo un middleware che intercetta la richiesta, estrae il token e lo convalida.
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Estrae <token> da 'Bearer <token>'
if (!token) {
return res.status(401).json({ message: 'Token mancante' });
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.status(403).json({ message: 'Token non valido o scaduto' });
}
req.user = user; // Aggiungiamo i dati dell'utente all'oggetto richiesta
next(); // Procediamo al controller successivo
});
};
// Esempio di rotta protetta
app.get('/profile', authenticateToken, (req, res) => {
res.json({ message: `Benvenuto nel tuo profilo, ${req.user.username}!` });
});
Il metodo jwt.verify controlla che il token non sia stato manomesso e che non sia scaduto. Se la verifica ha successo, l'oggetto user (il payload decodificato) viene passato alla richiesta, rendendolo disponibile per tutte le logiche di business successive.
Gestione Avanzata: Access Tokens e Refresh Tokens
Un problema critico dei JWT è l'invalidazione. Poiché il server non tiene traccia dei token emessi, non può "disconnettere" un utente prima che il token scada. Se un token con durata di 24 ore venisse rubato, l'attaccante avrebbe accesso per un intero giorno.
La soluzione professionale consiste nell'utilizzare due tipi di token:
Access Token
- Durata breve (es. 15 minuti).
- Utilizzato per ogni richiesta API.
- Memorizzato in memoria (JavaScript state) per ridurre il rischio di XSS.
Refresh Token
- Durata lunga (es. 7 giorni).
- Utilizzato esclusivamente per richiedere un nuovo Access Token quando quello precedente scade.
- Memorizzato nel database lato server e inviato al client tramite un cookie
HttpOnlyeSecure.
Quando l'Access Token scade, il client invia il Refresh Token a un endpoint specifico (/refresh). Il server verifica che il Refresh Token sia ancora valido nel database e ne emette uno nuovo insieme a un nuovo Access Token. Questo permette di mantenere l'utente connesso senza compromettere la sicurezza.
Esempi Pratici e Casi d'Uso Reali
Caso 1: Autorizzazione basata sui Ruoli (RBAC)
In un'applicazione aziendale, non tutti gli utenti possono accedere a ogni risorsa. Possiamo estendere il nostro middleware per controllare il ruolo contenuto nel JWT.
const authorizeRoles = (...allowedRoles) => {
return (req, res, next) => {
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({ message: 'Accesso negato: permessi insufficienti' });
}
next();
};
};
// Solo gli amministratori possono eliminare utenti
app.delete('/users/:id', authenticateToken, authorizeRoles('admin'), (req, res) => {
res.send('Utente eliminato correttamente');
});
Caso 2: Integrazione con MongoDB
In un'app reale, invece di un array users, useremmo Mongoose. La logica rimarrebbe simile, ma la ricerca dell'utente avverrebbe tramite User.findOne({ username }). Il payload del JWT conterrebbe l' _id di MongoDB per rendere le query più efficienti.
Errori Comuni e FAQ
1. Salvare dati sensibili nel payload
Errore: Inserire la password o il numero di carta di credito nel JWT. Perché: Il payload è solo codificato in Base64. Chiunque possieda il token può leggere i dati usando strumenti online come jwt.io. Inserite solo l'ID utente, il ruolo o informazioni non critiche.
2. Non gestire la scadenza (exp)
Errore: Creare token che non scadono mai.
Perché: Se un token viene compromesso, l'accesso sarebbe perpetuo. Impostate sempre un expiresIn ragionevole.
3. Memorizzare il token in LocalStorage
Errore: Salvare il JWT in localStorage.setItem('token', token).
Perché: Il LocalStorage è accessibile da qualsiasi script JavaScript nella pagina, rendendo l'app vulnerabile agli attacchi XSS (Cross-Site Scripting). La pratica migliore è l'uso di cookie HttpOnly, che non sono accessibili via JS.
4. Dimenticare di validare l'algoritmo
Errore: Accettare token con l'algoritmo none.
Perché: Alcune vecchie versioni di librerie JWT permettevano l'algoritmo none, che bypassava la firma. Assicuratevi di usare versioni aggiornate della libreria jsonwebtoken.
Prossimi Passi e Approfondimenti
L'autenticazione con JWT è un punto di partenza, ma per applicazioni di livello enterprise è necessario approfondire altri concetti di sicurezza:
- OAuth2 e OpenID Connect: Se l'applicazione deve permettere il login tramite Google, GitHub o Microsoft, studiate questi standard che utilizzano JWT come base.
- CORS (Cross-Origin Resource Sharing): Configurate correttamente i middleware CORS per permettere solo al vostro frontend ufficiale di comunicare con l'API.
- Rate Limiting: Implementate strumenti come
express-rate-limitper prevenire attacchi brute-force agli endpoint di login. - Helmet.js: Utilizzate questo middleware per aggiungere header di sicurezza HTTP che proteggono l'app da diverse vulnerabilità comuni.
In conclusione, i JWT offrono un modo elegante e performante per gestire l'identità in sistemi distribuiti. La chiave del successo risiede non solo nell'implementazione tecnica, ma nella rigorosa applicazione delle best practices di sicurezza per proteggere i dati degli utenti.