Come funziona Google Authenticator

Un corso con il codice davanti. Alla fine avrai due pagine funzionanti — registrazione e login a due fattori — e saprai esattamente cosa succede dietro quelle sei cifre che cambiano ogni trenta secondi.

Il server, in questo momento
Unix time
Finestra T
Scade tra

Quel numero Finestra T è il cuore di tutto: è ciò che il tuo telefono e il server calcolano nello stesso istante senza scambiarsi nulla.

00Il problema che risolve

Una password è un segreto che viaggia. Ogni volta che accedi la mandi al server: se qualcuno la intercetta, la ruba da un database o te la fa scrivere su un sito falso, l'ha per sempre. Non ha scadenza.

Il secondo fattore serve a rompere proprio questa proprietà. L'obiettivo è dimostrare "possiedo il telefono" con qualcosa che:

TOTP fa esattamente questo, ed è definito da due specifiche pubbliche: RFC 4226 (HOTP, il meccanismo di base) e RFC 6238 (TOTP, la variante basata sul tempo). "Google Authenticator" è solo un'app che implementa quello standard: Aegis, 1Password, Bitwarden, Authy fanno la stessa identica cosa e sono intercambiabili.

01L'idea in tre righe

Il meccanismo, tutto qui Il telefono e il server condividono un solo segreto, scambiato una volta sola (il QR code). Poi non si parlano mai più. Entrambi guardano l'orologio, calcolano in quale finestra da 30 secondi si trovano, e passano (segreto + numero di finestra) nella stessa funzione matematica. Se le sei cifre coincidono, chi le ha digitate possiede il segreto.

Nota cosa non succede: il codice non viene mai inviato dal server al telefono. Non c'è niente da intercettare in transito, perché non transita. Viene calcolato in due posti diversi, in parallelo, a partire dagli stessi due ingredienti.

I due fattori del login sono di natura diversa, ed è questo che li rende utili insieme:

Primo fattoreSecondo fattore
Cos'èqualcosa che saiqualcosa che possiedi
Nella demola passwordil codice dell'app
Viaggiaa ogni loginuna volta sola, nel QR
Scademaiogni 30 secondi
Sul serverhash scrypt irreversibilesegreto in chiaro (serve per ricalcolare)

02Passo 1 — La password, primo fattore

Il TOTP da solo non è autenticazione a due fattori: è autenticazione a un fattore fatta diversamente. Serve anche la password — e va gestita bene, altrimenti il secondo fattore protegge una porta già aperta.

Una password non si salva mai in chiaro, e nemmeno con un hash veloce come SHA-256: una GPU ne prova miliardi al secondo. Serve una funzione deliberatamente lenta e affamata di memoria. Qui si usa scrypt, già dentro Node.

lib/password.js
const crypto = require('crypto');

function hashPassword(plain) {
  const salt = crypto.randomBytes(16);                    // diverso per ogni utente
  const derived = crypto.scryptSync(String(plain), salt, 64);
  return `scrypt$${salt.toString('hex')}$${derived.toString('hex')}`;
}

Il salt casuale fa sì che due utenti con la stessa password producano hash diversi: le rainbow table precalcolate diventano inutili. Salt e hash stanno nella stessa stringa, perché per verificare servono entrambi.

lib/password.js
function verifyPassword(plain, stored) {
  const [algo, saltHex, hashHex] = String(stored).split('$');
  const salt = Buffer.from(saltHex, 'hex');
  const expected = Buffer.from(hashHex, 'hex');
  const derived = crypto.scryptSync(String(plain), salt, expected.length);
  return crypto.timingSafeEqual(derived, expected);   // confronto a tempo costante
}
Alternative argon2id è oggi la raccomandazione preferita da OWASP (richiede il pacchetto argon2); bcrypt resta diffusissimo e valido. scrypt è la scelta di questa demo perché non aggiunge dipendenze.

03Passo 2 — Il segreto TOTP

È una sequenza di byte casuali. RFC 4226 §4 dice: minimo 128 bit, raccomandati 160, cioè 20 byte, da un generatore crittografico — non da Math.random().

lib/totp.js
const crypto = require('crypto');

const SECRET_BYTES = 20; // 160 bit — raccomandazione RFC 4226 §4

function generateSecret() {
  return base32Encode(crypto.randomBytes(SECRET_BYTES));
}

Perché Base32 e non esadecimale o Base64

