Analisi

Consegne firmate, e un’email che non dice nulla.

Un webhook invia ogni nuova risposta a un URL https scelto da voi. Ogni consegna porta un’intestazione X-NumoForms-Signature: sha256= seguito da un HMAC-SHA256 del corpo esatto della richiesta, calcolato con un segreto univoco per quel webhook, così il vostro endpoint può dimostrare che il payload viene da noi. Separatamente, gli indirizzi email che indicate possono essere avvisati quando arriva una risposta — e quelle email, deliberatamente, non contengono alcuna risposta.

Che cosa arriva al vostro endpoint

Un solo POST JSON per risposta. Il corpo contiene cinque campi e nulla più, così potete scriverci un parser senza andare a indovinare.

CampoChe cosa contiene
eventresponse.created
survey_idIl questionario a cui appartiene la risposta.
response_idL’identificativo univoco di questa risposta.
created_atQuando la risposta è stata registrata.
answersLe risposte, indicizzate per identificativo di domanda.
IntestazioneA che cosa serve
X-NumoForms-EventIl nome dell’evento. Oggi ce n’è uno: response.created.
X-NumoForms-Signaturesha256= seguito dall’HMAC-SHA256 del corpo esatto della richiesta, in esadecimale.
X-NumoForms-DeliveryUn identificativo univoco di questo tentativo di consegna, per riconoscere un duplicato.

La consegna è innescata da un trigger di database sulla tabella delle risposte, non da una determinata schermata dell’applicazione. Conta più di quanto sembri: una risposta inviata da un modulo incorporato, da un link ospitato o da una parziale ripresa giorni dopo con salva e riprendi percorre la stessa strada, quindi non esiste un percorso che ci siamo dimenticati di collegare.

Il webhook parte quando la risposta arriva. Se avete attivato le approvazioni, non attende la decisione di approvazione o rifiuto, e quando quella decisione viene presa non parte una seconda consegna. Trattate un payload in arrivo come «esiste una risposta», non come «una risposta è stata approvata».

Perché la firma non è facoltativa

L’URL di un webhook è un segreto destinato a trapelare. Finisce in un ticket, in un messaggio su Slack, in una schermata dentro un documento di passaggio di consegne, nella cronologia di un portatile condiviso. Senza una firma, chiunque abbia visto quell’URL può inviarci ciò che vuole, e i vostri sistemi lo archivieranno come una risposta autentica alla consultazione. Per un comune che conta le osservazioni a una pratica edilizia, non è un problema teorico.

Per questo ogni consegna è firmata. Ogni webhook riceve un proprio segreto generato in modo casuale, mostrato nell’editor dietro un comando «mostra il segreto di firma». Noi calcoliamo un HMAC-SHA256 sui byte esatti del corpo della richiesta usando quel segreto; voi lo ricalcolate con la vostra copia e lo confrontate. Una richiesta contraffatta non supera il confronto, perché chi la contraffà non ha il segreto.

L’URL deve essere https. È imposto da un vincolo di database, non da un suggerimento nel modulo, perché un segreto di firma che viaggia su una connessione non cifrata non è un segreto.

Verificare una consegna in Node.js

Nella pratica sono due i dettagli che fanno quasi tutti i danni. Usate il corpo grezzo della richiesta, non un oggetto analizzato e riserializzato: le differenze negli spazi cambiano l’hash. E confrontate a tempo costante, così chi attacca non può ricavare la firma corretta un byte alla volta dai tempi di risposta.

import { createHmac, timingSafeEqual } from 'node:crypto';

const SECRET = process.env.NOISSIME_WEBHOOK_SECRET;

// rawBody must be the exact bytes received, before any JSON parsing.
// Re-serialising a parsed object changes the whitespace and the check fails.
function isFromNoissime(rawBody, header) {
  if (typeof header !== 'string') return false;

  const expected =
    'sha256=' + createHmac('sha256', SECRET).update(rawBody).digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(header);

  // timingSafeEqual throws on a length mismatch, so compare lengths first.
  return a.length === b.length && timingSafeEqual(a, b);
}

Chiamatela prima di analizzare qualsiasi cosa. Se restituisce false, rispondete 401 e scartate la richiesta. Se restituisce true, avete una risposta che può provenire soltanto dal vostro questionario.

Il registro delle consegne

Un webhook che ha smesso di funzionare in silenzio ha esattamente lo stesso aspetto di uno che non è mai stato chiamato: non succede nulla, e nulla ve lo dice. Perciò ogni tentativo viene scritto in un registro delle consegne, e l’ultimo stato HTTP è mostrato accanto al webhook nell’editor — verde per un 2xx, rosso per qualsiasi altra cosa.

