L09 - Autenticazione JWT | React Vite Node | Prof. Giagnotti
PROF GIAGNOTTI · TORNA A → · GUIDE
📘 GUIDE · React + Vite · Lezione 9 di 10
Docente: Prof. Giagnotti · Sviluppo Web Moderno

Lezione 9 — Autenticazione JWT: login, hash e rotte protette

Le note di DevNotes sono pubbliche. Chiunque le legge, crea, cancella. In questa lezione aggiungi un sistema di autenticazione completo: password hashata con bcrypt, token JWT firmato dal server, route protette lato backend e lato React con AuthContext e ProtectedRoute.

⏱ ~25 min lettura 🎯 Livello: Avanzato 💻 Prerequisiti: L07-L08 Express + fetch ⚗️ Lab Pratico — lab-09-auth-jwt.md ↗ 🧩 Serie: DevNotes 🔐 Focus: JWT · bcrypt · AuthContext
✓Capire struttura JWT (Header · Payload · Signature) e differenza tra autenticare e cifrare
✓Implementare register/login con bcrypt, jwt.sign e gestione errori 400/401/409
✓Creare authMiddleware che verifica Bearer token e popola req.user
✓Costruire AuthContext con login/logout, hook useAuth e persistenza in localStorage
✓Proteggere rotte Express e React con ProtectedRoute, gestire 401 e scadenza token

1. Cos'è un JWT e come funziona

Un JSON Web Token è una stringa codificata in Base64url composta da tre segmenti separati da un punto. È uno standard aperto RFC 7519 per trasmettere claims tra due parti in modo verificabile. È stateless: il server non conserva sessione.

📌 Definizione da ricordare

JWT = Header (algoritmo + tipo) + Payload (claims come sub, iat, exp) + Signature (HMAC-SHA256 con JWT_SECRET). Header e payload sono solo Base64url, leggibili da chiunque. La signature garantisce integrità.

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciJ9 . SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header (algo + tipo)   Payload (claims)   Signature (HMAC-SHA256)

Il server firma il token con una chiave segreta che solo lui conosce. Chiunque può leggere header e payload, ma nessuno può modificarli senza invalidare la firma. Questo permette al server di verificare l'autenticità senza consultare il database ad ogni richiesta — differenza fondamentale rispetto alle sessioni tradizionali.

Claim standardSignificatoEsempio
subSubject — chi è il tokenID utente: "42"
iatIssued At — quando è stato emessoUnix timestamp
expExpiration — quando scadeUnix timestamp futuro
issIssuer — chi ha emesso il token"devnotes-api"
⚠️ Errore comune visto in aula — payload non cifrato

Il payload è leggibile da chiunque decodifichi Base64url. Non mettere mai password, carte di credito, dati medici dentro il JWT. Il JWT autentica chi sei, non nasconde cosa hai. Se ti serve segretezza usa JWE o non mettere il dato nel token.

💬 Tip del docente

Quando spiego JWT in classe uso jwt.io. Copia un token e vedi subito header e payload decodificati. È il modo più veloce per capire che il token non è magia, è solo JSON firmato. Mostralo a un compagno: capirà in 2 minuti.

2. Il flusso completo login → richiesta protetta

Flusso che implementeremo: registrazione con hash, login con verifica, firma token, salvataggio in localStorage, invio in ogni richiesta, verifica con middleware, 401 se assente o scaduto.

CLIENT REACT :5173 POST /api/auth/login {email,pw} → EXPRESS SERVER :3001 bcrypt.compare() verifica password jwt.sign(payload, SECRET) ← { token: eyJ… } — localStorage salva tokenAuthContext login(token) ogni richiesta successiva GET /api/notes + Authorization: Bearer eyJ… ↓ authMiddleware jwt.verify() Route Handler req.user → dati ✅ 401 se assente/scaduto/manomesso

Flusso JWT — login, salvataggio, invio Bearer, verifica middleware

🎯 Esempio in classe

DevNotes senza auth è come un armadietto senza lucchetto: tutti aprono tutto. Con JWT ogni studente vede solo le sue note perché req.user.sub filtra per id. Prova a decodificare il token di due utenti diversi su jwt.io: cambia solo sub ed email.