Il segreto deve poter essere digitato a mano quando la fotocamera non funziona o l'app è su un altro dispositivo. L'alfabeto Base32 (RFC 4648) è A–Z più 2–7: niente minuscole, niente 0, 1, 8. Sparisce ogni ambiguità fra O e 0, fra l e 1. Base64 avrebbe metà di questi problemi e in più + e /, impossibili da dettare al telefono.

lib/totp.js — la codifica, bit per bit
const B32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

function base32Encode(buffer) {
  let bits = 0;    // quanti bit "in attesa" ho accumulato
  let value = 0;   // il magazzino di bit
  let output = '';
  for (const byte of buffer) {
    value = (value << 8) | byte;   // aggiungo 8 bit al magazzino
    bits += 8;
    while (bits >= 5) {            // ogni 5 bit disponibili emetto un carattere
      output += B32_ALPHABET[(value >>> (bits - 5)) & 31];
      bits -= 5;
    }
  }
  if (bits > 0) output += B32_ALPHABET[(value << (5 - bits)) & 31];
  return output;
}

Ogni carattere Base32 trasporta 5 bit. 20 byte × 8 bit ÷ 5 = 32 caratteri: è la lunghezza del segreto che vedrai nella pagina di registrazione.

Errore classico Usare Base64 al posto di Base32, o lasciare il padding =. Il risultato è "il codice è sempre sbagliato" senza nessun messaggio d'errore utile, perché l'app decodifica byte diversi da quelli del server.

04Passo 3 — Il tempo

Il secondo ingrediente non viaggia affatto: è l'orologio. RFC 6238 lo formalizza così:

T = floor((tempo_unix_corrente - T0) / X)

// T0 = 0  -> si parte dall'epoch Unix (1 gennaio 1970)
// X  = 30 -> ogni finestra dura 30 secondi
lib/totp.js
const PERIOD = 30;
const T0 = 0;

function timeCounter(unixSeconds = Math.floor(Date.now() / 1000), period = PERIOD) {
  return Math.floor((unixSeconds - T0) / period);
}

function secondsRemaining(unixSeconds = Math.floor(Date.now() / 1000), period = PERIOD) {
  return period - ((unixSeconds - T0) % period);
}

Il valore mostrato in cima alla pagina è proprio questo T. Il tuo telefono, adesso, sta calcolando lo stesso identico numero. È l'unica cosa che i due dispositivi "si dicono", e se la dicono senza parlarsi.

La conseguenza pratica Se l'orologio del server o del telefono sbanda di più di 30 secondi, nessun codice sarà mai valido. In produzione NTP non è un dettaglio: è un requisito. Nella pagina di login trovi un controllo del disallineamento fra il tuo browser e il server, proprio per diagnosticare questo caso.

05Passo 4 — Da segreto + tempo a sei cifre

Qui sta il cuore, definito da RFC 4226 §5.3. Quattro passaggi.

4.1 — Il contatore diventa 8 byte

Big-endian, sempre 8 byte. Con 4 byte "funziona" fino al giorno dell'overflow: è un bug che si presenta anni dopo il rilascio.

const counterBuf = Buffer.alloc(8);
counterBuf.writeBigUInt64BE(BigInt(counter));

4.2 — HMAC-SHA1

È qui che entra il segreto. HMAC è una funzione a senso unico con chiave: dallo stesso (chiave, messaggio) esce sempre lo stesso hash, ma dall'hash non si risale alla chiave. Chi non ha il segreto non può produrre l'output giusto.

const hs = crypto.createHmac('sha1', key).update(counterBuf).digest(); // 20 byte
Perché SHA1, che è considerato debole? SHA1 è rotto per la resistenza alle collisioni (trovare due input con lo stesso hash), proprietà che qui non serve. HMAC-SHA1 come primitiva di autenticazione resta solido, e non esiste attacco pratico noto contro TOTP-SHA1. È rimasto perché Google Authenticator ignora il parametro algorithm: usare SHA256 romperebbe la compatibilità con l'app più diffusa. Le RFC ammettono SHA256 e SHA512, e altre app li supportano.

4.3 — Dynamic truncation: da 20 byte a 31 bit

Servono 6 cifre, non 160 bit. Ma non si prendono semplicemente i primi byte: si usa l'hash stesso per decidere da dove leggere.

// i 4 bit bassi dell'ULTIMO byte dicono da dove iniziare a leggere (0..15)
const offset = hs[hs.length - 1] & 0x0f;

