Autenticazione base con JWT
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.
/api/auth/register
e /api/auth/login + un middleware che protegge /api/notes.
Il frontend React avrà un AuthContext, una pagina di login, un hook useAuth
e un componente ProtectedRoute che reindirizza al login se non autenticati.
Il token JWT vivrà nel localStorage.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 e opzionalmente cifrato.
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 (sono solo Base64url, non cifrati), ma nessuno può modificarli senza invalidare la firma. Questo permette al server di verificare l’autenticità senza dover consultare il database ad ogni richiesta — è la 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" |
2. Il flusso completo login → richiesta protetta
salva token
jwt.verify(token, SECRET)
dati ✅
3. Backend — dipendenze e struttura
Installa i pacchetti necessari
# 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
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
// 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
.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).4. Store utenti in-memory e rotte auth
// backend/data/users.js // Store in-memory: sostituire con un DB reale in produzione const users = [] let nextId = 1 function 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 }
// 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
5. Il middleware authMiddleware.js
Il middleware legge l’header Authorization: Bearer <token>, verifica
la firma e aggiunge l’utente decodificato a req.user per le route successive.
// 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
// 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)
6. Frontend — AuthContext e useAuth
Il token JWT e lo stato di autenticazione devono essere accessibili da qualsiasi componente. La soluzione React standard è il Context API.
// 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 }
7. Inviare il token in ogni richiesta
Il layer API deve includere il token JWT nell’header Authorization di ogni
richiesta alle route protette. Aggiorna notesApi.js con una funzione helper:
// frontend/src/api/notesApi.js — versione aggiornata 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()
8. Route protette in React: ProtectedRoute
// 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 /> }
// frontend/src/main.jsx — struttura 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> )
9. localStorage: vantaggi, limiti e alternative
| 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 JavaScript → protetto da XSS | Più complesso da implementare, richiede gestione CSRF | ✅ Produzione con dati sensibili |
| sessionStorage | Cancellato alla chiusura tab | Non condiviso tra tab, non persiste | Token temporanei, sessioni one-shot |
localStorage perché è la scelta più didattica e comprensibile.
In L10 (deploy) vedrai come configurare le variabili d’ambiente per la produzione.
Per un’app reale con dati sensibili, considera i cookie HttpOnly come passo successivo.Laboratorio L09 — Autenticazione con JWT
Implementa il sistema di autenticazione completo su DevNotes: registrazione, login, token JWT, middleware Express, AuthContext e route protette React.
→ Apri il laboratorioRiepilogo lezione
- JWT (RFC 7519) — tre segmenti Base64url: header, payload, firma HMAC-SHA256 con chiave segreta
- bcrypt — hashing one-way con salt;
SALT_ROUNDS=10bilancia sicurezza e performance - Risposta generica al login — “Credenziali non valide” sia per email inesistente sia per password errata (anti-enumeration)
jwt.sign()— firma il payload con la chiave segreta e aggiungeexpautomaticamentejwt.verify()— verifica firma e scadenza in un’unica chiamata; lancia se non valido- Middleware Express — legge
Authorization: Bearer <token>, aggiungereq.user - AuthContext + useAuth — stato di autenticazione globale con inizializzazione lazy da localStorage
- ProtectedRoute — pattern con
<Outlet />e<Navigate replace /> - localStorage vs HttpOnly Cookie — localStorage è più semplice ma vulnerabile a XSS
Risorse
Lo standard originale: struttura, claims registrati, algoritmi supportati, requisiti di sicurezza
Documentazione della libreria: sign(), verify(), decode(), opzioni algoritmo
Funzioni hash() e compare(), spiegazione dei salt rounds e impatto sulle performance
Best practice di sicurezza: algoritmo alg: none attack, validazione claims, rotazione dei segreti
Strumento per decodificare e ispezionare token JWT nel browser: utile per debug durante il lab