← blog
Node.js Netlify Functions Senza Firebase Auth Sicurezza

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.

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.

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.

netlify/functions/otp.js — generazione
// 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.

netlify/functions/otp.js — verifica timing-safe
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.

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.

Articolo correlato
Firma elettronica con OTP su Firebase — audit trail, hash PDF e certificato con pdf-lib