const binary =
    ((hs[offset]     & 0x7f) << 24) |   // 0x7f azzera il bit più alto
    ((hs[offset + 1] & 0xff) << 16) |
    ((hs[offset + 2] & 0xff) <<  8) |
     (hs[offset + 3] & 0xff);

Due dettagli che sembrano arbitrari e non lo sono:

4.4 — Modulo e padding

const code = String(binary % 10 ** 6).padStart(6, '0');

Lo zero-padding non è cosmetico: senza, un risultato come 42 diventerebbe un codice di due cifre e il confronto fallirebbe.

Mettendo insieme tutto — TOTP = HOTP(segreto, T):

lib/totp.js
function generateTOTP(secretBase32, { at = Math.floor(Date.now() / 1000) } = {}) {
  return hotp(secretBase32, timeCounter(at));
}

Sono venti righe. Non c'è nient'altro.

06Passo 5 — Consegnare il segreto al telefono

Il QR code non è magia e non contiene l'algoritmo: contiene una stringa di testo, in un formato definito da Google e diventato standard di fatto — il Key URI Format.

otpauth://totp/Demo%20TOTP:giuseppe?secret=JBSWY3DPEHPK3PXP&issuer=Demo%20TOTP&algorithm=SHA1&digits=6&period=30
ParametroObbligoValoriDefault
secretrichiestoBase32 senza padding
issuerfortemente raccomandatonome del servizio
algorithmopzionaleSHA1 · SHA256 · SHA512SHA1
digitsopzionale6 · 86
periodopzionalesecondi30
La trappola più costosa La documentazione ufficiale di Google dice che algorithm, digits e period sono ignorati da Google Authenticator. Se generi un URI con digits=8, funziona su Aegis e 1Password e si rompe su Google Authenticator senza errori: mostra semplicemente codici che il server rifiuta. Per la compatibilità con GA: SHA1, 6 cifre, 30 secondi. Sempre.
lib/totp.js
function buildOtpauthURI({ secret, account, issuer }) {
  if (String(account).includes(':') || String(issuer).includes(':')) {
    throw new Error('issuer e account non possono contenere il carattere ":"');
  }
  const label = `${encodeURIComponent(issuer)}:${encodeURIComponent(account)}`;
  // NB: encodeURIComponent, NON URLSearchParams: quest'ultimo codifica lo spazio
  // come "+" (form-encoding) mentre otpauth richiede "%20" (RFC 3986).
  const params = [
    ['secret', secret], ['issuer', issuer],
    ['algorithm', 'SHA1'], ['digits', '6'], ['period', '30'],
  ].map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
  return `otpauth://totp/${label}?${params}`;
}

E il disegno del QR è una riga, con la libreria qrcode:

server.js
const QRCode = require('qrcode');

const qrDataUrl = await QRCode.toDataURL(otpauthUri, { margin: 2, width: 260 });
// -> "data:image/png;base64,..." da mettere direttamente in <img src>
Provalo Nella pagina di registrazione l'URI completo è mostrato sotto il QR, in chiaro. Leggilo: capirai che scansionare il QR e digitare il segreto a mano sono letteralmente la stessa operazione.

07Passo 6 — La registrazione

Due chiamate, e la seconda è quella che quasi tutti i tutorial saltano.

6.1 — Credenziali, segreto, QR

server.js
app.post('/api/register/start', async (req, res) => {
  const passwordHash = pwd.hashPassword(password);   // primo fattore, subito in hash
  const secret = totp.generateSecret();              // secondo fattore, creato UNA volta
  store.createPending(username, passwordHash, secret);   // salvato come NON confermato

  const otpauthUri = totp.buildOtpauthURI({ secret, account: username, issuer: ISSUER });
  const qrDataUrl = await QRCode.toDataURL(otpauthUri, { margin: 2, width: 260 });

  res.json({ secret, otpauthUri, qrDataUrl });
});

6.2 — Conferma prima di attivare

Il passo cruciale Il segreto è salvato come non confermato. Diventa attivo solo dopo che l'utente ha digitato un primo codice corretto — cioè dopo aver dimostrato di aver scansionato bene il QR. Senza questo passaggio, una scansione fallita chiude l'utente fuori dal proprio account per sempre.
server.js
app.post('/api/register/confirm', (req, res) => {
  const user = store.get(req.body.username);
  const esito = totp.verifyTOTP(user.secret, req.body.token);
  if (!esito.valid) return res.status(401).json({ error: esito.reason });

  store.update(user.username, {
    confirmed: true,                    // solo ora il 2FA è attivo
    lastUsedCounter: esito.counter,     // anti-replay, vedi sotto
  });
  res.json({ ok: true });
});

