OTP via email con Netlify Function: hash, salt e verifica timing-safe
Generare e verificare un codice OTP a 6 cifre via email, senza Firebase Authentication —
hash+salt mai in chiaro, scadenza 5 minuti, confronto a tempo costante e limite tentativi.
Email OTP with a Netlify Function: hashing, salting and timing-safe verification
Generate and verify a 6-digit OTP code via email, without Firebase Authentication —
hash+salt never stored in plaintext, 5-minute expiry, constant-time comparison and attempt limit.
Perché non Firebase Authentication
Se gli utenti sono già identificati nell'app con il proprio account, un secondo livello "ufficiale" di
autenticazione spesso non serve. Firebase Phone Auth ha inoltre un costo non banale sugli SMS italiani
e richiede configurazione reCAPTCHA. Una Netlify Function con OTP via email è più semplice e a costo zero.
Why not Firebase Authentication
If users are already identified in the app with their own account, a second "official" authentication
layer is often unnecessary. Firebase Phone Auth also has a non-trivial cost for Italian SMS and requires
reCAPTCHA setup. A Netlify Function with email OTP is simpler and zero-cost.
Generazione: mai salvare il codice in chiaro
Si genera il codice a 6 cifre, lo si combina con un salt casuale, e si salva solo l'hash —
mai il codice stesso — con una scadenza di 5 minuti.
Generation: never store the plaintext code
Generate the 6-digit code, combine it with a random salt, and store only the hash —
never the code itself — with a 5-minute expiry.
// Genera codice a 6 cifre
const otp = String(crypto.randomInt(100000, 999999));
const salt = crypto.randomBytes(16).toString('hex');
const otpHash = crypto.createHash('sha256')
.update(otp + salt)
.digest('hex');
const scadenza = Date.now() + 5 * 60 * 1000; // 5 minuti
// Salva solo hash+salt, mai il codice in chiaro
await salvaRecord(`otp/${userId}`, {
otpHash, salt, scadenza,
tentativi: 0,
stato: 'in_attesa'
});
// otp viene inviato via email — mai loggato, mai salvato
Verifica: timing-safe comparison e limite tentativi
La verifica usa crypto.timingSafeEqual per confrontare gli hash, evitando timing attack dove
un confronto stringa normale potrebbe rivelare informazioni sulla lunghezza della corrispondenza.
Dopo 5 tentativi falliti il record viene invalidato.
Verification: timing-safe comparison and attempt limit
Verification uses crypto.timingSafeEqual to compare hashes, avoiding timing attacks where a
normal string comparison could leak information about match length. After 5 failed attempts the record
is invalidated.
const rec = await leggiRecord(`otp/${userId}`);
// Controllo scadenza
if (Date.now() > rec.scadenza) {
return err(400, 'Codice scaduto');
}
// Controllo max tentativi
if (rec.tentativi >= 5) {
return err(400, 'Troppi tentativi');
}
// Timing-safe compare — evita timing attack
const hashInput = crypto.createHash('sha256').update(codice + rec.salt).digest('hex');
const bufInput = Buffer.from(hashInput, 'hex');
const bufStored = Buffer.from(rec.otpHash, 'hex');
if (bufInput.length !== bufStored.length ||
!crypto.timingSafeEqual(bufInput, bufStored)) {
await salvaRecord(key, { ...rec, tentativi: rec.tentativi + 1 });
return err(401, `Codice errato. Tentativi rimasti: ${4 - rec.tentativi}`);
}
// OTP corretto — cattura IP server-side e registra esito
const ip = event.headers['x-forwarded-for']?.split(',')[0].trim() || 'sconosciuto';
Perché l'IP server-side: il client non conosce il proprio IP pubblico in modo affidabile. Va letto dall'header x-forwarded-for al momento della verifica, lato server, garantendo che sia autentico e non manipolabile dal client.
Why capture IP server-side: the client can't reliably know its own public IP. It must be read from the x-forwarded-for header at verification time, server-side, ensuring it's authentic and not manipulable by the client.
Terminologia legale: questo è un approccio di firma elettronica semplice con audit trail (nome, timestamp, IP, hash documento, OTP verificato) — non una "firma digitale" nel senso legale italiano (firma elettronica qualificata, rilasciata da certificatore accreditato). Usa il termine corretto nell'interfaccia per evitare aspettative legali non corrispondenti.
Legal terminology: this is a simple electronic signature with audit trail approach (name, timestamp, IP, document hash, verified OTP) — not a legally qualified "digital signature" issued by an accredited certifier. Use the correct term in the UI to avoid mismatched legal expectations.
Articolo correlato
Firma elettronica con OTP su Firebase — audit trail, hash PDF e certificato con pdf-lib
Related article
Electronic signature with OTP on Firebase — audit trail, PDF hash and pdf-lib certificate
→