दस्तावेज़ पर जाएँ
PHP

किसी delivery का सत्यापन

`OpenEmail::verifyWebhookSignature`: constant-time, replay विंडो के साथ, और पार्स किया गया इवेंट वापस।

किसी request handler में

वेबहुक URL सार्वजनिक होता है। इंटरनेट पर कुछ भी उस पर सही आकार का JSON POST कर सकता है, इसलिए जो handler सिग्नेचर जाँचे बिना इवेंट का type पढ़ता है वह एक खुला write API है।

webhook.php
use OpenEmail\Exception\WebhookSignatureException;use OpenEmail\OpenEmail; $payload = (string) file_get_contents('php://input'); try {    $event = OpenEmail::verifyWebhookSignature(        $payload,        $_SERVER,        (string) getenv('OPENEMAIL_WEBHOOK_SECRET'),        toleranceSeconds: 300,    );} catch (WebhookSignatureException) {    http_response_code(400);     return;} error_log($event['type'] . ' ' . $event['id']);http_response_code(204);

“कच्ची” बॉडी स्ट्रिंग के रूप में पास करें। उसे डिकोड करके फिर से encode करने से कुंजियों का क्रम और खाली जगहें बदल जाती हैं, और signature मेल नहीं खाएगा, इसीलिए payload का टाइप string है: array, जैसे json_decode() का नतीजा या किसी फ़्रेमवर्क का पार्स किया गया input, जाँचे जाने के बजाय TypeError होता है। हेडर किसी भी case में हेडर नाम वाला array हो सकते हैं, जैसा getallheaders() लौटाता है, $_SERVER, जहाँ हेडर HTTP_X_OPENEMAIL_SIGNATURE के रूप में आता है, Symfony या Laravel के HeaderBag जैसा कोई भी iterable, या एक PSR-7 रिक्वेस्ट। जो मान array है उसे उसके पहले तत्व से पढ़ा जाता है।

दो चीज़ें जिन्हें यह संभालता है और हाथ से लिखी जाँच आमतौर पर नहीं: यह hash_equals() से MAC की तुलना स्थिर समय में करता है, ताकि समय मापकर सही prefix का पता न लगाया जा सके, और यह किसी भी दिशा में toleranceSeconds: से पुरानी delivery को अस्वीकार करता है, जो आपके कुछ और न कहने पर पाँच मिनट है, ताकि पकड़ी गई रिक्वेस्ट हमेशा replay न की जा सके। toleranceSeconds: 0 replay जाँच बंद कर देता है। दोनों bugs चुपचाप रहते हैं। इनमें से किसी एक वाला handler हर वह टेस्ट पास कर लेता है जो आप लिखने की सोचेंगे।

यह हर विफलता पर OpenEmail\Exception\WebhookSignatureException throw करता है: ग़ायब X-OpenEmail-Signature हेडर, ऐसा हेडर जो t=<seconds>,v1=<hex> रूप में न हो, अवधि से बाहर का timestamp, मेल न खाने वाला signature, या ऐसी signed बॉडी जो JSON object न हो। सफल होने पर यह बॉडी को id, type, createdAt और data वाले camelCase कुंजियों के array में डिकोड करके लौटाता है, इसलिए ग़लत करने के लिए कोई दूसरा json_decode() नहीं बचता।

ख़ाली secret इसके बजाय OpenEmail\Exception\InvalidArgumentException throw करता है, क्योंकि यह ग़लत delivery नहीं बल्कि आपके कॉन्फ़िगरेशन की ग़लती है, और getenv() के आगे लगा (string) सेट न किए गए वेरिएबल को ठीक यही बना देता है। सिर्फ़ WebhookSignatureException को catch करें और 400 लौटाएँ। तब ग़ायब secret स्क्रिप्ट को एक uncaught exception के साथ ख़त्म करता है, जिस पर production में PHP 500 लौटाता है, और आपके ठीक करने के बाद delivery फिर आज़माई जाती है, बजाय इसके कि हर event को जाली मानकर लौटा दिया जाए।

जाँच hash_hmac() और hash_equals() पर चलती है, जो हर PHP build में होते हैं, इसलिए इसे किसी एक्सटेंशन की ज़रूरत नहीं। नमूना exit कॉल करने के बजाय return करता है, इसलिए यही पंक्तियाँ किसी फ़ंक्शन या front controller के अंदर भी काम करती हैं।

PSR-7 ऐप में

PSR-7 रिक्वेस्ट के साथ, जैसी Slim और Mezzio आपको देते हैं, हेडर के रूप में ख़ुद रिक्वेस्ट और स्ट्रिंग के रूप में उसकी बॉडी पास करें।

webhook_handler.php
use OpenEmail\Exception\WebhookSignatureException;use OpenEmail\OpenEmail;use Psr\Http\Message\ResponseFactoryInterface;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface; function receiveOpenEmail(ServerRequestInterface $request, ResponseFactoryInterface $responses): ResponseInterface{    try {        $event = OpenEmail::verifyWebhookSignature(            (string) $request->getBody(),            $request,            (string) getenv('OPENEMAIL_WEBHOOK_SECRET'),        );    } catch (WebhookSignatureException) {        return $responses->createResponse(400);    }     error_log($event['type'] . ' ' . $event['id']);     return $responses->createResponse(204);}

जल्दी जवाब दें: जिस delivery को 5 सेकंड के भीतर जवाब नहीं मिलता, उसे विफल माना जाता है और बाद में फिर भेजा जाता है, इसलिए काम queue को सौंपें और 2xx के साथ जवाब दें। queue को सौंपने समेत Laravel और Symfony के controllers फ़्रेमवर्क वाले पेज पर हैं।

OpenEmail\Constants\WebhookSignatureHeaders उन तीन हेडर के नाम देता है जो delivery में होते हैं: X-OpenEmail-Signature, event के टाइप के साथ X-OpenEmail-Event, और उसकी id के साथ X-OpenEmail-Delivery, जो बॉडी वाली id ही है। यह id event के हर retry और replay पर एक जैसी रहती है, इसलिए पहले संभाले जा चुके events को छोड़ते समय सहेजने वाली यही है।

जब secret rotate हो रहा हो

webhooks->rotateSecret में कोई overlap विंडो नहीं है: इसके लौटते ही डिलीवरी नए secret से हस्ताक्षरित होती हैं। पहले ऐसा receiver deploy करें जो दोनों में से कोई भी secret स्वीकार करे, rotate करें, नया secret वहाँ सहेजें जहाँ से receiver उसे पढ़ता है, फिर पुराने को हटा दें।

two_secrets.php
use OpenEmail\Exception\WebhookSignatureException;use OpenEmail\OpenEmail; function verifyDelivery(string $payload, mixed $headers): array{    $next = getenv('OPENEMAIL_WEBHOOK_SECRET_NEXT');     try {        return OpenEmail::verifyWebhookSignature($payload, $headers, (string) getenv('OPENEMAIL_WEBHOOK_SECRET'));    } catch (WebhookSignatureException $error) {        if (!is_string($next) || $next === '') {            throw $error;        }         return OpenEmail::verifyWebhookSignature($payload, $headers, $next);    }}

webhooks->test एक हस्ताक्षरित कृत्रिम इवेंट भेजता है, इसलिए पुराने को हटाने से पहले यह साबित करने के लिए कि नया secret सत्यापित होता है, rotation के बाद इसे कॉल करें।