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.
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à.
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 standard
Significato
Esempio
sub
Subject — chi è il token
ID utente: "42"
iat
Issued At — quando è stato emesso
Unix timestamp
exp
Expiration — quando scade
Unix timestamp futuro
iss
Issuer — 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.
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
⚠️ 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
Status
Quando
Perché
400
Campi mancanti
Validazione input base
409
Email già presente
Conflitto risorsa duplicata
422
Password < 8 chars
Regola di dominio non rispettata
401
Login fallito
Credenziali 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.
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.
Storage
Pro
Contro
Quando usarlo
localStorage
Semplice, persiste al refresh, supportato ovunque
Vulnerabile a XSS: script malevoli possono leggere il token
App interne, prototipi, basso rischio XSS
Cookie HttpOnly
Inaccessibile da JS → protetto da XSS
Più complesso, richiede gestione CSRF
✅ Produzione con dati sensibili
sessionStorage
Cancellato alla chiusura tab
Non condiviso tra tab, non persiste
Token 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:
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.
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?
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.
10. Riepilogo e concetti chiave
✓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.
✓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.