【问题标题】:Testing DocuSign webhooks with a connect key使用连接密钥测试 DocuSign webhook
【发布时间】:2022-02-15 04:15:14
【问题描述】:

我正在使用 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-&gt;computeHash($this-&gt;docusignConnectKey, $payload);

那么,我的问题肯定源于我发送数据的方式吗?

【问题讨论】:

  • 请检查(接受)您问题的最佳答案。 非常感谢!!

标签: php laravel docusignapi docusignconnect


【解决方案1】:

当您在传入的负载上计算自己的 HMAC(以查看它是否与在标头中发送的 HMAC 匹配)时,您必须按原样使用传入的负载。

在您的代码中:

public function handleWebhook(Request $request)
{
    $payload = json_decode($request->getContent(), true);

    $shouldProcessWebhook = $this->determineIfEnvelopeRelevant($payload);

您正在将 json 解码的有效负载发送到您的检查方法。这是不对的,您应该在原始有效负载到达时发送它。

(解码,然后编码 JSON 不一定会给您与原始相同的字节序列。)

JSON 解码方法应仅在您确认有效负载来自 DocuSign 之后应用于有效负载。

另外, 在对发件人进行身份验证之前进行 JSON 解码是一个安全问题。一个坏人可能正试图向您发送一些错误的输入。在您验证发件人之前(在本例中通过 HMAC),规则是不信任任何内容。

奖励评论

我建议您还配置 DocuSign Connect webhook 的基本身份验证功能。基本身份验证通常在 Web 服务器级别进行检查。 HMAC,因为它必须被计算,通常在应用程序级别检查。同时使用这两种方法可以为坏人提供坚实的防御。

【讨论】:

  • 嗨,我唯一要提到的是,我正在通过在构造函数中实际运行的中间件验证签名,甚至在调用 handleWebhook() 之前。我认为我的问题实际上源于这样一个事实,即在我的测试中,当我使用 postJson 时,它会转义数组,到处添加一堆斜线。正如您所指出的,如果我在 Hmac 验证器中使用实际的原始内容,它就可以正常工作。
  • 同意,将传入字节按原样用于 HMAC 和类似的校验和计算是至关重要的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-03
相关资源
最近更新 更多