08Passo 7 — Il login a due fattori

Il login è in due chiamate separate, apposta: rende visibile che il secondo fattore si verifica dopo il primo, e che superare la password da sola non autentica nessuno.

7.1 — Primo fattore: la password

Se è corretta, il server non emette una sessione: emette un challenge, una sessione parziale a scadenza breve che dà solo il diritto di presentare il secondo fattore.

server.js
app.post('/api/login/password', (req, res) => {
  const user = store.get(username);
  const generico = { error: 'Username o password non validi.' };

  if (!user || !user.confirmed) {
    // hash fittizio: senza, il tempo di risposta rivelerebbe se l'utente esiste
    pwd.verifyPassword(password, pwd.hashPassword('placeholder'));
    return res.status(401).json(generico);
  }
  if (!pwd.verifyPassword(password, user.passwordHash)) {
    registraFallimento(user, Date.now());
    return res.status(401).json(generico);
  }

  const challenge = nuovoChallenge(user.username);   // NON è una sessione autenticata
  res.json({ ok: true, challenge });
});
Due difese in dieci righe Il messaggio d'errore è identico per "utente inesistente" e "password sbagliata", e nel primo caso si calcola comunque un hash inutile. Altrimenti, o il testo o il tempo di risposta rivelerebbero quali username esistono — user enumeration.

7.2 — Secondo fattore: la finestra di tolleranza

Fra quando l'app mostra il codice e quando il server lo riceve passano secondi: l'utente legge, digita, la rete ritarda. Se il codice scade nel frattempo, il login fallisce pur essendo corretto. La soluzione è accettare anche le finestre adiacenti — ±1, non di più, come raccomanda RFC 6238 §5.2.

lib/totp.js
const current = timeCounter(at);
for (let drift = -window; drift <= window; drift++) {   // window = 1
  const counter = current + drift;
  const { code } = hotp(secretBase32, counter);
  if (timingSafeEqualStr(code, candidate)) { /* valido */ }
}

Allargare la finestra è una tentazione: con ±10 quasi nessun login fallisce mai. Ma significa accettare 21 codici validi in ogni istante invece di 3 — sette volte più facile da indovinare. La finestra è un compromesso di sicurezza, non una manopola del comfort.

7.3 — Anti-replay

Un codice resta valido fino a 90 secondi (finestra ±1). Chi lo intercetta — spalle, keylogger, sito di phishing — ha quella finestra per riusarlo. Si chiude memorizzando l'ultimo contatore consumato:

if (lastUsedCounter !== null && counter <= lastUsedCounter) {
  return { valid: false, reason: 'Codice già utilizzato: attendi il prossimo.' };
}

È il motivo per cui, subito dopo la registrazione, il login con lo stesso codice viene rifiutato: quella finestra è già stata bruciata dalla conferma. Provalo, è istruttivo.

7.4 — Confronto a tempo costante

Un normale === fra stringhe esce al primo carattere diverso. Misurando i tempi di risposta si scopre quante cifre iniziali erano giuste, e si ricostruisce il codice cifra per cifra: 60 tentativi invece di un milione.

function timingSafeEqualStr(a, b) {
  const bufA = Buffer.from(a, 'utf8'), bufB = Buffer.from(b, 'utf8');
  if (bufA.length !== bufB.length) return false;
  return crypto.timingSafeEqual(bufA, bufB);   // tempo indipendente dal contenuto
}

7.5 — Throttling

Sei cifre sono un milione di possibilità: senza limiti, uno script le prova tutte. RFC 4226 §7.3 chiede un limite ai tentativi falliti.

server.js
const MAX_TENTATIVI = 5, BLOCCO_SECONDI = 60;

if (user.lockedUntil && Date.now() < user.lockedUntil) {
  return res.status(429).json({ error: 'Troppi tentativi falliti.' });
}
// a ogni fallimento incrementa failedAttempts; al quinto imposta lockedUntil

09Consegnare il segreto in sicurezza

Tutto quello che hai letto finora descrive l'algoritmo. Ma il momento in cui il meccanismo e' davvero fragile e' un altro: l'enrollment, cioe' il passaggio del segreto dal server al telefono. E' l'unico istante in cui quel valore esiste in chiaro fuori dal database.