3. Backend — dipendenze e struttura con .env

Installa i pacchetti necessari

TERMINALE · Backend
# nella cartella backend/ npm install jsonwebtoken bcrypt dotenv# jsonwebtoken : firma e verifica JWT # bcrypt : hashing sicuro delle password (10 round = ~100ms) # dotenv : carica variabili d'ambiente da .env

Struttura aggiornata del backend

STRUTTURA
backend/ ├── .env # JWT_SECRET e PORT (non committare!) ├── server.js ├── data/ │ ├── notes.js │ └── users.js # store in-memory degli utenti ├── middleware/ │ └── authMiddleware.js # verifica il JWT └── routes/ ├── notes.js └── auth.js # /register e /login

Il file .env

ENV · backend/.env
PORT=3001 JWT_SECRET=cambia_questa_stringa_con_qualcosa_di_lungo_e_casuale_min_32_chars JWT_EXPIRES_IN=7d NODE_ENV=development
⚠️ Mai committare .env — errore che blocca in laboratorio

Aggiungi .env al .gitignore prima del primo commit. Un JWT_SECRET esposto su GitHub permette a chiunque di forgiare token validi. In produzione usa le variabili d'ambiente della piattaforma di deploy (vedi L10 su Vercel e Render).

4. Store utenti e rotte /register /login con bcrypt

In-memory per semplicità didattica. In produzione sostituisci con database reale. Hash con bcrypt e validazione lunga password.

JS · backend/data/users.js
// Store in-memory: sostituire con un DB reale in produzione const users = [] let nextId = 1function findByEmail(email) { return users.find(u => u.email === email.toLowerCase()) }function createUser({ email, passwordHash }) { const user = { id: nextId++, email: email.toLowerCase(), passwordHash } users.push(user) return user }module.exports = { findByEmail, createUser }
JS · backend/routes/auth.js
const router = require('express').Router() const bcrypt = require('bcrypt') const jwt = require('jsonwebtoken') const { findByEmail, createUser } = require('../data/users')const SALT_ROUNDS = 10 // ~100ms su hardware moderno — bilanciamento sicurezza/performance// ─── POST /api/auth/register ─────────────────── router.post('/register', async (req, res, next) => { try { const { email, password } = req.body if (!email || !password) return res.status(400).json({ messaggio: 'Email e password obbligatorie' }) if (findByEmail(email)) return res.status(409).json({ messaggio: 'Email già registrata' }) if (password.length < 8) return res.status(422).json({ messaggio: 'Password min 8 caratteri' })const passwordHash = await bcrypt.hash(password, SALT_ROUNDS) const user = createUser({ email, passwordHash }) res.status(201).json({ messaggio: 'Registrazione completata', id: user.id }) } catch (err) { next(err) } })// ─── POST /api/auth/login ────────────────────── router.post('/login', async (req, res, next) => { try { const { email, password } = req.body const user = findByEmail(email)// Risposta generica: non rivelare se l'email esiste o no (timing attack) if (!user) return res.status(401).json({ messaggio: 'Credenziali non valide' })const match = await bcrypt.compare(password, user.passwordHash) if (!match) return res.status(401).json({ messaggio: 'Credenziali non valide' })const token = jwt.sign( { sub: String(user.id), email: user.email }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || '7d' } ) res.json({ token, email: user.email }) } catch (err) { next(err) } })module.exports = router
StatusQuandoPerché
400Campi mancantiValidazione input base
409Email già presenteConflitto risorsa duplicata
422Password < 8 charsRegola di dominio non rispettata
401Login fallitoCredenziali non valide generico anti-enumeration
💬 Tip del docente — bcrypt

In laboratorio confronta bcrypt.hash con SHA256. Con bcrypt ogni hash è diverso anche per stessa password grazie al salt interno. SALT_ROUNDS=10 è il compromesso che usiamo: sotto 10 è veloce ma debole, sopra 12 inizia a diventare lento su Render free tier.

5. Middleware authMiddleware — Bearer token e jwt.verify

Il middleware legge header Authorization: Bearer <token>, verifica firma e aggiunge utente decodificato a req.user per le route successive.

