{"id":133,"date":"2026-06-14T20:46:28","date_gmt":"2026-06-14T20:46:28","guid":{"rendered":"https:\/\/shattered.io\/it\/2026\/06\/14\/totp-2fa-nodejs\/"},"modified":"2026-06-14T20:47:55","modified_gmt":"2026-06-14T20:47:55","slug":"totp-2fa-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/it\/totp-2fa-nodejs\/","title":{"rendered":"TOTP 2FA in Node.js: Autenticatore in 12 Step [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">L&#8217;autenticazione a due fattori basata su <strong>TOTP<\/strong> (Time-based One-Time Password) \u00e8 oggi lo standard minimo per proteggere un account contro il furto di credenziali. Quando un utente inserisce le sei cifre generate dalla sua app, dimostra di possedere un segreto condiviso che non transita mai in rete dopo l&#8217;attivazione. In questo tutorial costruisci, da zero e in 12 step, un sistema 2FA TOTP completo in Node.js: generazione del segreto, QR code di provisioning, verifica con tolleranza temporale, codici di backup, rate limiting e integrazione nel login. Tutto il codice \u00e8 funzionante e allineato a <strong>RFC 6238<\/strong>, alle linee guida OWASP e a NIST SP 800-63B.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Alla fine avrai un progetto Express pronto per la produzione, compatibile con Google Authenticator, Microsoft Authenticator e Authy. Il focus \u00e8 pratico: ogni step include codice reale, output atteso e gli errori che vediamo pi\u00f9 spesso nelle code review. Data di aggiornamento: 14 giugno 2026.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"che-cose-il-totp-e-come-funziona-davvero\">Che cos&#8217;\u00e8 il TOTP e come funziona davvero<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il <strong>TOTP<\/strong> \u00e8 definito in <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6238\" target=\"_blank\" rel=\"noopener\">RFC 6238<\/a> come estensione dell&#8217;algoritmo HOTP descritto in <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc4226\" target=\"_blank\" rel=\"noopener\">RFC 4226<\/a>. La differenza \u00e8 netta: HOTP usa un contatore di eventi che si incrementa a ogni codice, mentre TOTP sostituisce quel contatore con un fattore derivato dal tempo. In pratica il client e il server calcolano lo stesso codice perch\u00e9 condividono due cose: un segreto e l&#8217;orologio.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il meccanismo, ridotto all&#8217;osso, funziona cos\u00ec. Si prende l&#8217;ora corrente in secondi dall&#8217;epoca Unix, la si divide per un intervallo fisso (lo <em>step<\/em>, di norma 30 secondi) e si ottiene un contatore intero. Questo contatore viene passato, insieme al segreto, a una funzione HMAC. Il digest risultante viene troncato secondo l&#8217;algoritmo di dynamic truncation di RFC 4226 fino a ottenere sei cifre decimali. Ogni 30 secondi il contatore cambia, quindi cambia anche il codice. Nessun dato sensibile viaggia in rete dopo l&#8217;enrollment: il segreto resta sul telefono dell&#8217;utente e nel database del server.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;algoritmo predefinito dell&#8217;ecosistema TOTP \u00e8 <strong>HMAC-SHA1<\/strong>, ereditato da HOTP. Non \u00e8 una scelta debole come pu\u00f2 sembrare: SHA-1 qui \u00e8 usato all&#8217;interno di un HMAC con chiave segreta, contesto in cui le collisioni note di SHA-1 non hanno alcun impatto pratico. RFC 6238 prevede comunque anche HMAC-SHA-256 e HMAC-SHA-512, ma attenzione: la maggior parte delle app di autenticazione assume SHA-1, e cambiare algoritmo senza che il client lo supporti produce codici che non combaciano mai. \u00c8 la prima causa di &#8220;il codice \u00e8 sempre sbagliato&#8221; che incontriamo.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il segreto condiviso \u00e8 quasi sempre codificato in <strong>Base32<\/strong>, perch\u00e9 si presta a essere scansionato in un QR code e digitato a mano senza ambiguit\u00e0 (niente 0\/O o 1\/l). La lunghezza raccomandata \u00e8 di 160 bit, valore che si allinea naturalmente all&#8217;output da 160 bit di HMAC-SHA1 e che NIST considera adeguato per un autenticatore di questo tipo. Da questi pochi parametri (segreto, step, cifre, algoritmo) deriva tutto il comportamento del sistema: vale la pena fissarli in modo esplicito invece di affidarsi ai default impliciti.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"totp-vs-hotp-vs-passkey-quale-scegliere-nel-2026\">TOTP vs HOTP vs passkey: quale scegliere nel 2026<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Prima di scrivere codice conviene capire dove si colloca il TOTP nel panorama del 2026. HOTP, basato su contatore, soffre di problemi di desincronizzazione: se l&#8217;utente genera codici senza usarli, contatore client e server divergono e serve una finestra di recupero. Per questo TOTP, ancorato al tempo, ha vinto come standard di fatto per le app di autenticazione.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il limite vero del TOTP \u00e8 un altro, e va detto con chiarezza fin da subito: <strong>non \u00e8 phishing-resistant<\/strong>. Un attaccante che gestisce un proxy Adversary-in-the-Middle (AiTM) pu\u00f2 intercettare in tempo reale sia la password sia il codice a sei cifre e replicarli verso il sito legittimo entro la finestra di validit\u00e0. Le analisi di sicurezza del 2024-2025 hanno mostrato un aumento netto di questi attacchi con kit di phishing come Evilginx. Per questo, dove possibile, le passkey FIDO2\/WebAuthn restano superiori perch\u00e9 legano l&#8217;autenticazione all&#8217;origine del sito.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Detto questo, il TOTP rimane lo strumento giusto in moltissimi contesti: non richiede hardware dedicato, funziona offline, \u00e8 gratuito, \u00e8 compatibile con tutte le app diffuse e alza enormemente il costo di un attacco rispetto alla sola password. La strategia sensata nel 2026 \u00e8 offrire le passkey come opzione primaria e il TOTP come secondo fattore universale e fallback. Il sistema che costruiamo qui \u00e8 esattamente questo secondo livello.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Caratteristica<\/th><th>TOTP (RFC 6238)<\/th><th>HOTP (RFC 4226)<\/th><th>Passkey (FIDO2)<\/th><\/tr><\/thead><tbody><tr><td>Fattore mobile<\/td><td>Tempo (30 s)<\/td><td>Contatore eventi<\/td><td>Chiave crittografica<\/td><\/tr><tr><td>Phishing-resistant<\/td><td>No<\/td><td>No<\/td><td>S\u00ec<\/td><\/tr><tr><td>Richiede hardware<\/td><td>No<\/td><td>No<\/td><td>No (usa il dispositivo)<\/td><\/tr><tr><td>Funziona offline<\/td><td>S\u00ec<\/td><td>S\u00ec<\/td><td>S\u00ec<\/td><\/tr><tr><td>Desincronizzazione<\/td><td>Solo per drift orario<\/td><td>Frequente<\/td><td>Assente<\/td><\/tr><tr><td>App compatibili<\/td><td>Tutte<\/td><td>Limitate<\/td><td>Browser e OS moderni<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"prerequisiti-versioni-librerie-e-ambiente\">Prerequisiti: versioni, librerie e ambiente<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Per seguire il tutorial servono competenze base di JavaScript e Node.js e un terminale. Useremo <strong>otplib<\/strong> come libreria TOTP perch\u00e9 \u00e8 attivamente mantenuta e implementa correttamente RFC 6238 e RFC 4226. Una nota sulla scelta: <code>speakeasy<\/code>, a lungo la libreria pi\u00f9 popolare, \u00e8 ferma alla versione 2.0.0 da anni e non riceve aggiornamenti significativi; per un progetto nuovo nel 2026 consigliamo otplib. La tabella riporta le versioni verificate al 14 giugno 2026.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Componente<\/th><th>Versione<\/th><th>Ruolo nel progetto<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>22 LTS o superiore<\/td><td>Runtime, modulo <code>crypto<\/code> nativo<\/td><\/tr><tr><td>otplib<\/td><td>13.4.1<\/td><td>Generazione e verifica TOTP<\/td><\/tr><tr><td>qrcode<\/td><td>1.5.4<\/td><td>QR code dall&#8217;URI otpauth:\/\/<\/td><\/tr><tr><td>express<\/td><td>5.2.1<\/td><td>Server e rotte di login\/2FA<\/td><\/tr><tr><td>express-rate-limit<\/td><td>ultima versione<\/td><td>Throttling sulla verifica OTP<\/td><\/tr><tr><td>better-sqlite3<\/td><td>ultima versione<\/td><td>Persistenza dei segreti (demo)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Verifica subito la versione di Node, perch\u00e9 otplib 13 richiede un runtime moderno e il modulo <code>crypto<\/code> aggiornato per la cifratura del segreto a riposo che vedremo nello Step 5.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node --version\n# atteso: v22.x.x o superiore\n\nnpm --version\n# atteso: 10.x o superiore<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-1-2-inizializzare-il-progetto-e-installare-le-dipendenze\">Step 1-2: Inizializzare il progetto e installare le dipendenze<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Crea la cartella del progetto e inizializza npm. Usiamo i moduli ES (<code>\"type\": \"module\"<\/code>) perch\u00e9 sono ormai lo standard. Negli esempi alterniamo la sintassi <code>import<\/code> ES e, dove pi\u00f9 chiaro, l&#8217;equivalente CommonJS.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Step 1: progetto\nmkdir totp-2fa-node &amp;&amp; cd totp-2fa-node\nnpm init -y\nnpm pkg set type=module\n\n# Step 2: dipendenze\nnpm install otplib@13.4.1 qrcode@1.5.4 express@5.2.1 \\\n  express-rate-limit better-sqlite3<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Output atteso al termine dell&#8217;installazione: npm elenca i pacchetti aggiunti senza vulnerabilit\u00e0 critiche. Se compaiono warning su <code>better-sqlite3<\/code> relativi alla compilazione nativa, verifica di avere i build tools del sistema operativo (su Debian\/Ubuntu: <code>build-essential<\/code> e <code>python3<\/code>). La struttura minima che andremo a popolare \u00e8 questa:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>totp-2fa-node\/\n\u251c\u2500\u2500 package.json\n\u251c\u2500\u2500 totp.js         # logica TOTP: segreto, URI, verifica\n\u251c\u2500\u2500 crypto-store.js # cifratura del segreto a riposo\n\u251c\u2500\u2500 backup.js       # codici di recupero\n\u251c\u2500\u2500 db.js           # persistenza demo\n\u2514\u2500\u2500 server.js       # app Express completa<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-3-generare-il-segreto-condiviso-totp\">Step 3: Generare il segreto condiviso TOTP<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Il cuore del 2FA \u00e8 il segreto. Con otplib lo generi in una riga: <code>authenticator.generateSecret()<\/code> restituisce una stringa Base32 pronta per il provisioning. Per impostazione predefinita otplib produce un segreto di lunghezza adeguata; passando 20 byte ottieni i 160 bit raccomandati da RFC 4226. Configuriamo anche le opzioni globali in modo esplicito, cos\u00ec il comportamento \u00e8 prevedibile e documentato.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ totp.js\nimport { authenticator } from 'otplib';\n\n\/\/ Parametri espliciti, allineati a RFC 6238 e alle app diffuse\nauthenticator.options = {\n  step: 30,          \/\/ intervallo in secondi\n  digits: 6,         \/\/ lunghezza del codice\n  algorithm: 'sha1', \/\/ default compatibile con Google Authenticator\n  window: 1,         \/\/ tolleranza: +\/- 1 step (vedi Step 7)\n};\n\nexport function generaSegreto() {\n  \/\/ 20 byte = 160 bit di entropia, codificati in Base32\n  return authenticator.generateSecret(20);\n}\n\nconsole.log(generaSegreto());\n\/\/ output esempio: KZXW6YTBOI======<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Una regola d&#8217;oro: <strong>genera un segreto nuovo per ogni utente e per ogni nuovo enrollment<\/strong>. Non riutilizzare mai lo stesso segreto tra account diversi e non derivarlo dalla password. Il segreto \u00e8 materiale crittografico ad alta entropia e va trattato come tale. Nel prossimo step lo trasformiamo in qualcosa che l&#8217;app dell&#8217;utente possa importare con una scansione.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-4-creare-luri-otpauth-e-il-qr-code-di-provisioning\">Step 4: Creare l&#8217;URI otpauth:\/\/ e il QR code di provisioning<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Le app di autenticazione importano il segreto tramite un URI standard nel formato <code>otpauth:\/\/totp\/Emittente:account?secret=...&amp;issuer=...<\/code>. L&#8217;etichetta contiene l&#8217;emittente (il nome del tuo servizio) e l&#8217;identificativo dell&#8217;utente; i parametri trasportano il segreto Base32 e ripetono l&#8217;issuer. otplib costruisce questo URI con <code>authenticator.keyuri()<\/code>. Poi <code>qrcode<\/code> lo trasforma in un&#8217;immagine scansionabile.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ totp.js (continua)\nimport QRCode from 'qrcode';\n\nexport function creaOtpauthUri(account, segreto, emittente = 'ShatteredApp') {\n  return authenticator.keyuri(account, emittente, segreto);\n}\n\nexport async function creaQrDataUrl(otpauthUri) {\n  \/\/ Data URL PNG da incorporare in un tag img nella pagina di setup\n  return QRCode.toDataURL(otpauthUri, { margin: 1, width: 240 });\n}\n\n\/\/ Esempio d'uso\nconst segreto = generaSegreto();\nconst uri = creaOtpauthUri('mario.rossi@example.com', segreto);\nconsole.log(uri);\n\/\/ otpauth:\/\/totp\/ShatteredApp:mario.rossi@example.com?secret=KZXW6YTB...&amp;issuer=ShatteredApp<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L&#8217;<code>issuer<\/code> non \u00e8 cosmetico: \u00e8 ci\u00f2 che l&#8217;utente vede nell&#8217;app accanto al codice. Impostarlo correttamente evita la confusione tra account multipli e riduce il rischio che l&#8217;utente cancelli la voce sbagliata. Mostra all&#8217;utente, nella stessa schermata, anche il segreto in chiaro come fallback per chi non pu\u00f2 scansionare il QR (per esempio chi usa un gestore di password desktop). Per approfondire come le diverse app gestiscono questi URI, vedi il nostro confronto su <a href=\"\/it\/google-authenticator-vs-microsoft-vs-authy\/\">Google Authenticator, Microsoft Authenticator e Authy<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-5-cifrare-il-segreto-a-riposo-nel-database\">Step 5: Cifrare il segreto a riposo nel database<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Qui si gioca la sicurezza reale del sistema. Se un attaccante esfiltra il database e i segreti TOTP sono in chiaro, pu\u00f2 rigenerare i codici di chiunque e il secondo fattore evapora. Il segreto non va hashato (deve restare recuperabile per il calcolo), quindi va <strong>cifrato a riposo<\/strong> con una chiave che vive fuori dal database, in una variabile d&#8217;ambiente o in un secrets manager. Usiamo AES-256-GCM dal modulo <code>crypto<\/code> nativo di Node, che fornisce confidenzialit\u00e0 e autenticazione del dato.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ crypto-store.js\nimport crypto from 'node:crypto';\n\n\/\/ 32 byte (256 bit) in esadecimale, da variabile d'ambiente\nconst KEY = Buffer.from(process.env.TOTP_ENC_KEY, 'hex');\n\nexport function cifra(testo) {\n  const iv = crypto.randomBytes(12); \/\/ 96 bit, raccomandato per GCM\n  const cipher = crypto.createCipheriv('aes-256-gcm', KEY, iv);\n  const enc = Buffer.concat([cipher.update(testo, 'utf8'), cipher.final()]);\n  const tag = cipher.getAuthTag();\n  \/\/ formato: iv:tag:ciphertext (tutto in base64)\n  return [iv.toString('base64'), tag.toString('base64'), enc.toString('base64')].join(':');\n}\n\nexport function decifra(pacchetto) {\n  const [ivB64, tagB64, dataB64] = pacchetto.split(':');\n  const decipher = crypto.createDecipheriv('aes-256-gcm', KEY, Buffer.from(ivB64, 'base64'));\n  decipher.setAuthTag(Buffer.from(tagB64, 'base64'));\n  return Buffer.concat([decipher.update(Buffer.from(dataB64, 'base64')), decipher.final()]).toString('utf8');\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Genera la chiave una sola volta con <code>openssl rand -hex 32<\/code> e conservala fuori dal repository. Per i fondamenti della cifratura simmetrica in Node trovi il dettaglio nella nostra guida alla <a href=\"\/it\/crittografia-end-to-end-nodejs\/\">crittografia end-to-end in Node.js<\/a>. Da qui in poi, ogni volta che salvi un segreto chiami <code>cifra(segreto)<\/code> e quando devi verificare un codice chiami <code>decifra()<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-6-verificare-il-codice-totp-inserito-dallutente\">Step 6: Verificare il codice TOTP inserito dall&#8217;utente<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La verifica \u00e8 il momento in cui il server ricalcola il codice atteso e lo confronta con quello digitato. Con otplib usi <code>authenticator.verify({ token, secret })<\/code>, che restituisce un booleano. La libreria gestisce internamente il confronto e la finestra di tolleranza configurata nello Step 3. Non implementare mai il confronto a mano con <code>===<\/code> su stringhe: otplib applica gi\u00e0 la logica corretta.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ totp.js (continua)\nexport function verificaToken(token, segreto) {\n  \/\/ token: le 6 cifre digitate; segreto: stringa Base32 decifrata\n  try {\n    return authenticator.verify({ token: String(token).trim(), secret: segreto });\n  } catch {\n    return false; \/\/ token malformato (non numerico, lunghezza errata)\n  }\n}\n\n\/\/ Test rapido\nconst s = generaSegreto();\nconst codiceCorrente = authenticator.generate(s);\nconsole.log(verificaToken(codiceCorrente, s)); \/\/ true\nconsole.log(verificaToken('000000', s));        \/\/ quasi certamente false<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Nota l&#8217;uso di <code>trim()<\/code> e <code>String()<\/code>: gli utenti incollano spesso codici con spazi o li passano come numeri che perdono lo zero iniziale. Un codice come <code>012345<\/code> diventa <code>12345<\/code> se trattato come intero, e la verifica fallisce. Normalizza sempre l&#8217;input prima di verificarlo. Questo \u00e8 uno degli errori pi\u00f9 frequenti e pi\u00f9 frustranti da diagnosticare.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-7-gestire-il-clock-skew-e-la-deriva-temporale\">Step 7: Gestire il clock skew e la deriva temporale<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">TOTP dipende dal fatto che orologio del client e del server siano allineati. In pratica non lo sono mai perfettamente: il telefono dell&#8217;utente pu\u00f2 essere avanti o indietro di qualche secondo. Per assorbire questa deriva si accetta anche il codice degli step adiacenti, configurando il parametro <code>window<\/code>. Con <code>window: 1<\/code> il server accetta il codice corrente pi\u00f9 quello precedente e quello successivo, coprendo circa 90 secondi totali.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">NIST SP 800-63B \u00e8 esplicito su questo punto: la durata accettata di un OTP deve essere basata sulla deriva attesa dell&#8217;orologio dell&#8217;autenticatore, in entrambe le direzioni. Tradotto in pratica: tieni la finestra stretta. Una finestra ampia (per esempio <code>window: 5<\/code>, che coprirebbe oltre cinque minuti) sembra comoda ma moltiplica i codici validi contemporaneamente, riducendo l&#8217;entropia effettiva e facilitando il brute force.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Valore <code>window<\/code><\/th><th>Codici accettati<\/th><th>Copertura temporale<\/th><th>Raccomandazione<\/th><\/tr><\/thead><tbody><tr><td>0<\/td><td>Solo corrente<\/td><td>~30 s<\/td><td>Troppo rigido per il mondo reale<\/td><\/tr><tr><td>1<\/td><td>Precedente, corrente, successivo<\/td><td>~90 s<\/td><td>Consigliato (default sensato)<\/td><\/tr><tr><td>2<\/td><td>5 codici<\/td><td>~150 s<\/td><td>Solo con utenti dagli orologi notoriamente instabili<\/td><\/tr><tr><td>5 o pi\u00f9<\/td><td>11+ codici<\/td><td>5,5+ minuti<\/td><td>Sconsigliato: indebolisce la sicurezza<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Assicurati che il server sincronizzi l&#8217;ora via NTP (<code>systemd-timesyncd<\/code> o <code>chrony<\/code>). Un server con orologio sbagliato fa fallire ogni verifica, anche con il codice giusto, e produce ticket di supporto impossibili da capire. \u00c8 la causa numero uno dei problemi in produzione e la prima cosa da controllare quando &#8220;tutti i codici sono errati&#8221;.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-8-generare-e-gestire-i-codici-di-backup\">Step 8: Generare e gestire i codici di backup<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cosa succede se l&#8217;utente perde il telefono? Senza una via di recupero, perde l&#8217;accesso all&#8217;account. I <strong>codici di backup<\/strong> (recovery codes) risolvono il problema: una serie di codici monouso da conservare in luogo sicuro, ognuno valido una sola volta. Vanno trattati come password: non salvarli mai in chiaro nel database, ma hashati. Qui usiamo lo scrypt nativo di Node.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ backup.js\nimport crypto from 'node:crypto';\n\nexport function generaCodiciBackup(quanti = 10) {\n  const codici = [];\n  for (let i = 0; i &lt; quanti; i++) {\n    \/\/ 5 byte -&gt; 10 cifre esadecimali, leggibili e robuste\n    codici.push(crypto.randomBytes(5).toString('hex'));\n  }\n  return codici;\n}\n\nexport function hashCodice(codice) {\n  const salt = crypto.randomBytes(16);\n  const dk = crypto.scryptSync(codice, salt, 32);\n  return salt.toString('hex') + ':' + dk.toString('hex');\n}\n\nexport function verificaCodiceBackup(codice, hashSalvato) {\n  const [saltHex, dkHex] = hashSalvato.split(':');\n  const dk = crypto.scryptSync(codice, Buffer.from(saltHex, 'hex'), 32);\n  \/\/ confronto a tempo costante per evitare timing attack\n  return crypto.timingSafeEqual(dk, Buffer.from(dkHex, 'hex'));\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Mostra i codici di backup all&#8217;utente <strong>una sola volta<\/strong>, subito dopo l&#8217;attivazione del 2FA, e invitalo a stamparli o salvarli nel gestore di password. Quando un codice viene usato, rimuovilo o marcalo come consumato. L&#8217;uso di <code>crypto.timingSafeEqual<\/code> non \u00e8 un dettaglio: il confronto a tempo costante impedisce a un attaccante di dedurre il codice carattere per carattere misurando i tempi di risposta. Per le basi dell&#8217;hashing sicuro vedi la nostra guida su <a href=\"\/it\/hashing-password-bcrypt-nodejs\/\">hashing delle password con bcrypt<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-9-rate-limiting-sulla-verifica-otp-contro-il-brute-force\">Step 9: Rate limiting sulla verifica OTP contro il brute force<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un codice a sei cifre ha un milione di combinazioni. Sembrano tante, ma senza limiti un attaccante con la password gi\u00e0 rubata pu\u00f2 tentarle tutte in poco tempo, soprattutto con una finestra <code>window<\/code> generosa che tiene validi pi\u00f9 codici insieme. Il <strong>rate limiting<\/strong> sull&#8217;endpoint di verifica \u00e8 obbligatorio, non opzionale. OWASP lo elenca tra i controlli fondamentali per l&#8217;autenticazione multifattore.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ nel server.js\nimport rateLimit from 'express-rate-limit';\n\nexport const limiteOtp = rateLimit({\n  windowMs: 15 * 60 * 1000, \/\/ 15 minuti\n  max: 5,                   \/\/ max 5 tentativi per IP+utente nella finestra\n  standardHeaders: true,\n  legacyHeaders: false,\n  message: { errore: 'Troppi tentativi. Riprova tra qualche minuto.' },\n  keyGenerator: (req) =&gt; `${req.ip}:${req.body?.userId ?? 'anon'}`,\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cinque tentativi ogni 15 minuti sono un punto di partenza ragionevole. Affianca al rate limiting un contatore persistente di fallimenti per utente: dopo N fallimenti consecutivi, blocca temporaneamente il 2FA e notifica l&#8217;utente via email, perch\u00e9 tentativi ripetuti sono spesso il segnale di un attacco in corso con credenziali gi\u00e0 compromesse. Lo stesso principio difensivo lo applichiamo nella nostra guida alla <a href=\"\/it\/protezione-csrf-nodejs\/\">protezione CSRF in Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-10-integrare-il-2fa-nel-flusso-di-login-con-express\">Step 10: Integrare il 2FA nel flusso di login con Express<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ora colleghiamo i pezzi. Il flusso corretto \u00e8 in due fasi: prima l&#8217;utente supera la verifica della password (primo fattore), poi viene messo in uno stato &#8220;in attesa di 2FA&#8221; e solo dopo aver fornito un TOTP valido riceve la sessione autenticata completa. Non emettere mai una sessione piena prima del secondo fattore.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js (estratto delle rotte 2FA)\nimport express from 'express';\nimport { verificaToken } from '.\/totp.js';\nimport { decifra } from '.\/crypto-store.js';\nimport { verificaCodiceBackup } from '.\/backup.js';\nimport { getUtente, consumaBackup } from '.\/db.js';\n\nconst app = express();\napp.use(express.json());\n\n\/\/ Fase 2: l'utente ha gia superato la password ed ha stato \"pending2fa\"\napp.post('\/2fa\/verifica', limiteOtp, (req, res) =&gt; {\n  const { userId, token } = req.body;\n  const u = getUtente(userId);\n  if (!u || !u.totpAttivo) return res.status(400).json({ errore: 'Setup non valido' });\n\n  const segreto = decifra(u.segretoCifrato);\n  if (verificaToken(token, segreto)) {\n    req.session = { userId, mfa: true }; \/\/ sessione piena\n    return res.json({ ok: true });\n  }\n\n  \/\/ Fallback: prova come codice di backup\n  for (const h of u.codiciBackup) {\n    if (verificaCodiceBackup(token, h)) {\n      consumaBackup(userId, h);\n      req.session = { userId, mfa: true };\n      return res.json({ ok: true, backupUsato: true });\n    }\n  }\n  return res.status(401).json({ errore: 'Codice non valido' });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Questo pattern di &#8220;sessione a due livelli&#8221; si integra bene con i token applicativi: se usi i JWT per le API, emetti il token finale solo dopo la verifica del secondo fattore. Approfondisci nella nostra guida all&#8217;<a href=\"\/it\/autenticazione-jwt-nodejs\/\">autenticazione JWT in Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-11-disattivare-rigenerare-e-gestire-il-ciclo-di-vita\">Step 11: Disattivare, rigenerare e gestire il ciclo di vita<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Un sistema 2FA non finisce all&#8217;attivazione. Devi gestire la disattivazione (che richiede sempre una conferma con un codice TOTP valido o la password, mai a clic libero), la rigenerazione del segreto se l&#8217;utente cambia dispositivo, e la rigenerazione dei codici di backup quando si esauriscono. Ogni operazione sensibile va registrata in un log di audit con timestamp e IP.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js (gestione ciclo di vita)\napp.post('\/2fa\/disattiva', limiteOtp, (req, res) =&gt; {\n  const { userId, token } = req.body;\n  const u = getUtente(userId);\n  const segreto = decifra(u.segretoCifrato);\n  if (!verificaToken(token, segreto)) {\n    return res.status(401).json({ errore: 'Conferma con un codice valido' });\n  }\n  disattiva2fa(userId);          \/\/ azzera segreto e codici di backup\n  logAudit(userId, '2fa_off', req.ip);\n  res.json({ ok: true });\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Punto critico spesso trascurato: durante l&#8217;enrollment, attiva il 2FA solo dopo che l&#8217;utente ha confermato un primo codice valido. Se imposti <code>totpAttivo = true<\/code> appena generi il segreto, senza verificare che l&#8217;utente lo abbia davvero importato, rischi di bloccarlo fuori dal proprio account perch\u00e9 ha scansionato male il QR. La sequenza corretta \u00e8: genera segreto, mostra QR, l&#8217;utente inserisce un codice, verifichi, e solo se \u00e8 valido attivi.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"step-12-il-progetto-completo-funzionante\">Step 12: Il progetto completo funzionante<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Mettiamo insieme i moduli in un <code>server.js<\/code> avviabile. Questa versione usa un archivio in memoria per chiarezza; in produzione sostituisci con <code>better-sqlite3<\/code> o il tuo database, mantenendo la cifratura del segreto a riposo dello Step 5.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ server.js (versione demo eseguibile)\nimport express from 'express';\nimport { authenticator } from 'otplib';\nimport QRCode from 'qrcode';\nimport { cifra, decifra } from '.\/crypto-store.js';\nimport { verificaToken } from '.\/totp.js';\nimport { generaCodiciBackup, hashCodice } from '.\/backup.js';\n\nconst app = express();\napp.use(express.json());\nconst utenti = new Map(); \/\/ demo: { segretoCifrato, totpAttivo, codiciBackup }\n\n\/\/ 1) Avvio enrollment: genera segreto e QR\napp.post('\/2fa\/setup', async (req, res) =&gt; {\n  const { userId, email } = req.body;\n  const segreto = authenticator.generateSecret(20);\n  const uri = authenticator.keyuri(email, 'ShatteredApp', segreto);\n  const qr = await QRCode.toDataURL(uri);\n  utenti.set(userId, { segretoCifrato: cifra(segreto), totpAttivo: false, codiciBackup: [] });\n  res.json({ qr, segreto }); \/\/ segreto in chiaro solo qui, come fallback manuale\n});\n\n\/\/ 2) Conferma enrollment: l'utente prova un codice\napp.post('\/2fa\/conferma', (req, res) =&gt; {\n  const { userId, token } = req.body;\n  const u = utenti.get(userId);\n  const segreto = decifra(u.segretoCifrato);\n  if (!verificaToken(token, segreto)) return res.status(401).json({ errore: 'Codice errato' });\n  const codici = generaCodiciBackup(10);\n  u.codiciBackup = codici.map(hashCodice);\n  u.totpAttivo = true;\n  res.json({ ok: true, codiciBackup: codici }); \/\/ mostrati una sola volta\n});\n\napp.listen(3000, () =&gt; console.log('2FA TOTP su http:\/\/localhost:3000'));<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Avvia con <code>TOTP_ENC_KEY=$(openssl rand -hex 32) node server.js<\/code>. Chiama <code>POST \/2fa\/setup<\/code>, scansiona il QR con la tua app, poi conferma con <code>POST \/2fa\/conferma<\/code>. Output atteso alla conferma: <code>{ \"ok\": true, \"codiciBackup\": [ ... ] }<\/code>. Hai un sistema 2FA TOTP funzionante end-to-end.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"errori-comuni-da-evitare-con-il-totp\">Errori comuni da evitare con il TOTP<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Questi sono i passi falsi che vediamo pi\u00f9 spesso e che, presi insieme, spiegano la quasi totalit\u00e0 dei sistemi 2FA difettosi in produzione.<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li><strong>Salvare il segreto in chiaro<\/strong> nel database. Cifralo sempre a riposo (Step 5): un dump del DB non deve compromettere il secondo fattore.<\/li><li><strong>Trattare il codice come numero intero<\/strong>, perdendo gli zero iniziali. Normalizza l&#8217;input come stringa con <code>trim()<\/code> prima della verifica.<\/li><li><strong>Finestra di tolleranza troppo ampia<\/strong>. Un <code>window<\/code> alto sembra comodo ma indebolisce la sicurezza moltiplicando i codici validi.<\/li><li><strong>Nessun rate limiting<\/strong> sull&#8217;endpoint di verifica. Senza throttling, un milione di combinazioni \u00e8 alla portata di uno script.<\/li><li><strong>Attivare il 2FA senza conferma<\/strong>. Se non verifichi un primo codice, rischi di bloccare fuori l&#8217;utente che ha sbagliato la scansione.<\/li><li><strong>Dimenticare i codici di backup<\/strong>. Senza recovery, ogni telefono perso \u00e8 un account perso e una richiesta di supporto.<\/li><li><strong>Cambiare algoritmo in <code>sha256<\/code><\/strong> assumendo che le app lo seguano. La maggior parte usa SHA-1: il codice non combacer\u00e0 mai.<\/li><li><strong>Server con orologio non sincronizzato<\/strong>. Senza NTP, TOTP fallisce anche con il codice corretto.<\/li><\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"risoluzione-dei-problemi-8-casi-frequenti\">Risoluzione dei problemi: 8 casi frequenti<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Quando il 2FA &#8220;non funziona&#8221;, il problema \u00e8 quasi sempre uno di questi otto. La tabella riporta sintomo, causa probabile e soluzione.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Sintomo<\/th><th>Causa probabile<\/th><th>Soluzione<\/th><\/tr><\/thead><tbody><tr><td>Il codice \u00e8 sempre rifiutato<\/td><td>Orologio del server desincronizzato<\/td><td>Attiva NTP (chrony\/systemd-timesyncd) e riprova<\/td><\/tr><tr><td>Funziona a intermittenza<\/td><td>Drift orario del telefono<\/td><td>Imposta <code>window: 1<\/code> e invita a sincronizzare l&#8217;ora del dispositivo<\/td><\/tr><tr><td>Codice che inizia per 0 fallisce<\/td><td>Codice trattato come intero<\/td><td>Gestisci il token come stringa, niente <code>parseInt<\/code><\/td><\/tr><tr><td>QR non scansionabile<\/td><td>URI otpauth:\/\/ malformato<\/td><td>Verifica issuer e codifica dei parametri con <code>keyuri()<\/code><\/td><\/tr><tr><td>Verifica sempre <code>false<\/code> nei test<\/td><td>Algoritmo impostato su sha256\/sha512<\/td><td>Torna a <code>algorithm: 'sha1'<\/code> salvo client dedicato<\/td><\/tr><tr><td>App mostra &#8220;account duplicato&#8221;<\/td><td>Enrollment ripetuto senza pulizia<\/td><td>Rigenera segreto e fai cancellare la voce vecchia<\/td><\/tr><tr><td>Errore di decifratura del segreto<\/td><td><code>TOTP_ENC_KEY<\/code> cambiata o assente<\/td><td>Usa la stessa chiave usata per cifrare; ruotala con re-cifratura<\/td><\/tr><tr><td>Troppi &#8220;Too Many Requests&#8221;<\/td><td>Rate limit troppo aggressivo<\/td><td>Calibra <code>max<\/code> e <code>windowMs<\/code>; separa la chiave per utente<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"tecniche-avanzate-per-il-totp-in-produzione\">Tecniche avanzate per il TOTP in produzione<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Una volta che il flusso base funziona, alcune migliorie distinguono un sistema giocattolo da uno robusto. <strong>Rotazione della chiave di cifratura<\/strong>: la <code>TOTP_ENC_KEY<\/code> va ruotata periodicamente; implementa una procedura che decifra con la vecchia chiave e ri-cifra con la nuova, mantenendo un identificatore di versione della chiave nel pacchetto cifrato.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Prevenzione del replay<\/strong>: RFC 6238 raccomanda di non accettare lo stesso codice due volte nella stessa finestra. Salva l&#8217;ultimo contatore di step verificato per utente e rifiuta un codice il cui step \u00e8 gi\u00e0 stato consumato. Questo blocca un attaccante che intercetta un codice e prova a riusarlo nei secondi successivi.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Notifiche di sicurezza<\/strong>: invia un&#8217;email all&#8217;utente a ogni attivazione, disattivazione o uso di un codice di backup. Un utente che riceve &#8220;il tuo 2FA \u00e8 stato disattivato&#8221; senza averlo fatto ha un segnale di compromissione immediato. <strong>Step-up authentication<\/strong>: richiedi un nuovo TOTP per le operazioni ad alto rischio (cambio email, esportazione dati) anche dentro una sessione gi\u00e0 autenticata.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Infine, monitora le metriche: tasso di fallimento delle verifiche, percentuale di utenti con 2FA attivo, uso dei codici di backup. Un picco di fallimenti su molti account contemporaneamente \u00e8 spesso il primo sintomo visibile di una campagna di credential stuffing in corso, un tema che approfondiamo nella sezione <a href=\"\/it\/security-hub\/\">sicurezza<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sicurezza-del-totp-phishing-aitm-e-i-limiti-reali\">Sicurezza del TOTP: phishing, AiTM e i limiti reali<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Va detto senza giri di parole: il TOTP alza moltissimo l&#8217;asticella rispetto alla sola password, ma non \u00e8 invincibile. Il vettore di attacco rilevante nel 2026 \u00e8 il phishing in tempo reale tramite proxy Adversary-in-the-Middle. L&#8217;utente viene attirato su un sito clone che inoltra in diretta credenziali e codice al sito vero. Poich\u00e9 il codice \u00e8 valido per circa 30-90 secondi, l&#8217;attaccante ha una finestra sufficiente per dirottare la sessione.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><p>Gli OTP basati sul tempo restano vulnerabili agli attacchi di phishing in tempo reale: un avversario che intercetta e ritrasmette il codice entro la sua finestra di validit\u00e0 pu\u00f2 aggirare il secondo fattore. Per questo le linee guida raccomandano gli autenticatori legati all&#8217;origine, come quelli FIDO2.<\/p><cite>Sintesi delle indicazioni di OWASP e NIST SP 800-63B<\/cite><\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Le contromisure pratiche: lega la sessione a fattori contestuali (impronta del dispositivo, IP, geolocalizzazione) e segnala accessi anomali; riduci la finestra di validit\u00e0 al minimo praticabile; e, soprattutto, offri le passkey come metodo primario per gli utenti che possono usarle. Il TOTP rimane il fallback universale ottimo, ma comunicare ai tuoi utenti che esistono alternative pi\u00f9 robuste \u00e8 parte di una strategia di autenticazione matura. Per il quadro generale sulla difesa delle credenziali, vedi la nostra guida alla <a href=\"\/it\/sicurezza-delle-password\/\">sicurezza delle password<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un&#8217;ultima nota sull&#8217;entropia: il segreto TOTP ha entropia fissa e il codice ha lunghezza fissa. Non aumentare artificiosamente le cifre oltre 6-8 pensando di renderlo pi\u00f9 sicuro; oltre un certo punto peggiori solo l&#8217;usabilit\u00e0 senza guadagni reali, perch\u00e9 la vera difesa contro il brute force \u00e8 il rate limiting, non la lunghezza del codice.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"domande-frequenti-sul-totp-in-node-js\">Domande frequenti sul TOTP in Node.js<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"devo-usare-otplib-o-speakeasy\">Devo usare otplib o speakeasy?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Per un progetto nuovo nel 2026 consigliamo otplib (versione 13.4.1), perch\u00e9 \u00e8 attivamente mantenuto e implementa correttamente RFC 6238 e RFC 4226. speakeasy \u00e8 fermo alla 2.0.0 da anni e non riceve aggiornamenti significativi: funziona ancora, ma non lo sceglieremmo per nuovo codice. Entrambi producono codici interoperabili con le app standard.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"il-totp-funziona-senza-connessione-internet\">Il TOTP funziona senza connessione internet?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">S\u00ec. Dopo l&#8217;enrollment, l&#8217;app dell&#8217;utente calcola i codici localmente usando solo il segreto e l&#8217;orologio del dispositivo. Non serve connessione n\u00e9 dal lato client n\u00e9 per la generazione. Il server, invece, deve essere raggiungibile e con l&#8217;ora sincronizzata per verificare il codice.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"perche-il-codice-viene-rifiutato-anche-se-sembra-giusto\">Perch\u00e9 il codice viene rifiutato anche se sembra giusto?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nel 90% dei casi \u00e8 un problema di orologio: server desincronizzato o telefono con ora errata. Attiva NTP sul server e imposta <code>window: 1<\/code> per assorbire piccole derive. Le altre cause frequenti sono il codice trattato come intero (perde lo zero iniziale) e un algoritmo impostato su SHA-256 mentre l&#8217;app usa SHA-1.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quanti-codici-di-backup-devo-generare\">Quanti codici di backup devo generare?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Dieci codici monouso sono lo standard adottato dalla maggior parte dei servizi. Conservali hashati nel database, mostrali all&#8217;utente una sola volta e consenti la rigenerazione (che invalida i vecchi) dalle impostazioni di sicurezza. Avvisa l&#8217;utente quando ne restano pochi.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"il-totp-e-sicuro-contro-il-phishing\">Il TOTP \u00e8 sicuro contro il phishing?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No, e va comunicato con onest\u00e0. Il TOTP protegge bene contro password rubate e database compromessi, ma un attacco Adversary-in-the-Middle in tempo reale pu\u00f2 intercettare e replicare il codice entro la sua finestra di validit\u00e0. Per la massima resistenza al phishing servono le passkey FIDO2\/WebAuthn. Il TOTP resta un ottimo secondo fattore universale e un fallback solido.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"posso-usare-lo-stesso-segreto-su-piu-dispositivi\">Posso usare lo stesso segreto su pi\u00f9 dispositivi?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Tecnicamente s\u00ec: scansionando lo stesso QR su pi\u00f9 telefoni, tutti genereranno gli stessi codici. Alcuni utenti lo fanno volutamente come backup. Dal lato server non cambia nulla. Sconsiglia per\u00f2 di condividere il segreto tra persone diverse: vanifica la funzione di possesso individuale del secondo fattore.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"quale-lunghezza-del-codice-scelgo-6-o-8-cifre\">Quale lunghezza del codice scelgo, 6 o 8 cifre?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sei cifre sono lo standard e la scelta giusta per la quasi totalit\u00e0 dei casi: \u00e8 ci\u00f2 che le app mostrano per impostazione predefinita. Otto cifre sono ammesse da RFC 6238 e aggiungono entropia, ma molte app non le supportano e l&#8217;usabilit\u00e0 peggiora. La difesa efficace contro il brute force resta il rate limiting, non le cifre extra.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"come-gestisco-la-rotazione-della-chiave-di-cifratura-del-segreto\">Come gestisco la rotazione della chiave di cifratura del segreto?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Aggiungi un identificatore di versione al pacchetto cifrato (per esempio <code>v2:iv:tag:ciphertext<\/code>). Quando ruoti la chiave, una procedura batch decifra con la vecchia chiave e ri-cifra con la nuova, aggiornando la versione. Cos\u00ec puoi ruotare senza disattivare il 2FA degli utenti e senza tempi di inattivit\u00e0.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"related-coverage\">Related Coverage<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"\/it\/hashing-password-bcrypt-nodejs\/\">Hashing Password con bcrypt in Node.js: 12 Step [2026]<\/a><\/li><li><a href=\"\/it\/autenticazione-jwt-nodejs\/\">Autenticazione JWT in Node.js: 12 Step [2026]<\/a><\/li><li><a href=\"\/it\/google-authenticator-vs-microsoft-vs-authy\/\">Google vs Microsoft vs Authy: 33M Esposti [2026]<\/a><\/li><li><a href=\"\/it\/protezione-csrf-nodejs\/\">Protezione CSRF in Node.js: 12 Step [2026]<\/a><\/li><li><a href=\"\/it\/crittografia-end-to-end-nodejs\/\">Crittografia End-to-End in Node.js: 12 Step [2026]<\/a><\/li><li><a href=\"\/it\/sicurezza-delle-password\/\">Sicurezza delle password: lunghezza, hashing e secondo fattore<\/a><\/li><\/ul>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"fonti-e-approfondimenti\">Fonti e approfondimenti<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc6238\" target=\"_blank\" rel=\"noopener\">RFC 6238: TOTP Time-Based One-Time Password Algorithm<\/a><\/li><li><a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc4226\" target=\"_blank\" rel=\"noopener\">RFC 4226: HOTP HMAC-Based One-Time Password Algorithm<\/a><\/li><li><a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Multifactor_Authentication_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">OWASP Multifactor Authentication Cheat Sheet<\/a><\/li><li><a href=\"https:\/\/pages.nist.gov\/800-63-3\/sp800-63b.html\" target=\"_blank\" rel=\"noopener\">NIST SP 800-63B Digital Identity Guidelines<\/a><\/li><li><a href=\"https:\/\/github.com\/yeojz\/otplib\" target=\"_blank\" rel=\"noopener\">otplib su GitHub (documentazione ufficiale)<\/a><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Ultimo aggiornamento: 14 giugno 2026. Le versioni dei pacchetti sono verificate alla data di pubblicazione; controlla sempre l&#8217;ultima release su npm prima di mettere in produzione.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>L&#8217;autenticazione a due fattori basata su TOTP (Time-based One-Time Password) \u00e8 oggi lo standard minimo per proteggere un account contro il furto di credenziali. Quando un utente inserisce le sei\u2026<\/p>\n","protected":false},"author":6,"featured_media":134,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-133","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\/133","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\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/comments?post=133"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/133\/revisions"}],"predecessor-version":[{"id":135,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/posts\/133\/revisions\/135"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media\/134"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/media?parent=133"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/categories?post=133"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/it\/wp-json\/wp\/v2\/tags?post=133"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}