Il principio che regge tutto L'enrollment non e' un'impostazione, e' un evento di autenticazione. NIST SP 800-63B (revisione 4, agosto 2025) lo formalizza: collegare un nuovo autenticatore richiede “an authenticated session during which the subscriber SHALL authenticate at the AAL of the account”.

9.1 — Richiedi di nuovo la password, anche a sessione aperta

E' la regola piu' importante di tutte, ed e' quella che viene saltata piu' spesso. Chi trova il tuo computer sbloccato e collega il proprio telefono diventa lui il secondo fattore: da quel momento ha una credenziale legittima e permanente. Nessuna crittografia e' stata toccata.

app.post('/api/2fa/enroll', requireAuth, requireRecentPassword, async (req, res) => {
  // requireRecentPassword: la password e' stata reinserita negli ultimi N minuti.
  // Vale anche per DISATTIVARE il 2FA: senza, si aggira tutto dal pannello impostazioni.
});

9.2 — Il segreto vive in uno stato provvisorio

Non entra nel record utente attivo finche' l'utente non ha dimostrato di averlo ricevuto. Sta in uno stato pending legato alla sessione, con scadenza breve. Le specifiche non fissano un TTL: 10–15 minuti e' pratica di settore.

9.3 — Mostrato una volta, poi distrutto

“Mostrato una volta” senza distruzione e' solo una promessa dell'interfaccia. Dopo la conferma il segreto va rimosso davvero, e non deve restare in nessuna cache.

Qui c'e' pero' un'asimmetria da capire bene: il backend non puo' distruggere il segreto. Gli serve per sempre, per ricalcolare il codice a ogni login. E' l'opposto della password, che dopo l'hash e' irrecuperabile per costruzione. La regola precisa e' quindi:

Una sola copia autoritativa, cifrata a riposo. Tutte le altre vanno distrutte. Pending, buffer, log, cache del browser, PNG del QR: ogni copia che non sia quella cifrata nel database e' superflua e va eliminata.

Nel frontend

function distruggiSegreto() {
  const qr = document.getElementById('qr');
  qr.src = '';                          // svuota prima di rimuovere
  qr.remove();
  document.getElementById('secret').remove();
  document.getElementById('uri').remove();
  segretoInMemoria = null;              // vedi il limite qui sotto
}
Un limite reale, da non illudersi In JavaScript non si puo' cancellare davvero una stringa dalla memoria: le stringhe sono immutabili e il garbage collector non e' deterministico. variabile = null rimuove il riferimento, non il contenuto. Quello che ottieni e' ridurre la finestra e impedire la persistenza, non l'azzeramento. Per azzerare sul serio servono Uint8Array/Buffer riempiti di zeri (buf.fill(0)), e vale sia nel browser sia in Node.

Nel backend

res.set('Cache-Control', 'no-store');   // NON 'private': quello consente comunque
res.set('Pragma', 'no-cache');          // la cache del browser, e la bfcache

9.4 — Subito dopo l'attivazione

9.5 — Cifratura a riposo

Il segreto TOTP e' l'unico dei due fattori che il server deve conservare in forma utilizzabile. Chi legge quel campo genera codici validi a tempo indeterminato. Va cifrato con AES-GCM e chiave in un KMS o in variabile d'ambiente — mai nel codice, mai nello stesso posto del dato cifrato.

Dove sta questa demo

PraticaQui
Generazione server-side, 160 bitsi'
Consegna su HTTPSsi'
Conferma prima dell'attivazionesi'
Stato pending separato dall'attivosi'
Throttling dei tentativisi' (5, contro il tetto NIST di 100)
no-store sulla risposta di enrollmentsi'
Distruzione del segreto nel DOM dopo la confermasi'
Re-autenticazione prima dell'enrollmentno — la registrazione e' aperta a chiunque
Visualizzazione una sola voltano — il pannello didattico lo ri-mostra di proposito
Cifratura a riposono — in chiaro, per poterlo mostrare
Codici di recuperono

Le prime tre assenze non sono dimenticanze: sono il prezzo consapevole di una demo che deve far vedere il meccanismo. In un'applicazione vera sono obbligatorie.

10Le librerie usate

