我正在使用 DocuSign Connect 从 DocuSign 检索 webhook 并在我的 Larave 中消化它们;应用。这是基本的想法。
<?php
namespace App\Http\Controllers;
use App\Http\Middleware\VerifyDocusignWebhookSignature;
use App\Mail\PaymentRequired;
use App\Models\PaymentAttempt;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class DocusignWebhookController extends Controller
{
/**
* Create a new controller instance.
* If a DocuSign Connect key is preset, validate the request.
*/
public function __construct()
{
$this->gocardlessTabs = ['GoCardless Agreement Number', 'GoCardless Amount', 'GoCardless Centre'];
$this->assumedCustomer = 2;
if (config('docusign.connect_key')) {
$this->middleware(VerifyDocusignWebhookSignature::class);
}
}
/**
* Handle an incoming DocuSign webhook.
*/
public function handleWebhook(Request $request)
{
$payload = json_decode($request->getContent(), true);
$shouldProcessWebhook = $this->determineIfEnvelopeRelevant($payload);
if ($shouldProcessWebhook) {
switch ($payload['status']) {
case 'sent':
return $this->handleSentEnvelopeStatus($payload);
break;
case 'completed':
return $this->handleCompletedEnvelopeStatus($payload);
break;
case 'voided':
// ...
break;
default:
}
}
}
}
逻辑本身工作正常,但如果你看这里:
if (config('docusign.connect_key')) {
$this->middleware(VerifyDocusignWebhookSignature::class);
}
如果我指定一个连接密钥,我会运行一些中间件来验证 webhook 来自 DocuSign。
验证签名的类来自 DocuSign,如下所示:
<?php
namespace App\DocuSign;
/**
* This class is used to validate HMAC keys sent from DocuSign webhooks.
* For more information see: https://developers.docusign.com/platform/webhooks/connect/hmac/
*
* Class taken from: https://developers.docusign.com/platform/webhooks/connect/validate/
*
* Sample headers
* [X-Authorization-Digest, HMACSHA256]
* [X-DocuSign-AccountId, caefc2a3-xxxx-xxxx-xxxx-073c9681515f]
* [X-DocuSign-Signature-1, DfV+OtRSnsuy.....NLXUyTfY=]
* [X-DocuSign-Signature-2, CL9zR6MI/yUa.....O09tpBhk=]
*/
class HmacVerifier
{
/**
* Compute a hmac hash from the given payload.
*
* Useful reference: https://www.php.net/manual/en/function.hash-hmac.php
* NOTE: Currently DocuSign only supports SHA256.
*
* @param string $secret
* @param string $payload
*/
public static function computeHash($secret, $payload)
{
$hexHash = hash_hmac('sha256', $payload, utf8_encode($secret));
$base64Hash = base64_encode(hex2bin($hexHash));
return $base64Hash;
}
/**
* Validate that a given hash is valid.
*
* @param string $secret: the secret known only by our application
* @param string $payload: the payload received from the webhook
* @param string $verify: the string we want to verify in the request header
*/
public static function validateHash($secret, $payload, $verify)
{
return hash_equals($verify, self::computeHash($secret, $payload));
}
}
现在,为了在本地进行测试,我编写了一个测试,但是每当我运行它时,中间件都会告诉我 webhook 无效。
这是我的测试课
<?php
namespace Tests\Feature\Http\Middleware;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
class VerifyDocusignWebhookSignatureTest extends TestCase
{
use RefreshDatabase, WithFaker;
public function setUp(): void
{
parent::setUp();
config(['docusign.connect_key' => 'probably-best-not-put-on-stack-overflow']);
$this->docusignConnectKey = config('docusign.connect_key');
}
/**
* Given a JSON payload, can we parse it and do what we need to do?
*
* @test
*/
public function it_can_retrieve_a_webhook_with_a_connect_key()
{
Mail::fake();
$payload = '{"status":"sent","documentsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/documents","recipientsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/recipients","attachmentsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/attachments","envelopeUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514","emailSubject":"Please DocuSign: newflex doc test.docx","envelopeId":"2ba67e2f-0db6-46af-865a-e217c9a1c514","signingLocation":"online","customFieldsUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/custom_fields","notificationUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/notification","enableWetSign":"true","allowMarkup":"false","allowReassign":"true","createdDateTime":"2022-02-14T11:36:01.18Z","lastModifiedDateTime":"2022-02-14T11:37:48.633Z","initialSentDateTime":"2022-02-14T11:37:49.477Z","sentDateTime":"2022-02-14T11:37:49.477Z","statusChangedDateTime":"2022-02-14T11:37:49.477Z","documentsCombinedUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/documents/combined","certificateUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/documents/certificate","templatesUri":"/envelopes/2ba67e2f-0db6-46af-865a-e217c9a1c514/templates","expireEnabled":"true","expireDateTime":"2022-06-14T11:37:49.477Z","expireAfter":"120","sender":{"userName":"Newable eSignature","userId":"f947420b-6897-4f29-80b3-4deeaf73a3c5","accountId":"366e9845-963a-41dd-9061-04f61c921f28","email":"e-signature@newable.co.uk"},"recipients":{"signers":[{"tabs":{"textTabs":[{"validationPattern":"","validationMessage":"","shared":"false","requireInitialOnSharedChange":"false","requireAll":"false","value":"","required":"true","locked":"false","concealValueOnDocument":"false","disableAutoSize":"false","maxLength":"4000","tabLabel":"GoCardless Amount","font":"lucidaconsole","fontColor":"black","fontSize":"size9","localePolicy":{},"documentId":"1","recipientId":"56041698","pageNumber":"1","xPosition":"319","yPosition":"84","width":"84","height":"22","tabId":"207f970c-4d3c-4d0c-be6b-1f3aeecf5f95","tabType":"text"},{"validationPattern":"","validationMessage":"","shared":"false","requireInitialOnSharedChange":"false","requireAll":"false","value":"","required":"true","locked":"false","concealValueOnDocument":"false","disableAutoSize":"false","maxLength":"4000","tabLabel":"GoCardless Centre","font":"lucidaconsole","fontColor":"black","fontSize":"size9","localePolicy":{},"documentId":"1","recipientId":"56041698","pageNumber":"1","xPosition":"324","yPosition":"144","width":"84","height":"22","tabId":"f6919e94-d4b7-4ef4-982d-3fc6c16024ab","tabType":"text"},{"validationPattern":"","validationMessage":"","shared":"false","requireInitialOnSharedChange":"false","requireAll":"false","value":"","required":"true","locked":"false","concealValueOnDocument":"false","disableAutoSize":"false","maxLength":"4000","tabLabel":"GoCardless Agreement Number","font":"lucidaconsole","fontColor":"black","fontSize":"size9","localePolicy":{},"documentId":"1","recipientId":"56041698","pageNumber":"1","xPosition":"332","yPosition":"200","width":"84","height":"22","tabId":"9495a53c-1f5e-42a5-beec-9abcf77b4387","tabType":"text"}]},"creationReason":"sender","isBulkRecipient":"false","requireUploadSignature":"false","name":"Jesse","firstName":"","lastName":"","email":"Jesse.Orange@newable.co.uk","recipientId":"56041698","recipientIdGuid":"246ce44f-0c11-4632-ac24-97f31911594e","requireIdLookup":"false","userId":"b23ada8e-577e-4517-b0fa-e6d8fd440f21","routingOrder":"1","note":"","status":"sent","completedCount":"0","deliveryMethod":"email","totalTabCount":"3","recipientType":"signer"},{"tabs":{"signHereTabs":[{"stampType":"signature","name":"SignHere","tabLabel":"Signature 7ac0c7c8-f838-4674-9e37-10a0df2f81c1","scaleValue":"1","optional":"false","documentId":"1","recipientId":"38774161","pageNumber":"1","xPosition":"161","yPosition":"275","tabId":"371bc702-1a91-4b71-8c77-a2e7abe3210e","tabType":"signhere"}]},"creationReason":"sender","isBulkRecipient":"false","requireUploadSignature":"false","name":"Jesse Orange","firstName":"","lastName":"","email":"jesseorange360@gmail.com","recipientId":"38774161","recipientIdGuid":"844f781c-1516-4a5a-821a-9d8fb2319369","requireIdLookup":"false","userId":"f544f7ff-91bb-4175-894e-b42ce736f273","routingOrder":"2","note":"","status":"created","completedCount":"0","deliveryMethod":"email","totalTabCount":"1","recipientType":"signer"}],"agents":[],"editors":[],"intermediaries":[],"carbonCopies":[],"certifiedDeliveries":[],"inPersonSigners":[],"seals":[],"witnesses":[],"notaries":[],"recipientCount":"2","currentRoutingOrder":"1"},"purgeState":"unpurged","envelopeIdStamping":"true","is21CFRPart11":"false","signerCanSignOnMobile":"true","autoNavigation":"true","isSignatureProviderEnvelope":"false","hasFormDataChanged":"false","allowComments":"true","hasComments":"false","allowViewHistory":"true","envelopeMetadata":{"allowAdvancedCorrect":"true","enableSignWithNotary":"false","allowCorrect":"true"},"anySigner":null,"envelopeLocation":"current_site","isDynamicEnvelope":"false"}';
// Compute a hash as in production this will come from DocuSign
$hash = $this->computeHash($this->docusignConnectKey, $payload);
// Validate the hash as we're going to use it as the header
$this->assertTrue($this->validateHash($this->docusignConnectKey, $payload, $hash));
// Convert this response to an array for the test
$payload = json_decode($payload, true);
// Post as JSON as Laravel only accepts POSTing arrays
$this->postJson(route('webhook-docusign'), $payload, [
'x-docusign-signature-3' => $hash
])->assertStatus(200);
$this->assertDatabaseHas('payment_attempts', [
'envelope_id' => $payload['envelopeId']
]);
Mail::assertNothingSent();
}
/**
* As we're testing we need a way to verify the signature so we're computing the hash.
*/
private function computeHash($secret, $payload)
{
$hexHash = hash_hmac('sha256', $payload, utf8_encode($secret));
$base64Hash = base64_encode(hex2bin($hexHash));
return $base64Hash;
}
/**
* Validate that a given hash is valid.
*
* @param string $secret: the secret known only by our application
* @param string $payload: the payload received from the webhook
* @param string $verify: the string we want to verify in the request header
*/
private function validateHash($secret, $payload, $verify)
{
return hash_equals($verify, self::computeHash($secret, $payload));
}
}
我还使用 webhook.site 来比较哈希:
鉴于此,我可以告诉您x-docusign-signature-3
与我运行时生成的哈希匹配
$hash = $this->computeHash($this->docusignConnectKey, $payload);
那么,我的问题肯定源于我发送数据的方式吗?