{"id":271,"date":"2026-06-20T16:30:59","date_gmt":"2026-06-20T16:30:59","guid":{"rendered":"https:\/\/shattered.io\/it\/2026\/06\/20\/oauth2-pkce-nodejs\/"},"modified":"2026-06-20T16:32:20","modified_gmt":"2026-06-20T16:32:20","slug":"oauth2-pkce-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/it\/2026\/06\/20\/oauth2-pkce-nodejs\/","title":{"rendered":"OAuth 2.0 e PKCE in Node.js: 12 Step in 30 Minuti [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">OAuth 2.0 \u00e8 il protocollo di autorizzazione alla base di ogni &#8220;Accedi con Google&#8221; o &#8220;Login con Microsoft&#8221; che usi ogni giorno. Nella sua implementazione classica con il flusso Authorization Code, presenta per\u00f2 una vulnerabilit\u00e0 critica: il codice di autorizzazione pu\u00f2 essere intercettato o iniettato da un attaccante prima che arrivi al server legittimo. La soluzione si chiama <strong>PKCE<\/strong> (Proof Key for Code Exchange, pronunciato &#8220;pixie&#8221;), definita nell&#8217;RFC 7636 e dal 2025 raccomandata da RFC 9700 per tutti i client OAuth 2.0, inclusi quelli confidenziali che gi\u00e0 usano un client secret.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Questa guida mostra come implementare OAuth 2.0 con PKCE in Node.js usando <code>openid-client<\/code> ed <code>express<\/code>, partendo dalla configurazione del provider fino alla gestione sicura dei token e al logout corretto. Al termine avrai un&#8217;applicazione funzionante pronta per la produzione, conforme alle raccomandazioni di RFC 9700 e allineata ai requisiti di sicurezza dell&#8217;identit\u00e0 previsti dalla Direttiva NIS2, recepita in Italia con il D.Lgs. 138\/2024.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"cose-oauth-2-0-e-perche-il-flusso-authorization-code-da-solo-non-basta\">Cos&#8217;\u00e8 OAuth 2.0 e Perch\u00e9 il Flusso Authorization Code da Solo Non Basta<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth 2.0 \u00e8 un framework di autorizzazione definito nell&#8217;RFC 6749 che consente a un&#8217;applicazione di ottenere accesso limitato alle risorse di un utente su un server di terze parti, senza mai ricevere le credenziali dell&#8217;utente. Quando accedi a un&#8217;app con &#8220;Accedi con Google&#8221; o &#8220;Login con GitHub&#8221;, stai usando OAuth 2.0. Il protocollo \u00e8 costruito attorno al concetto di token: l&#8217;autorizzazione viene espressa come un access token con scadenza breve, non come username e password persistenti.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il flusso pi\u00f9 sicuro per le applicazioni web \u00e8 l&#8217;Authorization Code Flow: il server di autorizzazione restituisce all&#8217;applicazione un codice monouso, che viene poi scambiato con access token e refresh token sul canale back-channel (da server a server), invisibile al browser. Questo evita che i token appaiano nell&#8217;URL o nella history del browser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il problema sorge su dispositivi mobili e in scenari dove il codice di autorizzazione pu\u00f2 essere intercettato prima di raggiungere il client legittimo. Su Android, per esempio, pi\u00f9 app possono registrare lo stesso URL scheme personalizzato: un&#8217;app malevola pu\u00f2 catturare il codice destinato all&#8217;app originale. Sui desktop, un malware locale con accesso alle comunicazioni di rete pu\u00f2 fare lo stesso. Il risultato in entrambi i casi: l&#8217;attaccante scambia il codice intercettato per ottenere i token e accedere alle risorse dell&#8217;utente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Anche nelle applicazioni server-side, un attacco di <strong>authorization code injection<\/strong> consente a un avversario di iniettare nel callback di un&#8217;applicazione vittima un codice OAuth ottenuto altrove. RFC 9700 identifica questo come uno dei vettori principali contro OAuth 2.0 e ne raccomanda la mitigazione con PKCE obbligatoriamente per tutti i tipi di client.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"pkce-rfc-7636-il-meccanismo-di-protezione-spiegato-passo-per-passo\">PKCE (RFC 7636): il Meccanismo di Protezione Spiegato Passo per Passo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">PKCE aggiunge un livello crittografico al flusso Authorization Code senza richiedere un client secret aggiuntivo. Il principio \u00e8 semplice ma efficace: prima di avviare il flusso, il client genera un valore segreto casuale chiamato <code>code_verifier<\/code> e ne calcola l&#8217;hash SHA-256, chiamato <code>code_challenge<\/code>. Solo l&#8217;hash viene inviato al server di autorizzazione all&#8217;inizio del flusso. Quando il client scambia il codice con i token, deve presentare il <code>code_verifier<\/code> originale. Il server verifica che l&#8217;hash del <code>code_verifier<\/code> corrisponda al <code>code_challenge<\/code> ricevuto in precedenza.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un intercettatore che cattura il codice di autorizzazione non pu\u00f2 completare lo scambio perch\u00e9 non conosce il <code>code_verifier<\/code>: questo valore non viene mai trasmesso sul canale esposto, solo il suo hash. Il server di autorizzazione rifiuta qualsiasi tentativo di scambio senza il <code>code_verifier<\/code> corretto, rendendo il codice intercettato inutilizzabile.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Lo stesso meccanismo blocca l&#8217;authorization code injection: il codice iniettato da un attaccante era stato originariamente associato a un diverso <code>code_challenge<\/code>. Il <code>code_verifier<\/code> della sessione vittima non corrisponder\u00e0 mai all&#8217;hash del codice iniettato, e il server rifiuter\u00e0 la richiesta.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Parametro PKCE<\/th><th>Formato e Vincoli<\/th><th>Note di sicurezza<\/th><\/tr><\/thead><tbody><tr><td><code>code_verifier<\/code><\/td><td>Stringa casuale 43-128 caratteri (A-Z, a-z, 0-9, -, ., _, ~)<\/td><td>Generato con crypto sicuro, mai trasmesso nella prima richiesta<\/td><\/tr><tr><td><code>code_challenge<\/code><\/td><td>BASE64URL(SHA256(ASCII(code_verifier)))<\/td><td>Inviato nella richiesta di autorizzazione<\/td><\/tr><tr><td><code>code_challenge_method<\/code><\/td><td><code>S256<\/code> (obbligatorio)<\/td><td>Non usare mai <code>plain<\/code><\/td><\/tr><tr><td><code>state<\/code><\/td><td>Stringa opaca casuale (consigliata \u2265 16 byte)<\/td><td>Protezione CSRF, complementare a PKCE<\/td><\/tr><tr><td><code>redirect_uri<\/code><\/td><td>URL esatto registrato sul provider<\/td><td>Matching esatto, zero wildcard<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">RFC 9700, pubblicato come standard IETF nel 2024 in aggiornamento di RFC 6819, raccomanda PKCE per tutti i client OAuth 2.0, compresi i client confidenziali che gi\u00e0 usano un client secret. PKCE non sostituisce il client secret: i due meccanismi sono complementari. Il client secret autentica il client (dimostra al server che la richiesta proviene dall&#8217;applicazione registrata), mentre PKCE protegge il flusso dall&#8217;intercettazione e dall&#8217;iniezione del codice. L&#8217;imminente OAuth 2.1, ancora in stato di draft IETF a giugno 2026, render\u00e0 PKCE obbligatorio per tutti i flussi Authorization Code.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"4-attacchi-che-pkce-blocca-guida-ai-vettori-oauth-2-0\">4 Attacchi che PKCE Blocca: Guida ai Vettori OAuth 2.0<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di scrivere codice, \u00e8 utile capire concretamente cosa PKCE protegge. Gli attacchi contro OAuth 2.0 documentati in RFC 9700 e RFC 6819 si dividono in quattro categorie principali, con impatto diverso a seconda del tipo di client e dell&#8217;ambiente di esecuzione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;<strong>Authorization Code Interception<\/strong> \u00e8 il vettore originale per cui PKCE \u00e8 stato progettato nel 2015 (RFC 7636). Su Android, pi\u00f9 app possono registrare lo stesso custom URL scheme. Quando il server di autorizzazione reindirizza con il codice verso il redirect URI, il sistema operativo potrebbe consegnarlo all&#8217;app sbagliata. Con PKCE, il codice intercettato \u00e8 inutilizzabile senza il <code>code_verifier<\/code> generato dalla sessione originale.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;<strong>Authorization Code Injection<\/strong> \u00e8 pi\u00f9 sofisticato. Un attaccante ottiene un codice di autorizzazione valido (da un suo account su un altro provider, o da una sessione separata) e lo inietta nel flusso di callback di un&#8217;applicazione bersaglio sostituendo il parametro <code>code<\/code> nell&#8217;URL di callback. Senza PKCE, il server accetta il codice e rilascia token associati all&#8217;account dell&#8217;attaccante all&#8217;interno della sessione della vittima. Con PKCE, il server verifica che il <code>code_verifier<\/code> della sessione corrente corrisponda al <code>code_challenge<\/code> associato al codice iniettato, e rigetta la richiesta.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il <strong>CSRF su OAuth<\/strong> \u00e8 mitigato principalmente dal parametro <code>state<\/code>, non da PKCE. Un attaccante costruisce un URL di autorizzazione OAuth completo e induce la vittima a cliccarlo. Senza la verifica dello <code>state<\/code>, la vittima completa il flusso e si ritrova loggata con l&#8217;account dell&#8217;attaccante nell&#8217;applicazione, che ora ha accesso ai dati inseriti dalla vittima. La verifica dello <code>state<\/code> nel callback \u00e8 obbligatoria e complementare a PKCE.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;<strong>Open Redirect Attack<\/strong> sfrutta provider che accettano <code>redirect_uri<\/code> con wildcard o matching non rigoroso. Un attaccante costruisce un URL di autorizzazione con un <code>redirect_uri<\/code> controllato da lui, che il provider accetta erroneamente. Dopo l&#8217;autenticazione, il codice viene reindirizzato verso il server dell&#8217;attaccante. La difesa \u00e8 la registrazione esatta del <code>redirect_uri<\/code> sul provider e il suo matching rigoroso.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequisiti-versioni-strumenti-e-provider-oauth\">Prerequisiti: Versioni, Strumenti e Provider OAuth<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Per seguire questa guida hai bisogno di Node.js 18 LTS o superiore. La versione 18 \u00e8 il requisito minimo di <code>openid-client<\/code> per il supporto nativo ai moduli ES e all&#8217;API Web Crypto standard. Verifica la versione installata con <code>node --version<\/code>; se hai una versione precedente, aggiorna con il Node Version Manager (nvm) usando <code>nvm install 20 &amp;&amp; nvm use 20<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Serve anche un provider OAuth 2.0 conforme a OpenID Connect Discovery che supporti PKCE. Le opzioni pi\u00f9 comuni per lo sviluppo sono tre. Okta offre un piano gratuito con 100 monthly active users e supporta PKCE nativamente. Auth0 ha un piano gratuito con 7.500 MAU. Google OAuth \u00e8 disponibile tramite Google Cloud Console senza limiti di MAU per i propri utenti, ma con alcune limitazioni sugli scope. Per ambienti completamente locali, <code>node-oidc-provider<\/code> implementa un authorization server completo in Node.js.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Dipendenza<\/th><th>Versione<\/th><th>Scopo<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>\u2265 18.0.0 (consigliato 20 LTS)<\/td><td>Runtime<\/td><\/tr><tr><td>express<\/td><td>4.19.2<\/td><td>Framework HTTP<\/td><\/tr><tr><td>express-session<\/td><td>1.18.0<\/td><td>Gestione sessione server-side<\/td><\/tr><tr><td>openid-client<\/td><td>5.6.5<\/td><td>Client OIDC\/OAuth 2.0 con supporto PKCE nativo<\/td><\/tr><tr><td>dotenv<\/td><td>16.4.5<\/td><td>Caricamento variabili d&#8217;ambiente<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Questa guida usa <code>openid-client<\/code> versione 5.x perch\u00e9 \u00e8 la versione stabile e ampiamente adottata. La versione 6.x ha introdotto breaking change nell&#8217;API: se stai usando la v6, l&#8217;API di discovery usa <code>client.discovery()<\/code> invece di <code>Issuer.discover()<\/code>. I concetti e il flusso PKCE rimangono identici in entrambe le versioni.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-configurazione-del-provider-oauth-okta-o-google\">Step 1: Configurazione del Provider OAuth (Okta o Google)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di scrivere codice, configura un&#8217;applicazione sul tuo provider OAuth. I passaggi variano leggermente tra i provider, ma i dati da raccogliere sono sempre gli stessi: Client ID, Client Secret (per i client confidenziali) e Issuer URL.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Su <strong>Okta<\/strong>: accedi alla Developer Console, vai su Applications, crea una nuova App Integration, seleziona OIDC e poi Web Application. Nella sezione Grant Types, assicurati che Authorization Code sia abilitato. Aggiungi <code>http:\/\/localhost:3000\/authorization-code\/callback<\/code> come Sign-in redirect URI. Annota Client ID, Client Secret e l&#8217;Issuer URL nella forma <code>https:\/\/your-org.okta.com\/oauth2\/default<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Su <strong>Google OAuth<\/strong>: apri la Google Cloud Console, crea un progetto, vai su APIs &amp; Services, poi Credentials. Crea un OAuth 2.0 Client ID di tipo Web application. Aggiungi <code>http:\/\/localhost:3000\/authorization-code\/callback<\/code> agli URI di reindirizzamento autorizzati. L&#8217;Issuer URL per Google \u00e8 <code>https:\/\/accounts.google.com<\/code>. Google non fornisce un client secret separato per i client web: usa invece il Client ID e il Client Secret che compaiono nella pagina delle credenziali.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Su entrambi i provider, puoi verificare il documento di discovery OpenID Connect aprendo l&#8217;URL <code>{issuer}\/.well-known\/openid-configuration<\/code> nel browser. Cerca il campo <code>code_challenge_methods_supported<\/code>: deve contenere <code>\"S256\"<\/code> per confermare che PKCE \u00e8 supportato.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-2-struttura-del-progetto-e-installazione-delle-dipendenze\">Step 2: Struttura del Progetto e Installazione delle Dipendenze<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crea una nuova directory e inizializza il progetto Node.js. Usiamo i moduli ES nativi perch\u00e9 <code>openid-client<\/code> v5 li richiede e perch\u00e9 l&#8217;ecosistema Node.js moderno li adotta come standard.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir oauth2-pkce-nodejs && cd oauth2-pkce-nodejs\nnpm init -y\nnpm install express@4.19.2 express-session@1.18.0 openid-client@5.6.5 dotenv@16.4.5<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Modifica <code>package.json<\/code> per aggiungere il tipo modulo e gli script di avvio:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"name\": \"oauth2-pkce-nodejs\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"node index.js\",\n    \"dev\": \"node --env-file=.env index.js\"\n  },\n  \"dependencies\": {\n    \"dotenv\": \"16.4.5\",\n    \"express\": \"4.19.2\",\n    \"express-session\": \"1.18.0\",\n    \"openid-client\": \"5.6.5\"\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La struttura finale del progetto prevede tre file principali: <code>index.js<\/code> (server Express), <code>auth.js<\/code> (logica OAuth e PKCE) e <code>.env<\/code> (credenziali del provider, mai incluso nel repository Git).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-variabili-dambiente-e-configurazione-di-base\">Step 3: Variabili d&#8217;Ambiente e Configurazione di Base<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crea il file <code>.env<\/code> con le credenziali del tuo provider. Aggiungi immediatamente <code>.env<\/code> al <code>.gitignore<\/code>: esporre le credenziali OAuth su un repository pubblico \u00e8 uno degli errori pi\u00f9 comuni e pi\u00f9 costosi, con codici di autorizzazione e token che possono essere usati in pochi secondi dai bot che scansionano GitHub.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env - NON committare questo file\nOAUTH_ISSUER=https:\/\/your-org.okta.com\/oauth2\/default\nOAUTH_CLIENT_ID=0oaxxxxxxxxxxxxxxxxx\nOAUTH_CLIENT_SECRET=il-tuo-client-secret\nAPP_BASE_URL=http:\/\/localhost:3000\nPOST_LOGOUT_URL=http:\/\/localhost:3000\/logout-completato\nSESSION_SECRET=sostituisci-con-stringa-casuale-di-almeno-32-caratteri\n\n# Aggiungere subito al .gitignore:\n# echo \".env\" >> .gitignore && echo \"node_modules\/\" >> .gitignore<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il valore <code>SESSION_SECRET<\/code> deve essere una stringa crittograficamente casuale di almeno 32 caratteri. Generalo con:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node -e \"console.log(require('crypto').randomBytes(32).toString('hex'))\"\n# Output esempio: a3f8c1e9d4b7f2a6e8c3d9b1f4a7e2c5d8b3f6a9e1c4d7b2f5a8e3c6d9b4f7a<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In produzione, gestisci i secret tramite un secrets manager dedicato (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, Google Secret Manager) e non attraverso variabili d&#8217;ambiente hardcoded. Non riutilizzare mai lo stesso <code>SESSION_SECRET<\/code> tra ambienti diversi (sviluppo, staging, produzione).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-4-e-5-generazione-di-code_verifier-e-code_challenge-e-costruzione-dellurl\">Step 4 e 5: Generazione di code_verifier e code_challenge, e Costruzione dell&#8217;URL<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crea il file <code>auth.js<\/code> che gestisce tutta la logica OAuth. Questo file esporta tre funzioni principali: <code>login<\/code> (avvia il flusso), <code>handleCallback<\/code> (gestisce il redirect del provider) e <code>logout<\/code>. Inizia con la configurazione del client OIDC e la funzione di login.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ auth.js\nimport { Issuer, generators } from 'openid-client';\nimport 'dotenv\/config';\n\n\/\/ Cache del client OIDC: la discovery viene eseguita una volta sola\nlet _client = null;\n\nasync function getClient() {\n  if (_client) return _client;\n  const issuer = await Issuer.discover(process.env.OAUTH_ISSUER);\n  _client = new issuer.Client({\n    client_id: process.env.OAUTH_CLIENT_ID,\n    client_secret: process.env.OAUTH_CLIENT_SECRET,\n    redirect_uris: [`${process.env.APP_BASE_URL}\/authorization-code\/callback`],\n    response_types: ['code'],\n  });\n  return _client;\n}\n\n\/\/ Step 4: Genera code_verifier e code_challenge (PKCE)\n\/\/ Step 5: Costruisce l'URL di autorizzazione e reindirizza\nexport async function login(req, res, next) {\n  try {\n    const client = await getClient();\n\n    const code_verifier = generators.codeVerifier();        \/\/ 43-128 caratteri casuali (crypto.randomBytes)\n    const code_challenge = generators.codeChallenge(code_verifier); \/\/ BASE64URL(SHA256(code_verifier))\n    const state = generators.state();                        \/\/ Protezione CSRF\n\n    \/\/ Salva in sessione server-side (mai nel cookie del client)\n    req.session.pkce = { code_verifier, state };\n    await new Promise((resolve, reject) =>\n      req.session.save(err => err ? reject(err) : resolve())\n    );\n\n    const authUrl = client.authorizationUrl({\n      scope: 'openid profile email offline_access',\n      state,\n      code_challenge,\n      code_challenge_method: 'S256',         \/\/ SHA-256 obbligatorio\n      redirect_uri: `${process.env.APP_BASE_URL}\/authorization-code\/callback`,\n    });\n\n    res.redirect(authUrl);\n  } catch (err) {\n    next(err);\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tre punti critici in questo blocco. Primo: <code>generators.codeVerifier()<\/code> usa internamente <code>crypto.randomBytes()<\/code>, una fonte di entropia crittografica adeguata. Non usare mai <code>Math.random()<\/code> per generare valori usati in contesti di sicurezza. Secondo: il <code>code_verifier<\/code> viene salvato in sessione server-side e non viene mai trasmesso al browser n\u00e9 incluso nei log. Terzo: <code>req.session.save()<\/code> \u00e8 chiamato con await prima del redirect per garantire che i dati PKCE siano persistiti prima che il browser venga reindirizzato al provider.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;URL di autorizzazione generato includer\u00e0 i parametri PKCE nella query string:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/your-org.okta.com\/oauth2\/default\/v1\/authorize?\n  client_id=0oaxxxxxxxxxxxxxxxxx\n  &response_type=code\n  &scope=openid+profile+email+offline_access\n  &redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauthorization-code%2Fcallback\n  &state=8xLOxBtZp8\n  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\n  &code_challenge_method=S256<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-6-e-7-gestione-del-callback-e-scambio-del-codice-con-i-token\">Step 6 e 7: Gestione del Callback e Scambio del Codice con i Token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dopo che l&#8217;utente completa l&#8217;autenticazione sul provider, viene reindirizzato al tuo <code>redirect_uri<\/code> con un parametro <code>code<\/code> nell&#8217;URL. Questo codice \u00e8 monouso, con scadenza tipicamente tra 60 e 300 secondi. Il callback handler deve eseguire quattro operazioni in sequenza: recuperare i dati PKCE dalla sessione, validare il parametro <code>state<\/code>, presentare il <code>code_verifier<\/code> per lo scambio, e salvare i token ricevuti.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Step 6: Gestione del callback OAuth\n\/\/ Step 7: Scambio codice + code_verifier per i token\nexport async function handleCallback(req, res, next) {\n  try {\n    const client = await getClient();\n    const params = client.callbackParams(req);\n\n    \/\/ Verifica state (protezione CSRF)\n    if (!req.session.pkce || req.session.pkce.state !== params.state) {\n      return res.status(403).send('Parametro state non valido. Possibile attacco CSRF.');\n    }\n\n    \/\/ Scambio codice di autorizzazione + code_verifier per i token\n    const tokenSet = await client.callback(\n      `${process.env.APP_BASE_URL}\/authorization-code\/callback`,\n      params,\n      {\n        code_verifier: req.session.pkce.code_verifier,\n        state: req.session.pkce.state,\n      }\n    );\n\n    \/\/ openid-client valida automaticamente l'ID Token:\n    \/\/ firma, iss, aud, exp, iat, nonce\n    const claims = tokenSet.claims();\n\n    \/\/ Salva i token in sessione server-side\n    req.session.tokens = {\n      access_token: tokenSet.access_token,\n      id_token: tokenSet.id_token,\n      refresh_token: tokenSet.refresh_token,\n      expires_at: tokenSet.expires_at,    \/\/ Unix timestamp\n    };\n    req.session.user = {\n      sub: claims.sub,\n      email: claims.email,\n      name: claims.name,\n    };\n\n    \/\/ Elimina i dati PKCE dalla sessione (non servono pi\u00f9)\n    delete req.session.pkce;\n    await new Promise((resolve, reject) =>\n      req.session.save(err => err ? reject(err) : resolve())\n    );\n\n    res.redirect('\/dashboard');\n  } catch (err) {\n    next(err);\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il metodo <code>client.callback()<\/code> esegue automaticamente la validazione completa dell&#8217;ID Token: verifica la firma crittografica contro la chiave pubblica del provider (recuperata dall&#8217;endpoint JWKS), controlla i claim <code>iss<\/code> (issuer), <code>aud<\/code> (audience), <code>exp<\/code> (scadenza), <code>iat<\/code> (issued at) e il <code>nonce<\/code> se presente. Non devi implementare questa logica manualmente: la libreria la gestisce per te.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-8-middleware-di-autenticazione-e-refresh-automatico-dei-token\">Step 8: Middleware di Autenticazione e Refresh Automatico dei Token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La durata raccomandata per gli access token \u00e8 tra 15 e 60 minuti. Con access token a vita breve, il middleware di autenticazione deve verificare la scadenza prima di servire ogni richiesta protetta e, se necessario, usare il refresh token per ottenere nuovi token senza richiedere il login all&#8217;utente.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Middleware: verifica autenticazione e aggiorna i token se necessario\nexport async function requireAuth(req, res, next) {\n  if (!req.session.tokens || !req.session.user) {\n    return res.redirect('\/login');\n  }\n\n  \/\/ Buffer di 60 secondi: aggiorna se scade entro un minuto\n  const now = Math.floor(Date.now() \/ 1000);\n  if (req.session.tokens.expires_at - 60 < now) {\n    try {\n      await refreshTokens(req);\n    } catch (err) {\n      \/\/ Refresh fallito (token revocato o scaduto): re-login obbligatorio\n      req.session.destroy(() => res.redirect('\/login'));\n      return;\n    }\n  }\n\n  next();\n}\n\nasync function refreshTokens(req) {\n  const client = await getClient();\n  if (!req.session.tokens.refresh_token) {\n    throw new Error('Nessun refresh token disponibile');\n  }\n\n  const newTokenSet = await client.refresh(req.session.tokens.refresh_token);\n\n  \/\/ Alcuni provider ruotano il refresh token: usa il nuovo se disponibile\n  req.session.tokens = {\n    access_token: newTokenSet.access_token,\n    id_token: newTokenSet.id_token ?? req.session.tokens.id_token,\n    refresh_token: newTokenSet.refresh_token ?? req.session.tokens.refresh_token,\n    expires_at: newTokenSet.expires_at,\n  };\n  await new Promise((resolve, reject) =>\n    req.session.save(err => err ? reject(err) : resolve())\n  );\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Il buffer di 60 secondi prima della scadenza evita che una richiesta API fallisca perch\u00e9 il token scade durante il trasferimento. Per access token con vita di 15 minuti (900 secondi), questo buffer rappresenta il 6,7% della durata totale, un valore proporzionato. Se il refresh fallisce (ad esempio perch\u00e9 il refresh token \u00e8 stato revocato dall&#8217;utente o scaduto), la sessione viene distrutta e l&#8217;utente viene reindirizzato al login.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-9-e-10-refresh-token-e-logout-sicuro\">Step 9 e 10: Refresh Token e Logout Sicuro<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il refresh token permette all&#8217;applicazione di ottenere nuovi access token senza interrompere l&#8217;esperienza dell&#8217;utente. Per riceverlo nella risposta iniziale, devi includere lo scope <code>offline_access<\/code> nella richiesta di autorizzazione. Su Okta, devi anche abilitare esplicitamente il Refresh Token come Grant Type nelle impostazioni dell&#8217;applicazione. Su Google, il refresh token viene emesso solo alla prima autorizzazione: nelle sessioni successive, aggiungi <code>prompt: 'consent'<\/code> all&#8217;URL di autorizzazione per forzarne l&#8217;emissione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Alcuni provider implementano il Refresh Token Rotation: ogni uso del refresh token genera un nuovo refresh token e invalida il vecchio. Se un refresh token viene rubato e usato da un attaccante, il tentativo legittimo successivo fallir\u00e0, segnalando la compromissione. Il codice deve gestire questa casistica mantenendo il vecchio refresh token se il provider non ne restituisce uno nuovo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il logout corretto in OAuth 2.0 richiede due passaggi distinti: la distruzione della sessione locale e il reindirizzamento all&#8217;End Session Endpoint del provider (OpenID Connect RP-Initiated Logout). Eseguire solo il logout locale lascia la sessione attiva sul provider: chiunque abbia accesso al browser pu\u00f2 riautenticarsi immediatamente senza reinserire le credenziali.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Step 10: Logout sicuro (locale + provider)\nexport async function logout(req, res, next) {\n  try {\n    const client = await getClient();\n    const idToken = req.session.tokens?.id_token;\n\n    \/\/ Costruisce l'URL di end session con id_token_hint\n    const logoutUrl = client.endSessionUrl({\n      id_token_hint: idToken,\n      post_logout_redirect_uri: process.env.POST_LOGOUT_URL,\n    });\n\n    \/\/ Prima distruggi la sessione locale, poi reindirizza al provider\n    req.session.destroy((err) => {\n      if (err) return next(err);\n      res.redirect(logoutUrl);\n    });\n  } catch (err) {\n    next(err);\n  }\n}\n\nexport function logoutCompletato(req, res) {\n  res.send(`\n    <h1>Disconnessione completata<\/h1>\n    <p>Hai effettuato il logout in modo sicuro.<\/p>\n    <a href=\"\/login\">Accedi di nuovo<\/a>\n  `);\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-11-e-12-server-express-completo-con-route-protette\">Step 11 e 12: Server Express Completo con Route Protette<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il file <code>index.js<\/code> assembla tutti i componenti: configurazione della sessione con parametri di sicurezza corretti, routing OAuth e route protette con il middleware <code>requireAuth<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ index.js\nimport express from 'express';\nimport session from 'express-session';\nimport 'dotenv\/config';\nimport {\n  login,\n  handleCallback,\n  logout,\n  logoutCompletato,\n  requireAuth,\n} from '.\/auth.js';\n\nconst app = express();\n\n\/\/ Step 12: Configurazione sessione sicura\napp.use(session({\n  secret: process.env.SESSION_SECRET,\n  resave: false,\n  saveUninitialized: false,\n  cookie: {\n    secure: process.env.NODE_ENV === 'production', \/\/ Solo HTTPS in produzione\n    httpOnly: true,     \/\/ Inaccessibile da JavaScript lato client\n    sameSite: 'lax',   \/\/ Protezione CSRF base per navigazioni normali\n    maxAge: 24 * 60 * 60 * 1000,  \/\/ 24 ore\n  },\n}));\n\napp.use(express.json());\n\n\/\/ Route OAuth 2.0 + PKCE\napp.get('\/login', login);\napp.get('\/authorization-code\/callback', handleCallback);\napp.get('\/logout', logout);\napp.get('\/logout-completato', logoutCompletato);\n\n\/\/ Step 11: Route protette con middleware requireAuth\napp.get('\/dashboard', requireAuth, (req, res) => {\n  res.json({\n    messaggio: 'Dashboard protetta da OAuth 2.0 PKCE',\n    utente: req.session.user,\n    token_scade: new Date(req.session.tokens.expires_at * 1000).toISOString(),\n  });\n});\n\napp.get('\/profilo', requireAuth, (req, res) => {\n  res.json(req.session.user);\n});\n\napp.get('\/', (req, res) => {\n  const loggato = !!req.session.tokens;\n  res.send(`\n    <h1>App OAuth 2.0 PKCE<\/h1>\n    ${loggato\n      ? `<p>Ciao, ${req.session.user?.name}! <a href=\"\/dashboard\">Dashboard<\/a> | <a href=\"\/logout\">Logout<\/a><\/p>`\n      : '<a href=\"\/login\">Accedi con OAuth 2.0<\/a>'\n    }\n  `);\n});\n\n\/\/ Gestione errori globale\napp.use((err, req, res, _next) => {\n  console.error(err.message);\n  res.status(500).json({ errore: 'Errore interno del server', dettaglio: err.message });\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n  console.log(`Server OAuth 2.0 PKCE avviato su http:\/\/localhost:${PORT}`);\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Quattro punti critici nella configurazione della sessione. Il flag <code>secure: true<\/code> in produzione garantisce che il cookie di sessione sia trasmesso solo su connessioni HTTPS cifrate. Il flag <code>httpOnly: true<\/code> impedisce a qualsiasi JavaScript sulla pagina di leggere il cookie di sessione, eliminando il rischio di furto tramite XSS. L&#8217;opzione <code>saveUninitialized: false<\/code> evita di creare sessioni per ogni visitatore non autenticato, riducendo il consumo di memoria e storage. Infine, <code>sameSite: 'lax'<\/code> fornisce una protezione CSRF di base: il browser non invia il cookie durante navigazioni cross-site iniziate da terze parti, ma lo invia per i redirect top-level normali (come quelli dal provider OAuth).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"archiviazione-sicura-dei-token-3-pattern-a-confronto\">Archiviazione Sicura dei Token: 3 Pattern a Confronto<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La scelta del meccanismo di archiviazione dei token influenza direttamente la sicurezza dell&#8217;applicazione. Esistono tre approcci principali con caratteristiche di sicurezza molto diverse, e la scelta dipende dall&#8217;architettura (web app tradizionale vs SPA vs app mobile).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La <strong>sessione server-side<\/strong> (l&#8217;approccio di questa guida) archivia i token in memoria del server o in un database Redis, associati a un session ID opaco inviato al client come cookie <code>httpOnly<\/code>. Il browser non vede mai il token direttamente: pu\u00f2 solo presentare il cookie di sessione. Questo elimina completamente il rischio di furto tramite XSS. Lo svantaggio \u00e8 la scalabilit\u00e0: con pi\u00f9 istanze del server dietro un load balancer, serve un session store condiviso come Redis o Memcached. L&#8217;installazione di <code>connect-redis<\/code> \u00e8 semplice e risolve il problema in produzione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il <strong>Backend For Frontend (BFF)<\/strong> \u00e8 l&#8217;evoluzione moderna per architetture SPA e React\/Vue\/Angular. Un microservizio Node.js dedicato (il BFF) gestisce interamente il flusso OAuth e mantiene i token lato server, esponendo al frontend solo cookie di sessione sicuri. Ogni chiamata API del frontend passa attraverso il BFF, che aggiunge l&#8217;Authorization header prima di inoltrarla all&#8217;API reale. \u00c8 l&#8217;approccio raccomandato da OAuth 2.0 Security BCP per le Single Page Application.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il <strong>localStorage o sessionStorage<\/strong> \u00e8 l&#8217;approccio da non usare mai per i token OAuth. Qualsiasi script sulla pagina, inclusi script di analytics, widget di chat o librerie di terze parti, pu\u00f2 leggere il contenuto del localStorage. Un attacco XSS che inietta anche una sola riga di codice nella pagina pu\u00f2 estrarre tutti i token in millisecondi e trasmetterli a un server remoto. OWASP raccomanda esplicitamente di non archiviare token sensibili nel localStorage.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Metodo<\/th><th>Rischio XSS<\/th><th>Scalabilit\u00e0<\/th><th>Complessit\u00e0<\/th><th>Raccomandato per<\/th><\/tr><\/thead><tbody><tr><td>Sessione server-side<\/td><td>Basso<\/td><td>Richiede Redis<\/td><td>Media<\/td><td>Web app tradizionali<\/td><\/tr><tr><td>Cookie httpOnly cifrato<\/td><td>Molto basso<\/td><td>Alta<\/td><td>Alta<\/td><td>API stateless<\/td><\/tr><tr><td>Backend For Frontend (BFF)<\/td><td>Molto basso<\/td><td>Alta<\/td><td>Alta<\/td><td>SPA (React, Vue, Angular)<\/td><\/tr><tr><td>localStorage<\/td><td>Molto alto<\/td><td>Alta<\/td><td>Bassa<\/td><td>Mai per token OAuth<\/td><\/tr><tr><td>sessionStorage<\/td><td>Alto<\/td><td>N\/A<\/td><td>Bassa<\/td><td>Mai per token sensibili<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"6-errori-comuni-nellimplementazione-oauth-2-0-con-pkce\">6 Errori Comuni nell&#8217;Implementazione OAuth 2.0 con PKCE<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Errore 1: non validare il parametro state nel callback.<\/strong> Molte implementazioni generano il <code>state<\/code> ma non eseguono la verifica nel callback. Senza questo check, l&#8217;applicazione \u00e8 vulnerabile ad attacchi CSRF: un attaccante pu\u00f2 forzare l&#8217;utente a completare un flusso OAuth non richiesto. La verifica deve essere esplicita: se i due valori non corrispondono, rispondi con HTTP 403 e non procedere con lo scambio del codice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Errore 2: usare code_challenge_method plain invece di S256.<\/strong> Il metodo <code>plain<\/code> trasmette il <code>code_verifier<\/code> non trasformato come <code>code_challenge<\/code>. Chiunque osservi la prima richiesta di autorizzazione (log del provider, proxy aziendale, MitM) ottiene il <code>code_verifier<\/code> direttamente e pu\u00f2 completare lo scambio. Il metodo <code>S256<\/code> risolve il problema inviando solo l&#8217;hash SHA-256, rendendo il <code>code_verifier<\/code> computazionalmente non ricavabile dall&#8217;hash.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Errore 3: archiviare i token in localStorage o variabili JavaScript.<\/strong> Come descritto nella sezione precedente, qualsiasi script sulla pagina pu\u00f2 accedere al localStorage. Uno script malevolo iniettato tramite XSS pu\u00f2 estrarre i token e trasmetterli a un server remoto. La soluzione \u00e8 la sessione server-side con cookie <code>httpOnly<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Errore 4: redirect_uri con wildcard o matching non esatto.<\/strong> Se il provider accetta <code>redirect_uri<\/code> con prefissi o wildcard, un attaccante pu\u00f2 costruire un URL di autorizzazione con un <code>redirect_uri<\/code> controllato da lui. Dopo l&#8217;autenticazione, il codice viene inviato al server dell&#8217;attaccante. Registra sempre l&#8217;URL completo e letterale: <code>https:\/\/app.esempio.it\/callback<\/code>, non <code>https:\/\/app.esempio.it\/*<\/code>. Verifica che il tuo provider implementi il matching rigoroso.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Errore 5: non eliminare i dati PKCE dalla sessione dopo il callback.<\/strong> Il <code>code_verifier<\/code> e lo <code>state<\/code> sono monouso: dopo il callback completato con successo, non hanno pi\u00f9 utilit\u00e0. Lasciarli in sessione aumenta inutilmente il volume dei dati nella sessione e potrebbe creare confusione in flussi di login multipli paralleli dalla stessa sessione. Rimuovili con <code>delete req.session.pkce<\/code> immediatamente dopo il callback riuscito.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Errore 6: non chiamare req.session.save() con await prima del redirect.<\/strong> Express-session salva la sessione in modo asincrono. Se chiami <code>res.redirect()<\/code> immediatamente dopo aver modificato la sessione senza attendere il completamento del salvataggio, il browser pu\u00f2 ricevere il redirect prima che i dati PKCE siano stati persistiti. Quando il provider reindirizza al callback, la sessione non conterr\u00e0 il <code>code_verifier<\/code> e l&#8217;autenticazione fallir\u00e0 con un errore non ovvio. Usa sempre <code>await req.session.save()<\/code> prima di qualsiasi redirect critico.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"troubleshooting-8-errori-frequenti-e-come-risolverli\">Troubleshooting: 8 Errori Frequenti e Come Risolverli<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>1. &#8220;invalid_grant&#8221; durante lo scambio del codice.<\/strong> Questo errore indica che il codice di autorizzazione \u00e8 scaduto (tipicamente entro 60-300 secondi), gi\u00e0 usato in precedenza, o che il <code>code_verifier<\/code> non corrisponde al <code>code_challenge<\/code>. Prima di tutto, verifica che il <code>code_verifier<\/code> sia salvato correttamente nella sessione. Se usi un load balancer senza sticky sessions, la richiesta di callback potrebbe arrivare a un&#8217;istanza diversa da quella che ha gestito il login, dove la sessione in memoria non esiste. La soluzione \u00e8 Redis come session store condiviso.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>2. &#8220;redirect_uri_mismatch&#8221;.<\/strong> Il <code>redirect_uri<\/code> nel codice non corrisponde esattamente a quello registrato sul provider. Controlla ogni carattere: il protocollo (http vs https), il numero di porta, le maiuscole\/minuscole, lo slash finale. Anche un parametro query inatteso nell&#8217;URL di callback causa questo errore su provider rigidi. Apri il documento di discovery del provider per vedere le regole di matching.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>3. State non trovato in sessione nel callback.<\/strong> La sessione esiste ma \u00e8 vuota o il campo <code>pkce<\/code> \u00e8 assente. Cause frequenti: il cookie di sessione non viene inviato al provider perch\u00e9 <code>secure: true<\/code> \u00e8 attivo su HTTP locale; il <code>sameSite: 'strict'<\/code> blocca il cookie durante il redirect cross-site dal provider; <code>req.session.save()<\/code> non \u00e8 stato atteso prima del redirect. In sviluppo locale, usa <code>secure: false<\/code> e <code>sameSite: 'lax'<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>4. &#8220;code_challenge_method not supported&#8221;.<\/strong> Il provider non supporta PKCE o non supporta S256. Verifica il campo <code>code_challenge_methods_supported<\/code> nel documento di discovery del provider. Se il campo \u00e8 assente o contiene solo <code>plain<\/code>, il provider non \u00e8 conforme alle raccomandazioni RFC 9700 e va aggiornato o sostituito.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>5. &#8220;JWT expired&#8221; nella validazione dell&#8217;ID Token.<\/strong> Indica uno skew temporale tra il tuo server e il server del provider. <code>openid-client<\/code> accetta per default 30 secondi di tolleranza. Puoi aumentarla con <code>client[custom.clockTolerance] = 60<\/code>, ma la soluzione duratura \u00e8 sincronizzare il clock del server tramite NTP. In produzione, sistemi come AWS EC2 o Google Compute Engine sincronizzano il clock automaticamente.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>6. Refresh token non presente nella risposta.<\/strong> Il refresh token viene emesso solo con lo scope <code>offline_access<\/code> e solo se il provider \u00e8 configurato per emetterlo. Su Okta, verifica che &#8220;Refresh Token&#8221; sia abilitato in Grant Types nelle impostazioni dell&#8217;applicazione. Su Google, il refresh token viene emesso solo alla prima autorizzazione; aggiungi <code>access_type: 'offline'<\/code> e <code>prompt: 'consent'<\/code> all&#8217;URL di autorizzazione per forzarne l&#8217;emissione nelle sessioni successive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>7. &#8220;Cannot read properties of undefined&#8221; su req.session.pkce.<\/strong> La sessione esiste ma il campo <code>pkce<\/code> non \u00e8 mai stato impostato o \u00e8 stato eliminato prematuramente. Verifica che la route di login imposti effettivamente <code>req.session.pkce<\/code> e che <code>req.session.save()<\/code> sia stato atteso prima del redirect al provider. Se il problema persiste, aggiungi un log prima del redirect per verificare il contenuto della sessione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>8. Il logout locale funziona ma l&#8217;utente rimane loggato sul provider.<\/strong> Hai distrutto la sessione locale ma non hai reindirizzato verso l&#8217;End Session Endpoint del provider. Verifica che il provider supporti OpenID Connect RP-Initiated Logout (campo <code>end_session_endpoint<\/code> nel documento di discovery). Non tutti i provider lo implementano: su GitHub OAuth, per esempio, non esiste un endpoint di logout standard. Se il provider non supporta l&#8217;end session, informa l&#8217;utente di chiudere manualmente la sessione nel browser del provider.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"consigli-avanzati-multi-provider-redis-e-conformita-nis2\">Consigli Avanzati: Multi-Provider, Redis e Conformit\u00e0 NIS2<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In un&#8217;applicazione reale potresti dover supportare pi\u00f9 provider OAuth contemporaneamente (Google, Microsoft Azure AD, GitHub, Okta). La libreria <code>openid-client<\/code> lo gestisce bene: ogni provider espone il proprio documento di discovery OpenID Connect all&#8217;URL <code>{issuer}\/.well-known\/openid-configuration<\/code>, e puoi istanziare un client separato per ciascuno. Mantieni una mappa di client in cache, indicizzati per provider name, e seleziona il client corretto in base a un parametro nella richiesta di login (ad esempio <code>\/login?provider=google<\/code>).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Per il deployment in produzione con pi\u00f9 istanze del server, sostituisci il session store in-memory con Redis usando <code>connect-redis<\/code>. Installa <code>connect-redis<\/code> e il client Redis (<code>redis<\/code>), crea un client Redis con la configurazione del tuo cluster, e passa lo store come opzione a <code>express-session<\/code>. Con Redis come session store, le sessioni sopravvivono ai riavvii del processo Node.js e funzionano correttamente con load balancer che non implementano sticky sessions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Per le organizzazioni soggette alla Direttiva NIS2 (recepita in Italia con il D.Lgs. 138\/2024 che coinvolge circa 12.000 aziende), l&#8217;implementazione di OAuth 2.0 con PKCE contribuisce ai controlli di sicurezza relativi alla gestione degli accessi e all&#8217;autenticazione. RFC 9700 e le raccomandazioni PKCE sono allineate ai requisiti NIS2 per l&#8217;autenticazione forte e la gestione sicura delle identit\u00e0 digitali. Abbinato all&#8217;autenticazione a due fattori (<a href=\"\/two-factor-authentication-nodejs\/\">2FA in Node.js<\/a>), OAuth 2.0 con PKCE soddisfa i requisiti di MFA per le entit\u00e0 essenziali e importanti secondo l&#8217;Allegato I del D.Lgs. 138\/2024.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Per le applicazioni con requisiti di sicurezza elevati, considera l&#8217;aggiunta di DPoP (Demonstration of Proof-of-Possession), definito nell&#8217;RFC 9449. DPoP lega l&#8217;access token alla chiave privata del client, rendendo i token rubati inutilizzabili su altri dispositivi. <code>openid-client<\/code> supporta DPoP nativamente: aggiungi la generazione della chiave DPoP prima del login e includila nelle richieste token.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"output-di-esempio-flusso-completo-dallinizio-al-token\">Output di Esempio: Flusso Completo dall&#8217;Inizio al Token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ecco l&#8217;output che dovresti vedere nel terminale durante un&#8217;autenticazione OAuth 2.0 PKCE completata con successo, utile per verificare che ogni passaggio funzioni correttamente.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Avvio server\n$ node --env-file=.env index.js\nServer OAuth 2.0 PKCE avviato su http:\/\/localhost:3000\n\n# Step 1: Utente visita \/login\nGET \/login 302 - Reindirizzamento a Okta\n  code_verifier generato: 128 caratteri\n  code_challenge (S256): E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM\n  state salvato in sessione: aBcDeFgHiJ1234567890\n\n# Step 2: Utente autentica su Okta (credenziali + eventuale MFA)\n# Step 3: Okta reindirizza a callback con codice monouso\nGET \/authorization-code\/callback?code=XXXXXXXXXXX&state=aBcDeFgHiJ1234567890 -\n\n# Step 4: Validazione state: OK (corrisponde alla sessione)\n# Step 5: Scambio codice + code_verifier per token\n  access_token ricevuto: eyJhbGciOiJSUzI1NiJ9... (JWT, 900 secondi)\n  id_token validato: sub=00uXXXX, email=mario@esempio.it, name=Mario Rossi\n  refresh_token ricevuto: def502...\n  expires_at: 1750425600 (15:00:00 UTC)\n\n# Step 6: Sessione aggiornata, reindirizzamento a \/dashboard\nGET \/dashboard 200\n{\n  \"messaggio\": \"Dashboard protetta da OAuth 2.0 PKCE\",\n  \"utente\": { \"sub\": \"00uXXXX\", \"email\": \"mario@esempio.it\", \"name\": \"Mario Rossi\" },\n  \"token_scade\": \"2026-06-20T15:00:00.000Z\"\n}\n\n# Dopo 15 minuti: refresh automatico trasparente\nGET \/profilo - Token in scadenza, refresh automatico...\n  Nuovo access_token emesso, expires_at aggiornato\nGET \/profilo 200 - { \"sub\": \"00uXXXX\", \"email\": \"mario@esempio.it\" }<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq-oauth-2-0-e-pkce-in-node-js\">FAQ: OAuth 2.0 e PKCE in Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>PKCE sostituisce il client secret?<\/strong> No. PKCE e il client secret sono meccanismi complementari con scopi diversi. Il client secret autentica l&#8217;applicazione client presso il server di autorizzazione. PKCE protegge il flusso di autorizzazione dall&#8217;intercettazione e dall&#8217;iniezione del codice. RFC 9700 raccomanda di usare entrambi nei client confidenziali: il secret per l&#8217;autenticazione del client, PKCE per la sicurezza del flusso.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>OAuth 2.0 e OpenID Connect sono la stessa cosa?<\/strong> No, anche se vengono spesso usati insieme. OAuth 2.0 \u00e8 un framework di autorizzazione che gestisce i permessi di accesso alle risorse. OpenID Connect (OIDC) \u00e8 un layer di identit\u00e0 costruito sopra OAuth 2.0 che aggiunge l&#8217;ID Token (un JWT con informazioni sull&#8217;utente) e l&#8217;endpoint <code>userinfo<\/code> per l&#8217;autenticazione. Quando includi <code>openid<\/code> nello scope OAuth, stai usando OIDC sopra OAuth 2.0.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Qual \u00e8 la differenza tra access token e ID token?<\/strong> L&#8217;access token autorizza le chiamate alle API protette: va incluso nell&#8217;header <code>Authorization: Bearer {token}<\/code>. L&#8217;ID token contiene informazioni sull&#8217;identit\u00e0 dell&#8217;utente (nome, email, sub): viene usato solo dal client per conoscere chi si \u00e8 autenticato, non va mai inviato alle API di terze parti. Condividere l&#8217;ID token con le API \u00e8 una vulnerabilit\u00e0 di sicurezza.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>\u00c8 sicuro usare OAuth 2.0 su HTTP in sviluppo locale?<\/strong> Su <code>localhost<\/code>, HTTP \u00e8 accettabile in sviluppo perch\u00e9 il traffico non esce dal computer. Molti provider e browser trattano <code>localhost<\/code> come un&#8217;origine sicura. In produzione, HTTPS \u00e8 obbligatorio e non negoziabile: il codice di autorizzazione e i token non devono mai transitare su connessioni non cifrate. Usa sempre <code>secure: true<\/code> per i cookie in produzione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Come gestire OAuth 2.0 con un frontend React separato dal backend Node.js?<\/strong> Il pattern corretto \u00e8 il Backend For Frontend (BFF): il frontend React\/Vue parla esclusivamente con il backend Node.js tramite chiamate API, senza mai gestire token direttamente. Il BFF gestisce il flusso OAuth, mantiene i token lato server, e restituisce al frontend solo cookie di sessione <code>httpOnly<\/code>. Le chiamate API del frontend passano attraverso il BFF, che aggiunge l&#8217;Authorization header prima di inoltrarle all&#8217;API reale.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Quanto devono durare gli access token?<\/strong> RFC 9700 raccomanda access token a vita breve: tra 15 e 60 minuti. Per API con dati particolarmente sensibili, 5-15 minuti. I refresh token possono avere vita pi\u00f9 lunga (ore, giorni o anche pi\u00f9 a seconda del provider), con revoca immediata se compromessi. L&#8217;obiettivo \u00e8 minimizzare la finestra temporale in cui un token rubato \u00e8 utilizzabile.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>OAuth 2.1 \u00e8 gi\u00e0 disponibile?<\/strong> A giugno 2026, OAuth 2.1 \u00e8 ancora in stato di draft IETF. Tuttavia, le sue raccomandazioni principali (PKCE obbligatorio per tutti i client, eliminazione del Implicit Flow e del Password Grant, PKCE anche per i client confidenziali) sono gi\u00e0 incorporate in RFC 9700, uno standard pubblicato. Implementare questa guida oggi significa essere gi\u00e0 conformi a OAuth 2.1 quando diventer\u00e0 standard definitivo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Come verificare che il provider supporti PKCE?<\/strong> Apri il documento di discovery OpenID Connect del provider all&#8217;URL <code>{issuer}\/.well-known\/openid-configuration<\/code> e cerca il campo <code>code_challenge_methods_supported<\/code>. Deve contenere <code>\"S256\"<\/code>. Se il campo \u00e8 assente, prova a fare una richiesta di autorizzazione con i parametri PKCE: un provider conforme accetter\u00e0 la richiesta anche senza dichiararlo esplicitamente nel discovery.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"letture-correlate\">Letture Correlate<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Per approfondire la sicurezza delle applicazioni Node.js e la gestione delle identit\u00e0:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n  <li><a href=\"\/jwt-authentication-nodejs\/\">JWT Authentication in Node.js: 10 Step per Implementarla Correttamente<\/a><\/li>\n  <li><a href=\"\/two-factor-authentication-nodejs\/\">Two-Factor Authentication in Node.js: 11 Step con TOTP [2026]<\/a><\/li>\n  <li><a href=\"\/csrf-protection-nodejs\/\">CSRF Protection in Node.js: 12 Step per Proteggere le Form<\/a><\/li>\n  <li><a href=\"\/nodejs-session-management\/\">Node.js Session Management: 11 Step per Sessioni Sicure<\/a><\/li>\n  <li><a href=\"\/owasp-top-10-nodejs\/\">OWASP Top 10 in Node.js: 12 Step per Proteggere la tua API<\/a><\/li>\n  <li><a href=\"\/passkeys-vs-passwords\/\">Passkey vs Password: 8,5s vs 31s di Sign-In nel 2026<\/a><\/li>\n  <li><a href=\"\/xss-prevention-nodejs\/\">XSS in Node.js: Prevenirlo in 12 Step<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Specifiche ufficiali e risorse tecniche esterne:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n  <li><a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7636\" rel=\"nofollow noopener\" target=\"_blank\">RFC 7636: Proof Key for Code Exchange by OAuth Public Clients<\/a><\/li>\n  <li><a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc9700\" rel=\"nofollow noopener\" target=\"_blank\">RFC 9700: OAuth 2.0 Security Best Current Practice<\/a><\/li>\n  <li><a href=\"https:\/\/oauth.net\/2\/pkce\/\" rel=\"nofollow noopener\" target=\"_blank\">PKCE per OAuth 2.0 (oauth.net)<\/a><\/li>\n  <li><a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Authorization_Cheat_Sheet.html\" rel=\"nofollow noopener\" target=\"_blank\">OWASP Authorization Cheat Sheet<\/a><\/li>\n  <li><a href=\"https:\/\/www.npmjs.com\/package\/openid-client\" rel=\"nofollow noopener\" target=\"_blank\">openid-client su npm<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>OAuth 2.0 \u00e8 il protocollo di autorizzazione alla base di ogni &#8220;Accedi con Google&#8221; o &#8220;Login con Microsoft&#8221; che usi ogni giorno. Nella sua implementazione classica con il flusso Authorization\u2026<\/p>\n","protected":false},"author":3,"featured_media":272,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-271","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\/271","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\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/comments?post=271"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/271\/revisions"}],"predecessor-version":[{"id":273,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/271\/revisions\/273"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media\/272"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media?parent=271"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/categories?post=271"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/tags?post=271"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}