PacchettoA cosa serveSostituibile con
expressserver HTTP e routingil modulo http di Node, Fastify, Koa
qrcodedisegna l'URI otpauth come PNGqrcode-generator, o generazione lato browser
node:cryptoHMAC-SHA1, byte casuali, scrypt, confronto a tempo costanteè già in Node, nessuna dipendenza

Due dipendenze in tutto. L'algoritmo TOTP è scritto a mano, in lib/totp.js: è il senso di questo corso. In un progetto vero useresti una libreria collaudata:

// npm install otplib
const { authenticator } = require('otplib');

const secret = authenticator.generateSecret();
const token  = authenticator.generate(secret);
const ok     = authenticator.check(token, secret);
const uri    = authenticator.keyuri('giuseppe', 'Demo TOTP', secret);

Alternative equivalenti: speakeasy (storica, meno mantenuta), @noble/hashes più poche righe (per chi vuole dipendenze minime), pyotp in Python, otp-java in Java.

Quando scrivere e quando usare una libreria Per capire: scrivilo. Per spedire: usa la libreria — non perché l'algoritmo sia difficile (l'hai appena letto), ma perché una libreria diffusa ha già incontrato i casi limite, gli errori di codifica e i problemi di interoperabilità con le app reali.

11Verificare che l'implementazione sia giusta

Un'implementazione TOTP può sembrare funzionante e non essere interoperabile: genera codici coerenti con sé stessa, ma diversi da quelli dell'app. Le RFC pubblicano vettori di test ufficiali proprio per questo — se passano, il codice è compatibile con qualunque autenticatore.

test/totp.test.js
// RFC 4226 Appendice D — segreto ASCII "12345678901234567890"
const HOTP_VECTORS = ['755224','287082','359152','969429','338314',
                      '254676','287922','162583','399871','520489'];

HOTP_VECTORS.forEach((atteso, counter) => {
  assert.strictEqual(totp.hotp(SECRET_B32, counter).code, atteso);
});

// RFC 6238 Appendice B — TOTP, SHA1, 8 cifre
assert.strictEqual(totp.generateTOTP(SECRET_B32, { at: 59,         digits: 8 }).code, '94287082');
assert.strictEqual(totp.generateTOTP(SECRET_B32, { at: 1111111109, digits: 8 }).code, '07081804');
$ npm test

HOTP — RFC 4226, Appendice D (contatori 0..9)
  ok counter=0 -> 755224
  ...
29 test superati, 0 falliti
Cosa ti dice questo risultato Che lib/totp.js produce esattamente i valori pubblicati nelle specifiche. Non "sembra funzionare": è dimostrabilmente conforme.

12Gli errori che si fanno davvero

SintomoCausa
Il codice è sempre sbagliato, da subitoSegreto in Base64 invece che Base32, o padding = non rimosso
Funzionava, poi ha smessoOrologio del server o del telefono fuori sincrono (> 30 s). Serve NTP
Funziona su Aegis, non su Google Authenticatordigits/period/algorithm non standard: GA li ignora
L'utente resta chiuso fuori dopo la registrazioneSegreto attivato senza il passo di conferma
Il codice smette di valere dopo anniContatore serializzato su 4 byte invece di 8
L'issuer appare come Demo+TOTPURLSearchParams al posto di encodeURIComponent
Login forzabile a tentativiFinestra di validazione troppo ampia, o throttling assente
Si scopre quali username esistonoMessaggi d'errore diversi, o tempi di risposta diversi

13Cosa manca a questa demo

Quello che hai in mano è completo dal punto di vista dell'algoritmo e incompleto come applicazione. Per un uso reale servirebbe:

Vale anche la pena sapere che TOTP non protegge dal phishing in tempo reale: un sito falso può farsi dare password e codice e usarli entro 30 secondi. Le passkey / WebAuthn risolvono anche quello, perché legano la credenziale al dominio. TOTP resta però il compromesso migliore fra sicurezza, costo e compatibilità universale.

14Ora provalo

Le due pagine mostrano dal vivo tutto ciò che hai letto. Nella registrazione vedrai il segreto e l'URI in chiaro; nel login un pannello ricalcola il codice atteso passo per passo — HMAC compreso — così puoi confrontarlo con quello che mostra il telefono.

Credenziali di test
usernamegiuseppe
password!CiaoMamma123!
secondo fattoreil codice dalla tua app authenticator

 

Fonti: RFC 6238 (TOTP) · RFC 4226 (HOTP) · Google Authenticator Key Uri Format. Sintesi verificata in docs/research/totp-google-authenticator.md.