Řešení problémů
Proč podpis webhooku nesouhlasí?
Opravte selhání ověření podpisu webhooků. Podepište surové tělo požadavku celým tajným klíčem whsec_ a hodnotou X-Emailit-Timestamp.
Tento článek pomůže, když váš endpoint odmítá webhooky Emailitu, protože podpis, který spočítáte, neodpovídá hlavičce X-Emailit-Signature. Téměř vždy se bajty, které podepisujete, nebo tajný klíč, který používáte, liší od toho, co použil Emailit.
Příznaky
- Vaše obsluha vrací u každého webhooku
401nebo400s „invalid signature“, včetně událostí Send test z webového rozhraní. - Požadavky na kartě Requests u webhooku ukazují neúspěšné pokusy s chybovým stavovým kódem vašeho endpointu.
- Ověření funguje v jednotkovém testu s ručně vytvořeným obsahem, ale u skutečných doručení selhává.
Příčina
Emailit podepisuje každý požadavek takto:
signature = hex( HMAC-SHA256( secret, "<X-Emailit-Timestamp>.<raw request body>" ) )Podpis selže, když se liší jeden ze tří vstupů:
- Tělo bylo znovu serializováno. Frameworky často zpracují JSON dřív, než se spustí vaše obsluha. Volání
JSON.stringify(req.body)vytvoří jiné mezery a jiné pořadí klíčů než bajty, které Emailit odeslal. Tělo je pole JSON až se 100 událostmi, takže kód, který očekává objekt, ho může také přetvořit. - Tajný klíč je chybný. Použijte celou hodnotu včetně předpony
whsec_. Každý webhook má vlastní tajný klíč a jeho výměnou stará hodnota okamžitě přestane platit, i pro čekající opakování. - Časové razítko chybí nebo se liší. Použijte přesnou hodnotu hlavičky
X-Emailit-Timestampa spojte ji s tělem jedinou tečkou. - Porovnání je chybné. Hlavička je hexadecimální řetězec malými písmeny, ne Base64, a nemá předponu
sha256=. - Tělo změnilo něco před vaší aplikací, například proxy, která ho překóduje nebo dekomprimuje.
Řešení
-
Čtěte surové tělo požadavku. Zachyťte bajty dřív, než se spustí jakýkoli parser JSON. V Expressu připojte
express.raw()jen na trasu webhooku:import crypto from 'node:crypto'; app.post('/webhooks/emailit', express.raw({ type: 'application/json' }), (req, res) => { const timestamp = req.get('X-Emailit-Timestamp'); const expected = crypto .createHmac('sha256', process.env.EMAILIT_WEBHOOK_SECRET) // whsec_... .update(`${timestamp}.${req.body.toString('utf8')}`) .digest('hex'); const received = req.get('X-Emailit-Signature') ?? ''; const valid = received.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)); if (!valid) return res.sendStatus(401); const events = JSON.parse(req.body); // an array of events res.sendStatus(200); });V route handlerech Next.js použijte
await request.text(). V Laravelu použijte$request->getContent(). Ve Flasku použijterequest.get_data(). -
Získejte správný tajný klíč. Zavolejte Načtení webhooku, které vrací aktuální
secret. Ve webovém rozhraní se tajný klíč zobrazí jen jednou, takže pokud jste ho ztratili, otevřete webhook, zvolte Webhook secret a klíč vyměňte a hned upravte proměnnou prostředí. -
Odešlete testovací událost. Na stránce webhooku zvolte Send test a vyberte typ události. Test je podepsaný stejným tajným klíčem a dialogové okno ukazuje stavový kód a tělo odpovědi vašeho endpointu.
-
Zkontrolujte toleranci času. Pokud kvůli ochraně proti útokům opakováním odmítáte stará časová razítka, povolte odchylku několika minut a udržujte hodiny serveru synchronizované. Každý pokus o doručení, včetně opakování, nese nové časové razítko.
-
Zopakujte, co selhalo. Jakmile ověření funguje, zvolte Retry failed a znovu odešlete neúspěšná doručení za posledních 7 dní.
Kód v dalších jazycích najdete na stránce Podpis požadavku webhooku. Formát těla popisuje stránka Požadavky webhooků.
Stále si nevíte rady?
Napište podpoře nebo se zeptejte na Discordu. Uveďte ID webhooku (wh_…) a ID neúspěšné události, nikdy ne tajný klíč.