Lezione 9 – Autenticazione base con JWT

🔐 React + Vite + Node.js — Guida completa · Lezione 09 di 10
← L08 — Connettere frontend e backend

LEZIONE 09  ·  AUTENTICAZIONE  ·  ADVANCED

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.


// cosa costruiremo Un flusso login/logout completo su DevNotes: il backend Express avrà route /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.

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 (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 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"
⚡ I JWT non sono cifrati Il payload è leggibile da chiunque decodifichi la stringa Base64url. Non mettere mai dati sensibili nel payload: password, numeri di carta di credito, dati medici. Il JWT autentica (chi sei) ma non nasconde (cosa hai).

2. Il flusso completo login → richiesta protetta

Flusso autenticazione JWT — dalla registrazione alla richiesta protetta
Client React
──POST /api/auth/login──→
Express Server
↓ verifica password con bcrypt.compare()
localStorage
salva token
←── { token: “eyJ…” } ──
jwt.sign(payload, SECRET)
↓ ogni richiesta successiva
Client React
──GET /api/notes──→
Middleware auth
jwt.verify(token, SECRET)
──→
Route Handler
dati ✅
token assente/scaduto/manomesso
──→
401 Unauthorized

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
🔒 Mai committare .env 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).

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

StorageProControQuando usarlo
localStorageSemplice, persiste al refresh, supportato ovunqueVulnerabile a XSS: script malevoli possono leggere il tokenApp interne, prototipi, basso rischio XSS
Cookie HttpOnlyInaccessibile da JavaScript → protetto da XSSPiù complesso da implementare, richiede gestione CSRF✅ Produzione con dati sensibili
sessionStorageCancellato alla chiusura tabNon condiviso tra tab, non persisteToken temporanei, sessioni one-shot
✅ Per questa lezione Usiamo 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 laboratorio

Riepilogo lezione

  • JWT (RFC 7519) — tre segmenti Base64url: header, payload, firma HMAC-SHA256 con chiave segreta
  • bcrypt — hashing one-way con salt; SALT_ROUNDS=10 bilancia 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 aggiunge exp automaticamente
  • jwt.verify() — verifica firma e scadenza in un’unica chiamata; lancia se non valido
  • Middleware Express — legge Authorization: Bearer <token>, aggiunge req.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

RFC
RFC 7519 — JSON Web Token (JWT)

Lo standard originale: struttura, claims registrati, algoritmi supportati, requisiti di sicurezza

DOC
jsonwebtoken — GitHub

Documentazione della libreria: sign(), verify(), decode(), opzioni algoritmo

DOC
bcrypt.js — GitHub

Funzioni hash() e compare(), spiegazione dei salt rounds e impatto sulle performance

SEC
OWASP — JWT Security Cheat Sheet

Best practice di sicurezza: algoritmo alg: none attack, validazione claims, rotazione dei segreti

GUIDE
jwt.io — Debugger interattivo

Strumento per decodificare e ispezionare token JWT nel browser: utile per debug durante il lab


Lascia un commento