{"id":196,"date":"2026-06-17T21:02:42","date_gmt":"2026-06-17T21:02:42","guid":{"rendered":"https:\/\/shattered.io\/it\/2026\/06\/17\/oauth2-openid-connect-nodejs\/"},"modified":"2026-06-17T21:06:56","modified_gmt":"2026-06-17T21:06:56","slug":"oauth2-openid-connect-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/it\/2026\/06\/17\/oauth2-openid-connect-nodejs\/","title":{"rendered":"OAuth 2.0 e OpenID Connect in Node.js: 12 Step, 30 Min [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Ogni volta che un utente clicca su &#8220;Accedi con Google&#8221; o &#8220;Accedi con GitHub&#8221;, entra in azione OAuth 2.0. Il protocollo gestisce miliardi di autenticazioni ogni giorno, ma implementarlo correttamente in Node.js richiede pi\u00f9 di una semplice installazione di <code>passport<\/code>. Bisogna capire PKCE, la rotazione dei refresh token, il pinning degli algoritmi e la corretta gestione dei cookie. Questa guida copre tutti e 12 gli step in 30 minuti.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"cose-oauth-2-0-e-perche-usarlo\">Cos&#8217;\u00e8 OAuth 2.0 e Perch\u00e9 Usarlo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 (RFC 6749) \u00e8 un framework di autorizzazione che permette a un&#8217;applicazione di ottenere accesso limitato a un account utente su un servizio di terze parti, senza che l&#8217;utente condivida le proprie credenziali. OpenID Connect (OIDC) \u00e8 uno strato di identit\u00e0 costruito sopra OAuth 2.0: aggiunge l&#8217;autenticazione vera e propria tramite un <em>ID token<\/em> in formato JWT.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La differenza pratica: OAuth 2.0 risponde alla domanda &#8220;posso accedere a questa risorsa?&#8221;, OIDC risponde a &#8220;chi sei?&#8221;. Nelle applicazioni moderne, i due protocolli vengono usati insieme. L&#8217;<em>authorization server<\/em> (Google, GitHub, Keycloak, Auth0) emette un <em>access token<\/em> per le API e un <em>ID token<\/em> per l&#8217;identit\u00e0.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il flusso pi\u00f9 sicuro nel 2026 \u00e8 l&#8217;<strong>Authorization Code Flow con PKCE<\/strong> (RFC 7636). PKCE, pronunciato &#8220;pixy&#8221;, risolve il rischio di intercettazione del codice di autorizzazione nei client pubblici (SPA, app mobile), rendendo ogni richiesta crittograficamente legata a un verificatore segreto generato localmente. RFC 9700 (OAuth 2.0 Security Best Current Practice) pubblicato nel 2025 raccomanda PKCE per tutti i tipi di client, senza eccezioni.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr><th>Flusso OAuth 2.0<\/th><th>Caso d&#8217;uso<\/th><th>PKCE richiesto<\/th><th>Livello di sicurezza<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>Authorization Code + PKCE<\/td><td>Web app, SPA, mobile<\/td><td>Si<\/td><td>Alto<\/td><\/tr>\n<tr><td>Authorization Code (senza PKCE)<\/td><td>Web app server-side legacy<\/td><td>No (sconsigliato)<\/td><td>Medio<\/td><\/tr>\n<tr><td>Client Credentials<\/td><td>M2M, microservizi<\/td><td>No<\/td><td>Alto (per M2M)<\/td><\/tr>\n<tr><td>Device Authorization<\/td><td>Smart TV, CLI<\/td><td>No<\/td><td>Medio<\/td><\/tr>\n<tr><td>Implicit Flow<\/td><td>Deprecato dal 2019<\/td><td>N\/A<\/td><td>Basso (non usare)<\/td><\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequisiti-e-versioni\">Prerequisiti e Versioni<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di iniziare, verifica di avere le versioni corrette installate. Il progetto usa Node.js LTS: la versione 22.x (LTS attivo) o 24.x (LTS corrente). A gennaio 2026 il team Node.js ha rilasciato patch di sicurezza per tutte le linee attive, risolvendo 2 vulnerabilit\u00e0 ad alta severit\u00e0 per versione, tra cui CVE-2026-21637 (HashDoS in V8 tramite collisioni hash su stringhe integer-like). Il 17 giugno 2026, data di pubblicazione di questa guida, \u00e8 uscita un&#8217;ulteriore tornata di patch per 26.x, 24.x e 22.x.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node -v      # &gt;= 20.20.2 oppure &gt;= 22.22.2 oppure &gt;= 24.14.1\nnpm -v       # &gt;= 10.x\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pacchetti necessari con versioni minime:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr><th>Pacchetto<\/th><th>Versione<\/th><th>Scopo<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>express<\/td><td>^5.x<\/td><td>Framework HTTP<\/td><\/tr>\n<tr><td>passport<\/td><td>^0.7.x<\/td><td>Middleware autenticazione<\/td><\/tr>\n<tr><td>passport-google-oauth20<\/td><td>^2.0.x<\/td><td>Strategy OAuth2 Google<\/td><\/tr>\n<tr><td>express-session<\/td><td>^1.18.x<\/td><td>Gestione sessioni lato server<\/td><\/tr>\n<tr><td>connect-redis<\/td><td>^8.x<\/td><td>Store sessioni Redis<\/td><\/tr>\n<tr><td>jose<\/td><td>^6.x<\/td><td>Verifica JWT e ID token OIDC<\/td><\/tr>\n<tr><td>dotenv<\/td><td>^16.x<\/td><td>Variabili d&#8217;ambiente<\/td><\/tr>\n<tr><td>crypto (built-in)<\/td><td>N\/A<\/td><td>Generazione PKCE code verifier<\/td><\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Ti serve anche un <strong>account Google Cloud<\/strong> con un progetto configurato in Google Cloud Console e le credenziali OAuth2 (Client ID e Client Secret). La sezione Step 3 mostra la procedura esatta. Per i test locali, puoi usare anche GitHub come provider alternativo con il pacchetto <code>passport-github2<\/code>, che segue lo stesso schema di configurazione.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"panoramica-del-flusso-authorization-code-con-pkce\">Panoramica del Flusso Authorization Code con PKCE<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di scrivere codice, capire il flusso evita molti errori di implementazione. Con PKCE, la sequenza tra browser, server Node.js e authorization server segue 6 passaggi distinti:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Il server genera un <code>code_verifier<\/code> casuale (32 byte, 256 bit di entropia) e ne calcola il <code>code_challenge<\/code> con SHA-256.<\/li>\n<li>Il server reindirizza l&#8217;utente all&#8217;authorization server inviando <code>code_challenge<\/code> e <code>code_challenge_method=S256<\/code>.<\/li>\n<li>L&#8217;utente si autentica e l&#8217;authorization server rilascia un codice di autorizzazione monouso.<\/li>\n<li>Il browser invia il codice di autorizzazione al server Node.js tramite il redirect URI.<\/li>\n<li>Il server Node.js scambia il codice con i token inviando anche il <code>code_verifier<\/code> originale.<\/li>\n<li>L&#8217;authorization server verifica il verifier contro il challenge e rilascia <code>access_token<\/code>, <code>refresh_token<\/code> e (con OIDC) <code>id_token<\/code>.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Il punto critico: il <code>code_verifier<\/code> non lascia mai il server. Se un attaccante intercetta il codice di autorizzazione al passo 3, non pu\u00f2 scambiarlo senza il verifier. Questo neutralizza il classico attacco CSRF sull&#8217;endpoint di callback OAuth2.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-inizializza-il-progetto-node-js\">Step 1: Inizializza il Progetto Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crea una cartella per il progetto e inizializza npm. Il progetto usa ES modules per avere <code>import<\/code>\/<code>export<\/code> nativi, coerenti con le best practice Node.js 2026. CommonJS (<code>require()<\/code>) funziona ancora ma molti pacchetti moderni sono ora ESM-only.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir oauth2-oidc-nodejs &amp;&amp; cd oauth2-oidc-nodejs\nnpm init -y\n# Abilita ES modules\nnpm pkg set type=module\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Crea la struttura del progetto. Separare le route dalle utility di autenticazione facilita i test e riduce il coupling:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>oauth2-oidc-nodejs\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 app.js           # Entry point Express\n\u2502   \u251c\u2500\u2500 auth\/\n\u2502   \u2502   \u251c\u2500\u2500 passport.js  # Configurazione Passport e strategy\n\u2502   \u2502   \u2514\u2500\u2500 pkce.js      # Utility PKCE per provider custom\n\u2502   \u251c\u2500\u2500 middleware\/\n\u2502   \u2502   \u2514\u2500\u2500 requireAuth.js\n\u2502   \u2514\u2500\u2500 routes\/\n\u2502       \u251c\u2500\u2500 auth.js      # \/auth\/google, \/auth\/google\/callback, \/auth\/logout\n\u2502       \u2514\u2500\u2500 profile.js   # Route protette\n\u251c\u2500\u2500 .env                 # Non committare mai\n\u251c\u2500\u2500 .env.example         # Template pubblico senza segreti\n\u2514\u2500\u2500 package.json\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-2-installa-le-dipendenze\">Step 2: Installa le Dipendenze<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Installa tutti i pacchetti in un unico comando, poi esegui <code>npm audit<\/code> subito dopo. Nell&#8217;ecosistema Node.js del 2026, le dipendenze di terze parti rimangono la principale superficie di attacco per le supply chain. Il blog ha gi\u00e0 documentato come nel 2025 oltre 1,2 milioni di pacchetti malevoli siano stati caricati su npm.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install express passport passport-google-oauth20   express-session connect-redis jose dotenv helmet\n\nnpm install --save-dev nodemon\n\n# Verifica zero vulnerabilit\u00e0 critiche prima di procedere\nnpm audit\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Se <code>npm audit<\/code> segnala vulnerabilit\u00e0 high o critical nelle dipendenze dirette, risolvi prima di andare avanti. Per le vulnerabilit\u00e0 nelle dipendenze transitive, valuta se il percorso di codice \u00e8 effettivamente raggiungibile nella tua applicazione. Il comando <code>npm audit --json | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d[&#8216;metadata&#8217;][&#8216;vulnerabilities&#8217;])\"<\/code> mostra il conteggio per severit\u00e0.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-configura-le-credenziali-oauth2-su-google-cloud\">Step 3: Configura le Credenziali OAuth2 su Google Cloud<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Vai su <a href=\"https:\/\/console.cloud.google.com\/\" rel=\"nofollow noopener\" target=\"_blank\">console.cloud.google.com<\/a>, crea un progetto nuovo (o usa uno esistente), poi segui questo percorso: <strong>API e servizi &gt; Credenziali &gt; Crea credenziali &gt; ID client OAuth 2.0<\/strong>. Seleziona &#8220;Applicazione web&#8221;. In &#8220;URI di reindirizzamento autorizzati&#8221; aggiungi i due endpoint:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>http:\/\/localhost:3000\/auth\/google\/callback   # sviluppo locale\nhttps:\/\/tuodominio.com\/auth\/google\/callback  # produzione\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Scarica il JSON con Client ID e Client Secret. Crea il file <code>.env<\/code> nella root del progetto:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>GOOGLE_CLIENT_ID=123456789-abcdefghijk.apps.googleusercontent.com\nGOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxx\nSESSION_SECRET=una_stringa_casuale_di_almeno_32_caratteri_generata_con_openssl_rand\nREDIS_URL=redis:\/\/localhost:6379\nBASE_URL=http:\/\/localhost:3000\nNODE_ENV=development\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pitfall critica:<\/strong> il file <code>.env<\/code> deve essere nel <code>.gitignore<\/code> prima del primo commit. Una chiave OAuth2 esposta su GitHub viene rilevata dai bot entro minuti e porta alla revoca forzata delle credenziali da parte di Google. Aggiungi subito: <code>echo \".env\" >> .gitignore<\/code>. Genera il SESSION_SECRET con <code>openssl rand -hex 32<\/code>: serve almeno 256 bit di entropia.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-4-crea-il-server-express-con-session-store-redis\">Step 4: Crea il Server Express con Session Store Redis<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il session store in memoria di Express funziona solo in sviluppo: perde tutte le sessioni al riavvio del server e produce memory leak in produzione. Redis risolve entrambi i problemi e scala orizzontalmente per pi\u00f9 istanze. Il file <code>src\/app.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import 'dotenv\/config';\nimport express from 'express';\nimport helmet from 'helmet';\nimport session from 'express-session';\nimport { createClient } from 'redis';\nimport { RedisStore } from 'connect-redis';\nimport passport from 'passport';\nimport '.\/auth\/passport.js';\nimport authRouter from '.\/routes\/auth.js';\nimport profileRouter from '.\/routes\/profile.js';\n\nconst app = express();\nconst PORT = process.env.PORT || 3000;\n\n\/\/ Sicurezza header HTTP con Helmet\napp.use(helmet({\n  contentSecurityPolicy: {\n    directives: {\n      defaultSrc: [\"'self'\"],\n      scriptSrc: [\"'self'\"],\n      connectSrc: [\"'self'\", 'https:\/\/accounts.google.com'],\n      frameSrc: [\"'none'\"],\n      objectSrc: [\"'none'\"]\n    }\n  },\n  hsts: {\n    maxAge: 31536000,\n    includeSubDomains: true,\n    preload: true\n  }\n}));\n\n\/\/ Client Redis con gestione errori\nconst redisClient = createClient({ url: process.env.REDIS_URL });\nredisClient.on('error', (err) => console.error('Redis error:', err));\nawait redisClient.connect();\n\n\/\/ Sessione con Redis store\napp.use(session({\n  store: new RedisStore({ client: redisClient }),\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    httpOnly: true,\n    secure: process.env.NODE_ENV === 'production',\n    sameSite: 'lax',\n    maxAge: 24 * 60 * 60 * 1000  \/\/ 24 ore\n  }\n}));\n\napp.use(express.json());\napp.use(express.urlencoded({ extended: false }));\napp.use(passport.initialize());\napp.use(passport.session());\n\napp.use('\/auth', authRouter);\napp.use('\/profile', profileRouter);\n\napp.get('\/', (req, res) => {\n  res.json({ authenticated: req.isAuthenticated(), user: req.user || null });\n});\n\napp.listen(PORT, () => console.log(`Server avviato su http:\/\/localhost:${PORT}`));\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le tre flag del cookie, <code>httpOnly<\/code>, <code>secure<\/code> e <code>sameSite<\/code>, lavorano in sinergia. <code>httpOnly<\/code> impedisce a JavaScript (inclusi script XSS) di leggere il cookie di sessione. <code>secure<\/code> garantisce che il cookie viaggi solo su HTTPS. <code>sameSite: 'lax'<\/code> blocca le richieste cross-site non intenzionali senza rompere i link normali da email o siti esterni. In produzione con requisiti di sicurezza elevati, considera <code>sameSite: 'strict'<\/code>, ma attenzione: rompe il reindirizzamento OAuth2 quando arriva da una pagina esterna.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-5-configura-passport-js-con-google-oauth2\">Step 5: Configura Passport.js con Google OAuth2<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Passport.js usa il concetto di &#8220;strategy&#8221;: ogni provider ha la propria. La strategy <code>GoogleStrategy<\/code> implementa l&#8217;intero flusso Authorization Code con PKCE. Il file <code>src\/auth\/passport.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import passport from 'passport';\nimport { Strategy as GoogleStrategy } from 'passport-google-oauth20';\n\npassport.use(new GoogleStrategy({\n  clientID: process.env.GOOGLE_CLIENT_ID,\n  clientSecret: process.env.GOOGLE_CLIENT_SECRET,\n  callbackURL: `${process.env.BASE_URL}\/auth\/google\/callback`,\n  scope: ['openid', 'profile', 'email'],\n  pkce: true,   \/\/ PKCE abilitato (passport-google-oauth20 v2.0+)\n  state: true   \/\/ Protezione CSRF tramite state parameter\n}, async (accessToken, refreshToken, idTokenData, profile, done) => {\n  \/\/ Normalizza il profilo utente\n  const user = {\n    id: profile.id,\n    email: profile.emails?.[0]?.value,\n    name: profile.displayName,\n    picture: profile.photos?.[0]?.value\n    \/\/ Non salvare accessToken in database non cifrato\n  };\n\n  if (!user.email) {\n    return done(new Error('Nessuna email associata all account Google'));\n  }\n\n  \/\/ In produzione: cerca o crea l'utente nel database\n  \/\/ const dbUser = await upsertUser(user);\n  \/\/ return done(null, dbUser);\n\n  return done(null, user);\n}));\n\n\/\/ Serializzazione: salva solo l'ID nella sessione, non l'intero profilo\npassport.serializeUser((user, done) => {\n  done(null, user.id);\n});\n\n\/\/ Deserializzazione: ricostruisce l'utente a ogni richiesta autenticata\npassport.deserializeUser(async (id, done) => {\n  \/\/ In produzione: const user = await getUserById(id);\n  done(null, { id });\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La deserializzazione chiama il database a ogni richiesta autenticata. Con migliaia di utenti concorrenti, aggiungi un cache layer Redis con TTL di 5 minuti per i dati utente non critici. La serializzazione deve salvare il minimo indispensabile: solo l&#8217;ID, non l&#8217;intero profilo con email e foto. Meno dati in sessione, minore la superficie esposta in caso di session hijacking.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-6-implementa-le-route-di-autenticazione\">Step 6: Implementa le Route di Autenticazione<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le route di autenticazione coprono quattro endpoint: avvio del flusso OAuth2, callback di Google, logout e status. Il file <code>src\/routes\/auth.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import express from 'express';\nimport passport from 'passport';\n\nconst router = express.Router();\n\n\/\/ Avvia il flusso OAuth2 con PKCE\nrouter.get('\/google', passport.authenticate('google', {\n  scope: ['openid', 'profile', 'email'],\n  accessType: 'offline',   \/\/ Richiede refresh_token da Google\n  prompt: 'consent'        \/\/ Forza il consenso per ottenere refresh_token\n}));\n\n\/\/ Callback di Google dopo autenticazione\nrouter.get('\/google\/callback',\n  passport.authenticate('google', {\n    failureRedirect: '\/?error=auth_failed',\n    failureMessage: true\n  }),\n  (req, res) => {\n    \/\/ Rigenera l'ID sessione dopo il login (prevenzione session fixation)\n    req.session.regenerate((err) => {\n      if (err) return res.redirect('\/?error=session_error');\n      req.session.user = req.user;\n      req.session.save((err) => {\n        if (err) return res.redirect('\/?error=session_save');\n        res.redirect('\/profile');\n      });\n    });\n  }\n);\n\n\/\/ Logout sicuro con distruzione sessione\nrouter.post('\/logout', (req, res, next) => {\n  req.logout((err) => {\n    if (err) return next(err);\n    req.session.destroy((err) => {\n      if (err) return next(err);\n      res.clearCookie('connect.sid');\n      res.json({ success: true });\n    });\n  });\n});\n\n\/\/ Stato autenticazione per il frontend\nrouter.get('\/status', (req, res) => {\n  res.json({\n    authenticated: req.isAuthenticated(),\n    user: req.user ? { id: req.user.id, email: req.user.email } : null\n  });\n});\n\nexport default router;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La rigenerazione dell&#8217;ID sessione dopo il login con <code>req.session.regenerate()<\/code> previene il <em>session fixation attack<\/em>: un attaccante potrebbe fissare un ID sessione noto prima del login e usarlo per accedere all&#8217;account dopo l&#8217;autenticazione. Senza <code>regenerate()<\/code>, l&#8217;ID sessione rimane lo stesso prima e dopo il login, rendendo possibile l&#8217;attacco. Authgear classifica questa come pratica obbligatoria per le app Node.js nel 2026.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-7-protezione-delle-route-con-middleware\">Step 7: Protezione delle Route con Middleware<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il file <code>src\/middleware\/requireAuth.js<\/code> definisce due middleware riutilizzabili per la protezione delle route:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Blocca le richieste non autenticate\nexport function requireAuth(req, res, next) {\n  if (req.isAuthenticated()) return next();\n\n  \/\/ Per richieste API: risponde con 401 JSON\n  if (req.headers.accept?.includes('application\/json')) {\n    return res.status(401).json({ error: 'Authentication required' });\n  }\n\n  \/\/ Per richieste web: salva la destinazione e reindirizza al login\n  req.session.returnTo = req.originalUrl;\n  res.redirect('\/auth\/google');\n}\n\n\/\/ Reindirizza gli utenti gi\u00e0 autenticati (es. pagina di login)\nexport function requireGuest(req, res, next) {\n  if (!req.isAuthenticated()) return next();\n  res.redirect('\/profile');\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il file <code>src\/routes\/profile.js<\/code> applica il middleware alle route protette:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import express from 'express';\nimport { requireAuth } from '..\/middleware\/requireAuth.js';\n\nconst router = express.Router();\n\nrouter.get('\/', requireAuth, (req, res) => {\n  res.json({\n    message: 'Profilo protetto',\n    user: {\n      id: req.user.id,\n      email: req.user.email,\n      name: req.user.name\n    }\n  });\n});\n\n\/\/ Endpoint API protetta per dati sensibili\nrouter.get('\/api\/data', requireAuth, async (req, res) => {\n  try {\n    res.json({ data: 'Dati riservati', userId: req.user.id });\n  } catch (error) {\n    console.error('Errore profile\/api\/data:', error);\n    \/\/ Non esporre stack trace nelle risposte di errore\n    res.status(500).json({ error: 'Internal server error' });\n  }\n});\n\nexport default router;\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-8-verifica-dellid-token-oidc-con-jose\">Step 8: Verifica dell&#8217;ID Token OIDC con jose<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Quando il provider supporta OpenID Connect, ricevi un <code>id_token<\/code> oltre all&#8217;<code>access_token<\/code>. Questo JWT contiene le <em>claims<\/em> sull&#8217;identit\u00e0 dell&#8217;utente. La libreria <code>jose<\/code> v6 permette di verificarlo controllando firma, issuer, audience e scadenza. Crea il file <code>src\/auth\/oidc.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { createRemoteJWKSet, jwtVerify } from 'jose';\n\n\/\/ Chiavi pubbliche di Google per la verifica delle firme\nconst GOOGLE_JWKS = createRemoteJWKSet(\n  new URL('https:\/\/www.googleapis.com\/oauth2\/v3\/certs')\n);\n\nexport async function verifyGoogleIdToken(idToken) {\n  const { payload } = await jwtVerify(idToken, GOOGLE_JWKS, {\n    issuer: 'https:\/\/accounts.google.com',\n    audience: process.env.GOOGLE_CLIENT_ID\n    \/\/ jose verifica automaticamente la scadenza (exp claim)\n  });\n\n  \/\/ Verifica claims obbligatorie OIDC Core 1.0\n  if (!payload.sub) throw new Error('ID token privo di sub claim');\n  if (!payload.email_verified) throw new Error('Email non verificata dal provider');\n\n  return {\n    sub: payload.sub,\n    email: payload.email,\n    name: payload.name,\n    picture: payload.picture,\n    issuedAt: payload.iat,\n    expiresAt: payload.exp\n  };\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Due punti critici sulla verifica. Primo: specifica sempre l&#8217;<code>issuer<\/code> e l&#8217;<code>audience<\/code> nella chiamata <code>jwtVerify()<\/code>. Un token emesso per un&#8217;altra applicazione non deve essere accettato dalla tua. Secondo: la libreria <code>jose<\/code> v6 rifiuta automaticamente <code>alg: none<\/code> e gli algoritmi simmetrici su chiavi asimmetriche, eliminando la categoria di algoritm confusion attacks. Il pinning degli algoritmi, raccomandato dalla guida di Authgear 2026, \u00e8 gi\u00e0 integrato nel comportamento di default di jose quando usi JWKS remoti.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-9-implementa-pkce-manuale-per-provider-custom\">Step 9: Implementa PKCE Manuale per Provider Custom<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Se integri un provider personalizzato (Keycloak, Auth0 con piano gratuito, Okta) che non ha una Passport strategy pronta, implementa PKCE con il modulo crittografico nativo di Node.js. Il file <code>src\/auth\/pkce.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { randomBytes, createHash } from 'node:crypto';\n\n\/\/ Genera un code_verifier casuale (RFC 7636: 43-128 caratteri URL-safe)\nexport function generateCodeVerifier() {\n  return randomBytes(32).toString('base64url');\n}\n\n\/\/ Calcola il code_challenge (SHA-256 del verifier)\nexport function generateCodeChallenge(verifier) {\n  return createHash('sha256').update(verifier).digest('base64url');\n}\n\n\/\/ Costruisce l'URL di autorizzazione con PKCE e state anti-CSRF\nexport function buildAuthorizationUrl(config) {\n  const verifier = generateCodeVerifier();\n  const challenge = generateCodeChallenge(verifier);\n  const state = randomBytes(16).toString('hex');\n\n  const params = new URLSearchParams({\n    response_type: 'code',\n    client_id: config.clientId,\n    redirect_uri: config.redirectUri,\n    scope: config.scope,\n    state,\n    code_challenge: challenge,\n    code_challenge_method: 'S256'\n  });\n\n  return {\n    url: `${config.authorizationEndpoint}?${params}`,\n    verifier,  \/\/ Salvare in sessione lato server\n    state\n  };\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il <code>code_verifier<\/code> deve essere salvato nella sessione del server, non passato al browser in nessuna forma. Il browser vede solo il <code>code_challenge<\/code>, un hash SHA-256 irreversibile. Quando arriva il callback con il codice di autorizzazione, il server recupera il verifier dalla sessione e lo invia al token endpoint. Il formato <code>base64url<\/code>, senza padding e senza caratteri <code>+<\/code> o <code>\/<\/code>, \u00e8 obbligatorio per RFC 7636.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-10-gestione-sicura-dei-refresh-token\">Step 10: Gestione Sicura dei Refresh Token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Gli access token Google scadono dopo 3600 secondi (1 ora). Il refresh token permette di ottenerne uno nuovo senza chiedere all&#8217;utente di riautenticarsi. La <em>refresh token rotation<\/em> invalida il token usato ogni volta che viene scambiato, limitando la finestra di abuso se viene rubato. Crea il file <code>src\/auth\/tokenRefresh.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export async function refreshAccessToken(refreshToken) {\n  const response = await fetch('https:\/\/oauth2.googleapis.com\/token', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application\/x-www-form-urlencoded' },\n    body: new URLSearchParams({\n      client_id: process.env.GOOGLE_CLIENT_ID,\n      client_secret: process.env.GOOGLE_CLIENT_SECRET,\n      grant_type: 'refresh_token',\n      refresh_token: refreshToken\n    })\n  });\n\n  if (!response.ok) {\n    const error = await response.json();\n    \/\/ invalid_grant = token revocato o scaduto\n    if (error.error === 'invalid_grant') {\n      throw Object.assign(new Error('SESSION_EXPIRED'), { code: 'INVALID_GRANT' });\n    }\n    throw new Error(`Token refresh fallito: ${error.error}`);\n  }\n\n  const tokens = await response.json();\n  return {\n    accessToken: tokens.access_token,\n    expiresAt: Date.now() + (tokens.expires_in * 1000),\n    \/\/ Google non sempre emette un nuovo refresh_token\n    refreshToken: tokens.refresh_token || refreshToken\n  };\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dove salvare i token? Non nel database in chiaro. Le opzioni sicure nel 2026 sono: <strong>cifratura AES-256-GCM<\/strong> prima della persistenza, con la chiave master in un secrets manager (AWS Secrets Manager, HashiCorp Vault), oppure mantenerli solo in Redis cifrato con TTL. Non usare mai <code>localStorage<\/code> per i token: qualsiasi script XSS pu\u00f2 leggerlo. Il pattern BFF (Backend for Frontend) \u00e8 la soluzione ottimale per le SPA: il backend gestisce tutti i token e il browser interagisce solo tramite cookie <code>httpOnly<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-11-logout-sicuro-con-revoca-token\">Step 11: Logout Sicuro con Revoca Token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un logout parziale (solo lato client) lascia il refresh token valido anche dopo che l&#8217;utente ha cliccato &#8220;Esci&#8221;. Per un logout sicuro completo bisogna: revocare i token lato authorization server, chiamare <code>req.logout()<\/code>, distruggere la sessione Redis e cancellare il cookie. Aggiorna la route logout in <code>src\/routes\/auth.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>router.post('\/logout', async (req, res, next) => {\n  const accessToken = req.session?.accessToken;\n\n  try {\n    \/\/ 1. Revoca il token su Google\n    if (accessToken) {\n      await fetch(`https:\/\/oauth2.googleapis.com\/revoke?token=${encodeURIComponent(accessToken)}`, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application\/x-www-form-urlencoded' }\n      });\n    }\n\n    \/\/ 2. Logout Passport (rimuove req.user)\n    await new Promise((resolve, reject) => {\n      req.logout((err) => err ? reject(err) : resolve());\n    });\n\n    \/\/ 3. Distruggi la sessione Redis\n    await new Promise((resolve, reject) => {\n      req.session.destroy((err) => err ? reject(err) : resolve());\n    });\n\n    \/\/ 4. Cancella il cookie lato client\n    res.clearCookie('connect.sid', {\n      httpOnly: true,\n      secure: process.env.NODE_ENV === 'production',\n      sameSite: 'lax'\n    });\n\n    res.json({ success: true, message: 'Logout effettuato correttamente' });\n\n  } catch (error) {\n    console.error('Errore durante il logout:', error.message);\n    next(error);\n  }\n});\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-12-test-funzionale-e-checklist-di-sicurezza\">Step 12: Test Funzionale e Checklist di Sicurezza<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima del deploy, esegui questi test manuali nell&#8217;ordine indicato. Ogni passo verifica un aspetto distinto del flusso:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Avvia il server in modalit\u00e0 sviluppo\nnpm run dev\n\n# Test 1: verifica endpoint non autenticato\ncurl http:\/\/localhost:3000\/auth\/status\n# Atteso: {\"authenticated\":false,\"user\":null}\n\n# Test 2: verifica protezione route senza sessione\ncurl -X GET http:\/\/localhost:3000\/profile -H \"Accept: application\/json\"\n# Atteso: {\"error\":\"Authentication required\"}\n\n# Test 3: avvia il flusso OAuth2 (apri nel browser)\n# Naviga su: http:\/\/localhost:3000\/auth\/google\n# Accedi con un account Google di test\n\n# Test 4: verifica sessione attiva dopo login\ncurl -b \"connect.sid=COOKIE_VALUE\" http:\/\/localhost:3000\/auth\/status\n# Atteso: {\"authenticated\":true,\"user\":{\"id\":\"...\",\"email\":\"...\"}}\n\n# Test 5: verifica accesso alla route protetta\ncurl -b \"connect.sid=COOKIE_VALUE\" http:\/\/localhost:3000\/profile\n# Atteso: {\"message\":\"Profilo protetto\",\"user\":{...}}\n\n# Test 6: logout e verifica invalidazione\ncurl -X POST -b \"connect.sid=COOKIE_VALUE\" http:\/\/localhost:3000\/auth\/logout\ncurl -b \"connect.sid=COOKIE_VALUE\" http:\/\/localhost:3000\/auth\/status\n# Atteso dopo logout: {\"authenticated\":false,\"user\":null}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Usa Chrome DevTools (scheda Network, poi Application &gt; Cookies) per verificare i cookie: il cookie di sessione deve avere i flag <code>HttpOnly<\/code> e <code>SameSite: Lax<\/code>, e <code>Secure<\/code> in produzione su HTTPS. Se uno di questi manca, c&#8217;\u00e8 un problema nella configurazione di <code>express-session<\/code>. Per un test di sicurezza pi\u00f9 approfondito del flusso OAuth2, usa Burp Suite come proxy per intercettare e ispezionare ogni richiesta del flusso come mostrato nel nostro <a href=\"\/it\/burp-suite-tutorial\/\">tutorial Burp Suite<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"errori-comuni-e-come-evitarli\">Errori Comuni e Come Evitarli<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Questi sono gli 8 errori pi\u00f9 frequenti nelle implementazioni OAuth2 in Node.js, raccolti da revisioni di codice su progetti in produzione:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Salvare i token in localStorage.<\/strong> JavaScript in qualsiasi script della pagina (inclusi script di terze parti) pu\u00f2 leggere <code>localStorage<\/code>. Un XSS anche minimo espone tutti i token. Usa sempre cookie <code>httpOnly<\/code>.<\/li>\n<li><strong>Non validare il parametro <code>state<\/code>.<\/strong> Il parametro <code>state<\/code> previene i CSRF sull&#8217;endpoint di callback. Se non lo confronti con quello salvato in sessione, un attaccante pu\u00f2 innescare un login con il proprio codice di autorizzazione e dirottare la sessione della vittima (login CSRF).<\/li>\n<li><strong>Non specificare l&#8217;algoritmo nella verifica JWT.<\/strong> L&#8217;attacco &#8220;alg:none&#8221; e la confusion attack tra HS256\/RS256 sono documentati da anni. Usa librerie che fanno il pinning automaticamente, come <code>jose<\/code> v6.<\/li>\n<li><strong>Usare il session store in memoria in produzione.<\/strong> <code>MemoryStore<\/code> di express-session non \u00e8 adatto alla produzione: perde dati al riavvio e produce memory leak sotto carico. Usa sempre Redis o un database persistente.<\/li>\n<li><strong>Non rigenerare l&#8217;ID sessione dopo il login.<\/strong> La session fixation \u00e8 un attacco noto. Chiama sempre <code>req.session.regenerate()<\/code> dopo l&#8217;autenticazione riuscita.<\/li>\n<li><strong>Richiedere scope eccessivi.<\/strong> Se la tua app ha bisogno solo dell&#8217;email, richiedi solo <code>openid email<\/code>. Scope aggiuntivi come <code>gmail.readonly<\/code> senza necessit\u00e0 espongono l&#8217;app a rischi maggiori e riducono i tassi di conversione.<\/li>\n<li><strong>Non gestire la revoca del refresh token.<\/strong> Un utente pu\u00f2 revocare l&#8217;accesso dalla sua Google Account. Il server deve gestire <code>invalid_grant<\/code> e forzare un nuovo login invece di andare in loop di retry.<\/li>\n<li><strong>Logare il valore completo dei token.<\/strong> Un token di accesso nei log \u00e8 un token esposto. Logga solo metadata: presenza\/assenza, scadenza, user ID associato. Mai il valore del token.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"risoluzione-dei-problemi-8-scenari-frequenti\">Risoluzione dei Problemi: 8 Scenari Frequenti<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr><th>Errore \/ Sintomo<\/th><th>Causa pi\u00f9 probabile<\/th><th>Soluzione<\/th><\/tr><\/thead>\n<tbody>\n<tr><td><code>redirect_uri_mismatch<\/code><\/td><td>URI nella richiesta diverso da quello registrato su Google Cloud Console<\/td><td>Verifica che BASE_URL in .env corrisponda esattamente all&#8217;URI registrato, incluso protocollo e porta<\/td><\/tr>\n<tr><td><code>invalid_client<\/code><\/td><td>Client ID o Client Secret errato o scaduto<\/td><td>Rigenera le credenziali su Google Cloud Console e aggiorna il file .env<\/td><\/tr>\n<tr><td><code>invalid_grant<\/code> al refresh<\/td><td>Refresh token revocato dall&#8217;utente o scaduto<\/td><td>Cattura il codice <code>INVALID_GRANT<\/code>, cancella i token salvati e reindirizza al login<\/td><\/tr>\n<tr><td>Sessione persa al riavvio server<\/td><td>MemoryStore in uso invece di Redis<\/td><td>Configura <code>connect-redis<\/code> come store nella configurazione della sessione<\/td><\/tr>\n<tr><td>Loop di redirect al login<\/td><td>Cookie non impostato per problema SameSite o CORS<\/td><td>In sviluppo: usa <code>sameSite: 'lax'<\/code>; verifica che il callback URL non venga da un dominio diverso<\/td><\/tr>\n<tr><td><code>passport.authenticate<\/code> non esegue il callback<\/td><td>Mancata registrazione di <code>serializeUser<\/code> o <code>deserializeUser<\/code><\/td><td>Controlla che entrambi siano registrati in <code>passport.js<\/code> prima dell&#8217;importazione delle route<\/td><\/tr>\n<tr><td>ID token con firma invalida<\/td><td>Chiavi JWKS scadute in cache o algoritmo non supportato<\/td><td>Usa <code>createRemoteJWKSet<\/code> di jose per aggiornamento automatico; verifica che il provider usi RS256<\/td><\/tr>\n<tr><td><code>Cannot read properties of undefined (reading 'emails')<\/code><\/td><td>Profilo Google senza email (account senza verifica)<\/td><td>Aggiungi guardia: <code>if (!profile.emails?.length) return done(new Error('No email'))<\/code><\/td><\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"debug-del-flusso-oauth2-passo-per-passo\">Debug del Flusso OAuth2 Passo per Passo<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Se non riesci a capire dove il flusso si rompe, abilita il debug di Passport con la variabile d&#8217;ambiente <code>DEBUG=passport:*<\/code> prima di avviare il server:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DEBUG=passport:* node src\/app.js\n\n# Output esempio:\n# passport:authenticate google +0ms\n# passport:strategy:google attempting to authenticate +1ms\n# passport:strategy:google redirecting to https:\/\/accounts.google.com\/o\/oauth2\/...\n\n# Per ispezionare la sessione Redis:\nredis-cli\n127.0.0.1:6379> KEYS sess:*\n127.0.0.1:6379> GET sess:abc123\n# Mostra il JSON della sessione (incluso lo stato dell'autenticazione)\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"consigli-avanzati-per-la-produzione\">Consigli Avanzati per la Produzione<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Una volta che il flusso base funziona, questi accorgimenti portano l&#8217;implementazione al livello enterprise:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"rate-limiting-sugli-endpoint-auth\">Rate Limiting sugli Endpoint Auth<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Gli endpoint OAuth2 sono bersagli di abuso. Applica rate limiting specifico sul path <code>\/auth\/*<\/code>, pi\u00f9 restrittivo di quello globale dell&#8217;applicazione. Una configurazione ragionevole: massimo 10 richieste per IP ogni 15 minuti. L&#8217;articolo <a href=\"\/it\/rate-limiting-nodejs\/\">Rate Limiting in Node.js<\/a> mostra l&#8217;implementazione completa con Redis e sliding window. Aggiungi anche logging degli IP che superano il limite per identificare pattern di bot.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"validazione-con-zod-prima-del-database\">Validazione con Zod prima del Database<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di passare i dati del profilo OAuth2 al database, validali con Zod. Un profilo Google pu\u00f2 contenere campi inaspettati o valori fuori range. La validazione con schema previene mass assignment e injection nei layer ORM:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { z } from 'zod';\n\nconst UserProfileSchema = z.object({\n  id: z.string().min(1).max(255),\n  email: z.string().email().max(255),\n  name: z.string().max(255).optional(),\n  picture: z.string().url().optional()\n});\n\n\/\/ Nel callback Passport:\nconst parsed = UserProfileSchema.safeParse(user);\nif (!parsed.success) {\n  return done(new Error('Profilo OAuth2 non valido'));\n}\nconst safeUser = parsed.data;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Multi-provider e account linking.<\/strong> Se permetti login sia con Google che con GitHub, due account possono avere la stessa email. Decidi in anticipo se trattarli come la stessa identit\u00e0 (link account per email) o identit\u00e0 separate. L&#8217;approccio pi\u00f9 sicuro \u00e8 richiedere conferma esplicita all&#8217;utente prima di collegare due account con la stessa email, per prevenire account takeover tramite provider compromise.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Logging strutturato degli eventi di sicurezza.<\/strong> Logga ogni evento di autenticazione, login riuscito, login fallito, logout, refresh token, con user ID, IP sorgente e timestamp. Non loggare mai il valore dei token. Usa un formato JSON strutturato per poterli analizzare con SIEM (Splunk, Elastic SIEM). Un login riuscito da un&#8217;IP mai vista prima merita un alert automatico.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"confronto-storage-token-per-le-spa-nel-2026\">Confronto: Storage Token per le SPA nel 2026<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead><tr><th>Metodo di Storage<\/th><th>Resistenza XSS<\/th><th>Resistenza CSRF<\/th><th>Scalabilit\u00e0<\/th><th>Raccomandazione 2026<\/th><\/tr><\/thead>\n<tbody>\n<tr><td>localStorage<\/td><td>Nulla<\/td><td>Alta<\/td><td>Alta<\/td><td>Non usare per token<\/td><\/tr>\n<tr><td>sessionStorage<\/td><td>Nulla<\/td><td>Alta<\/td><td>Alta<\/td><td>Non usare per token<\/td><\/tr>\n<tr><td>Cookie httpOnly lax<\/td><td>Alta<\/td><td>Media<\/td><td>Alta<\/td><td>Buono per web app<\/td><\/tr>\n<tr><td>Cookie httpOnly + CSRF token doppio<\/td><td>Alta<\/td><td>Alta<\/td><td>Alta<\/td><td>Ottimale per web app<\/td><\/tr>\n<tr><td>Memoria RAM (SPA in-memory)<\/td><td>Alta<\/td><td>Alta<\/td><td>Bassa (perde al refresh)<\/td><td>Solo per SPA con silent refresh<\/td><\/tr>\n<tr><td>BFF con cookie httpOnly<\/td><td>Alta<\/td><td>Alta<\/td><td>Alta<\/td><td>Ottimale per SPA enterprise<\/td><\/tr>\n<\/tbody>\n<\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Il pattern <strong>BFF (Backend for Frontend)<\/strong> \u00e8 la soluzione pi\u00f9 robusta per le SPA nel 2026: il backend Node.js gestisce tutta la logica OAuth2 e i token non raggiungono mai il browser in forma leggibile. La SPA interagisce solo con il BFF tramite cookie di sessione <code>httpOnly<\/code>. Questa architettura elimina l&#8217;intera categoria di rischi XSS sui token e semplifica la gestione del refresh, ma aggiunge un componente server-side che le SPA pure evitano.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"copertura-correlata\">Copertura Correlata<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"articoli-sul-tema-sicurezza-in-node-js\">Articoli sul Tema Sicurezza in Node.js<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/it\/autenticazione-jwt-nodejs\/\">JWT Authentication in Node.js: 10 Step<\/a> per la gestione dei token JSON Web Token senza OAuth2<\/li>\n<li><a href=\"\/it\/protezione-csrf-nodejs\/\">CSRF Protection in Node.js: 12 Step<\/a> per proteggere i form e le API da attacchi cross-site<\/li>\n<li><a href=\"\/it\/rate-limiting-nodejs\/\">Rate Limiting in Node.js: 12 Step<\/a> per proteggere gli endpoint OAuth2 da abusi e brute force<\/li>\n<li><a href=\"\/it\/owasp-top-10-nodejs-2026\/\">OWASP Top 10 2025 in Node.js: 10 Vulnerabilit\u00e0, 12 Difese<\/a> per una visione d&#8217;insieme delle vulnerabilit\u00e0 applicative<\/li>\n<li><a href=\"\/it\/hashing-password-bcrypt-nodejs\/\">bcrypt Password Hashing in Node.js<\/a> per le autenticazioni locali da affiancare a OAuth2<\/li>\n<li><a href=\"\/it\/burp-suite-tutorial\/\">Burp Suite: Test di Sicurezza Web in 12 Step<\/a> per intercettare e testare il flusso OAuth2 in dettaglio<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq-su-oauth-2-0-e-openid-connect-in-node-js\">FAQ su OAuth 2.0 e OpenID Connect in Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"qual-e-la-differenza-pratica-tra-oauth2-e-openid-connect\">Qual \u00e8 la differenza pratica tra OAuth2 e OpenID Connect?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 \u00e8 un framework di <em>autorizzazione<\/em>: definisce come un&#8217;applicazione ottiene accesso a risorse per conto di un utente. OpenID Connect \u00e8 un protocollo di <em>autenticazione<\/em> costruito sopra OAuth 2.0: aggiunge un ID token JWT con le informazioni sull&#8217;identit\u00e0 dell&#8217;utente. In pratica, usi OAuth2 per accedere alle API Google (Gmail, Drive) e OIDC per sapere chi \u00e8 l&#8217;utente autenticato.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"pkce-e-obbligatorio-per-le-web-app-server-side-nel-2026\">PKCE \u00e8 obbligatorio per le web app server-side nel 2026?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">RFC 9700 (OAuth 2.0 Security Best Current Practice, 2025) raccomanda PKCE per tutti i client, compresi quelli confidenziali server-side. Non \u00e8 tecnicamente obbligatorio se il client ha un Client Secret e il canale \u00e8 sicuro, ma aggiunge un secondo livello di difesa senza costi pratici. Google, GitHub e la maggior parte dei provider moderni supportano PKCE per tutti i tipi di client.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quanto-devono-durare-gli-access-token\">Quanto devono durare gli access token?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">La best practice del 2026 \u00e8 15 minuti per gli access token con refresh token rotation. Google emette access token con scadenza a 3600 secondi (1 ora), accettabile per la maggior parte delle applicazioni. Token pi\u00f9 corti riducono la finestra di esposizione se rubati, ma aumentano il numero di chiamate al token endpoint. Usa refresh token rotation per invalidare ogni token usato.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"posso-implementare-oauth2-in-node-js-senza-passport-js\">Posso implementare OAuth2 in Node.js senza Passport.js?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">S\u00ec. Puoi implementare il flusso Authorization Code manualmente con <code>node:crypto<\/code> per PKCE e <code>fetch<\/code> nativo (disponibile senza flag da Node.js 22+) per le chiamate HTTP. Il pacchetto <code>openid-client<\/code> di Panva (disponibile su <a href=\"https:\/\/github.com\/panva\/node-oidc-provider\" target=\"_blank\" rel=\"noopener\">GitHub<\/a>) \u00e8 un&#8217;alternativa moderna pi\u00f9 vicina agli standard OIDC. Passport.js rimane la scelta pi\u00f9 diffusa per la vastit\u00e0 del suo ecosistema di strategy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"come-gestisco-lerrore-invalid_grant-in-produzione\">Come gestisco l&#8217;errore &#8220;invalid_grant&#8221; in produzione?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;errore <code>invalid_grant<\/code> significa che il refresh token non \u00e8 pi\u00f9 valido: l&#8217;utente ha revocato l&#8217;accesso, il token \u00e8 scaduto dopo 6 mesi di inattivit\u00e0, oppure l&#8217;account ha cambiato password. La risposta corretta \u00e8: intercetta il codice di errore, cancella i token salvati per quell&#8217;utente nel database o in Redis, e reindirizza al login al prossimo accesso. Non fare loop di retry su <code>invalid_grant<\/code>: \u00e8 definitivo.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"come-integro-oauth2-con-il-sistema-di-permessi-rbac-esistente\">Come integro OAuth2 con il sistema di permessi RBAC esistente?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth2 identifica l&#8217;utente; RBAC (Role-Based Access Control) gestisce cosa pu\u00f2 fare. Nel callback Passport, dopo aver trovato o creato l&#8217;utente nel database, carica i suoi ruoli e includili nel profilo serializzato. Il middleware <code>requireAuth<\/code> si estende con un <code>requireRole('admin')<\/code> che legge <code>req.user.roles<\/code>. Non basarti mai sui ruoli dall&#8217;ID token OIDC: quelli vengono da Google, non dal tuo sistema interno.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"come-testo-limplementazione-oauth2-senza-dipendere-da-google\">Come testo l&#8217;implementazione OAuth2 senza dipendere da Google?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Usa un authorization server locale per i test automatizzati. Keycloak (disponibile come immagine Docker ufficiale) \u00e8 l&#8217;alternativa enterprise pi\u00f9 comune per gli ambienti di staging: avvia in pochi secondi con <code>docker run -p 8080:8080 quay.io\/keycloak\/keycloak:latest start-dev<\/code>. Il pacchetto <code>node-oidc-provider<\/code> \u00e8 pi\u00f9 leggero e adatto ai test di integrazione CI\/CD. Entrambi permettono di testare l&#8217;intero flusso OAuth2 senza rate limit o dipendenze esterne.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"cosa-sono-gli-scope-oidc-standard-e-quali-devo-richiedere\">Cosa sono gli scope OIDC standard e quali devo richiedere?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Lo scope <code>openid<\/code> \u00e8 obbligatorio per ottenere un ID token OIDC. <code>profile<\/code> aggiunge nome, foto e dati del profilo. <code>email<\/code> aggiunge l&#8217;indirizzo email e il flag <code>email_verified<\/code>. <code>offline_access<\/code> richiede il refresh token (Google usa invece <code>accessType: 'offline'<\/code>). Richiedi solo gli scope che usi effettivamente: ogni scope aggiuntivo richiede il consenso dell&#8217;utente e aumenta la superficie di rischio in caso di token compromise.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Ogni volta che un utente clicca su &#8220;Accedi con Google&#8221; o &#8220;Accedi con GitHub&#8221;, entra in azione OAuth 2.0. Il protocollo gestisce miliardi di autenticazioni ogni giorno, ma implementarlo correttamente\u2026<\/p>\n","protected":false},"author":8,"featured_media":197,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-196","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-security"],"_links":{"self":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/196","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/users\/8"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/comments?post=196"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/196\/revisions"}],"predecessor-version":[{"id":198,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/196\/revisions\/198"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media\/197"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media?parent=196"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/categories?post=196"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/tags?post=196"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}