JS · backend/middleware/authMiddleware.js
const jwt = require('jsonwebtoken')function authMiddleware(req, res, next) { const authHeader = req.headers['authorization']// Il formato atteso è: "Bearer eyJ..." if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ messaggio: 'Token mancante' }) }const token = authHeader.slice(7) // rimuove "Bearer "try { // Verifica firma e scadenza in un'unica operazione const payload = jwt.verify(token, process.env.JWT_SECRET) req.user = payload // disponibile nelle route: req.user.sub, req.user.email next() } catch (err) { const msg = err.name === 'TokenExpiredError' ? 'Token scaduto' : 'Token non valido' res.status(401).json({ messaggio: msg }) } }module.exports = authMiddleware

Applicare il middleware alle route protette in server.js

JS · backend/server.js — modifiche
require('dotenv').config() // carica .env PRIMA di tutto il resto const authMiddleware = require('./middleware/authMiddleware') const authRoutes = require('./routes/auth') const notesRoutes = require('./routes/notes')// Route pubbliche (no auth) app.use('/api/auth', authRoutes)// Route protette: authMiddleware viene eseguito prima di notesRoutes app.use('/api/notes', authMiddleware, notesRoutes)
⚠️ Errore comune — dotenv dopo le route

Se chiami require('dotenv').config() dopo aver già letto process.env.JWT_SECRET, la secret sarà undefined e jwt.sign firma con undefined. Il token risulterà sempre invalido. Metti dotenv come prima riga di server.js, prima di qualsiasi import che usa env.

6. Frontend — AuthContext e useAuth con localStorage

Il token JWT e lo stato di autenticazione devono essere accessibili da qualsiasi componente. La soluzione React standard è il Context API con inizializzazione lazy da localStorage.

JSX · frontend/src/context/AuthContext.jsx
import { createContext, useContext, useState, useCallback } from 'react'const AuthContext = createContext(null)export function AuthProvider({ children }) { // Inizializzazione lazy: legge localStorage una sola volta al mount const [token, setToken] = useState(() => localStorage.getItem('devnotes_token')) const [userEmail, setUserEmail] = useState(() => localStorage.getItem('devnotes_email'))const login = useCallback((newToken, email) => { localStorage.setItem('devnotes_token', newToken) localStorage.setItem('devnotes_email', email) setToken(newToken) setUserEmail(email) }, [])const logout = useCallback(() => { localStorage.removeItem('devnotes_token') localStorage.removeItem('devnotes_email') setToken(null) setUserEmail(null) }, [])return ( <AuthContext.Provider value={{ token, userEmail, login, logout, isAuth: !!token }}> {children} </AuthContext.Provider> ) }// Hook personalizzato: evita di importare useContext + AuthContext ovunque export function useAuth() { const ctx = useContext(AuthContext) if (!ctx) throw new Error('useAuth deve essere usato dentro AuthProvider') return ctx }
💬 Tip del docente — inizializzazione lazy

Nota la arrow function dentro useState(() => localStorage.getItem(...)). Senza arrow, getItem verrebbe chiamato ad ogni render. Con arrow è lazy: una sola volta al primo mount. Su app grandi questa differenza si sente. Spiegalo in verifica: è domanda classica.

7. Inviare il token e ProtectedRoute in React Router

Il layer API deve includere il token JWT nell'header Authorization di ogni richiesta alle route protette. E lato router serve un componente che reindirizza al login se non autenticati.

JS · frontend/src/api/notesApi.js — versione con auth
const API_BASE = '/api/notes'// Helper: costruisce gli header con il token JWT function authHeaders(extraHeaders = {}) { const token = localStorage.getItem('devnotes_token') return { 'Content-Type': 'application/json', ...(token && { 'Authorization': `Bearer ${token}` }), ...extraHeaders, } }export async function fetchNotes() { const res = await fetch(API_BASE, { headers: authHeaders() }) if (!res.ok) throw new Error(`Errore GET: ${res.status}`) return res.json() }// createNote e deleteNote: stessa modifica — aggiungi headers: authHeaders()
JSX · frontend/src/components/ProtectedRoute.jsx
import { Navigate, Outlet } from 'react-router-dom' import { useAuth } from '../context/AuthContext'export default function ProtectedRoute() { const { isAuth } = useAuth() // Se non autenticato, reindirizza al login conservando la destinazione return isAuth ? <Outlet /> : <Navigate to="/login" replace /> }
JSX · frontend/src/main.jsx — route con protezione
import { BrowserRouter, Routes, Route } from 'react-router-dom' import { AuthProvider } from './context/AuthContext' import ProtectedRoute from './components/ProtectedRoute' import App from './App' import LoginPage from './pages/LoginPage'ReactDOM.createRoot(document.getElementById('root')).render( <BrowserRouter> <AuthProvider> <Routes> <Route path="/login" element={<LoginPage />} /> {/* Tutte le route figlie di ProtectedRoute richiedono autenticazione */} <Route element={<ProtectedRoute />}> <Route path="/" element={<App />} /> </Route> </Routes> </AuthProvider> </BrowserRouter> )

8. localStorage vs cookie HttpOnly — pro, contro, deploy

Scelta di storage impatta sicurezza e UX. Per questa lezione usiamo localStorage perché è didattico, ma in produzione valuta cookie HttpOnly.

StorageProControQuando usarlo
localStorageSemplice, persiste al refresh, supportato ovunqueVulnerabile a XSS: script malevoli possono leggere il tokenApp interne, prototipi, basso rischio XSS
Cookie HttpOnlyInaccessibile da JS → protetto da XSSPiù complesso, richiede gestione CSRF✅ Produzione con dati sensibili
sessionStorageCancellato alla chiusura tabNon condiviso tra tab, non persisteToken temporanei, sessioni one-shot
📌 Ricorda per deploy

JWT_SECRET deve essere lungo, casuale, diverso tra sviluppo e produzione. JWT_EXPIRES_IN in produzione meglio breve (1h) + refresh token. Non committare .env: su Render e Vercel usa Environment Variables dashboard.

9. Verifica / Esercizio pratico

🔬 Attività — Lab 09

Apri il lab pratico dal meta-pill giallo in alto. Esegui tutti i passaggi e rispondi:

  1. Registra due utenti diversi e prova a fare login con email giusta e password sbagliata: quale status ricevi e perché il messaggio è generico? Intercetta la risposta in DevTools Network.
  2. Apri jwt.io e incolla il token ricevuto dal login: quali claims vedi in payload? Modifica una lettera della signature e prova a chiamare GET /api/notes: cosa succede?
  3. Rimuovi manualmente devnotes_token da localStorage e ricarica la pagina protetta: dove vieni reindirizzato? Ripristina login e verifica che authHeaders includa Bearer.
✅ Soluzione ragionata

1. Ricevi 401 "Credenziali non valide" sia per email inesistente sia per password errata: è anti-enumeration, non riveliamo se email esiste. 2. Su jwt.io vedi sub, email, iat, exp. Se tocchi signature, jwt.verify lancia JsonWebTokenError → 401 Token non valido. 3. Senza token isAuth è false → ProtectedRoute ritorna <Navigate to="/login" />. Dopo login, authHeaders aggiunge Authorization: Bearer eyJ... e GET /api/notes passa middleware.

✓JWT RFC 7519 — tre segmenti Base64url: header, payload, signature HMAC-SHA256 con chiave segreta.
✓bcrypt SALT_ROUNDS=10 — hashing one-way con salt automatico, ~100ms, bilancia sicurezza/performance.
✓jwt.sign / verify — sign firma payload con exp automatico, verify controlla firma e scadenza in un colpo.
✓authMiddleware — legge Authorization Bearer, popola req.user, risponde 401 Token mancante/scaduto/non valido.
✓AuthContext + useAuth + ProtectedRoute — stato globale auth, hook custom, pattern Outlet/Navigate replace.
✓localStorage vs HttpOnly — localStorage semplice ma XSS-vulnerabile, cookie HttpOnly per produzione sensibile.

⚗️ Hai completato la teoria? Passa al laboratorio pratico.

Nel lab L09 implementerai registrazione, login, middleware, AuthContext e ProtectedRoute su DevNotes. Trovi checklist completa, test con curl/Postman e gestione 401 nel frontend. Clicca il meta-pill giallo in alto per aprire lab-09-auth-jwt.md.