La consegna è asincrona, quindi lo stato viene riconciliato a posteriori anziché ipotizzato al momento dell’invio. Quello che vedete è lo stato che il vostro server ha effettivamente restituito. Ogni webhook può essere sospeso e riattivato senza eliminarlo, che è la cosa che di solito serve mentre il sistema ricevente viene rimesso in linea.

Un limite deliberato: non esiste un nuovo tentativo con attesa esponenziale. La consegna viene tentata una volta, l’esito viene registrato, e finisce lì. Costruire una coda di ritentativi che rispetti l’ordine e l’idempotenza è lavoro vero, e dichiararne una che non abbiamo costruito sarebbe peggio che dire questo. Nel frattempo la risposta non va mai persa: è nel database, visibile in analisi e reportistica, ed esportabile in CSV.

Un webhook che fallisce non può nemmeno interrompere la raccolta. Se la consegna solleva un errore, l’errore viene registrato e la risposta viene comunque salvata. Ciò che conta è la risposta della persona; la consegna è a impegno migliore.

Notifiche via email, senza le risposte

Aggiungete gli indirizzi che devono essere informati delle nuove risposte e riceveranno un’email all’arrivo di ciascuna. Il messaggio è in HTML personalizzato con un’alternativa in solo testo, perché sia i client testuali sia i filtri antispam leggono la parte in chiaro. Indica il questionario, riporta il totale progressivo delle risposte e rimanda ai risultati.

Ciò che non contiene sono le risposte. È una decisione, non una dimenticanza. Un’email di notifica viene inoltrata a un collega, archiviata automaticamente in una casella condivisa, sincronizzata su un telefono e conservata in un backup per anni. Immettere in quella catena le risposte in testo libero di una consultazione sparpaglia dati personali in sistemi che nessuno ha valutato, e disfa in sordina i controlli di accesso descritti nella pagina sulla sicurezza. I dati di chi risponde restano nel database, e l’email vi dice di andare a guardare.

Come i webhook, anche le notifiche sono innescate da un trigger di database, quindi seguono la risposta e non una particolare via di invio. Se non è configurato alcun indirizzo, non viene inviato nulla e non viene tentato nulla.

Che cosa questo non fa

Domande ricorrenti

Come verifico l’intestazione X-NumoForms-Signature?

Calcolate un HMAC-SHA256 del corpo grezzo della richiesta usando il segreto di firma di quel webhook, codificatelo in esadecimale, anteponete «sha256=» e confrontatelo con l’intestazione usando un confronto a tempo costante. Verificate prima di analizzare il JSON e rifiutate con un 401 tutto ciò che non corrisponde.

Che cosa succede se il mio endpoint è irraggiungibile quando arriva una risposta?

La consegna viene tentata una volta e l’esito è registrato nel registro delle consegne con lo stato HTTP. Non esiste un nuovo tentativo automatico con attesa progressiva. La risposta in sé non corre alcun rischio: il fallimento di un webhook non può impedire il salvataggio di una risposta, e i dati potete sempre recuperarli dalla schermata dei risultati o da un’esportazione CSV.

Un webhook parte anche per una risposta inviata da un modulo incorporato?

Sì. La consegna è innescata da un trigger di database sulla tabella delle risposte e non da una determinata pagina dell’applicazione, quindi avviene da qualunque parte arrivi la risposta: un link ospitato, un modulo incorporato o una risposta parziale appena completata.

Posso inviare un webhook a un URL http://?

No. Un vincolo di controllo nel database impone che l’URL cominci con https:// e l’editor rifiuta qualsiasi altra cosa. Un segreto di firma inviato su una connessione non cifrata non è un segreto.

Le email di notifica contengono le risposte di chi ha compilato?

No, deliberatamente. L’email dice che è arrivata una risposta, indica il questionario, riporta il totale progressivo e rimanda ai risultati. Le notifiche vengono inoltrate e restano nei backup delle caselle di posta, quindi i dati di chi risponde restano nel database, dove ci sono i controlli di accesso.

Esiste un’app Zapier o un’integrazione con Google Sheets?

No. Oggi i webhook sono l’unica integrazione in uscita. Non esiste un’app Zapier né un connettore per Google Sheets, e per nessuno dei due è stata pubblicata una data.

Collegate un questionario ai vostri sistemi.

Puntate un webhook verso un endpoint https e ogni risposta vi arriva firmata, così chi la riceve può dimostrare che viene da noi e non da chiunque abbia trovato l’URL. Aggiungete gli indirizzi da avvisare quando arriva una risposta, e i dati restano nel database dove sono i controlli di accesso.

  • Firma HMAC-SHA256 su ogni consegna
  • Registro delle consegne con l’ultimo stato HTTP
  • Attivato da un trigger del database, qualunque sia la via d’ingresso
Iniziate a costruire