sendgrid/sendgrid-nodejs

Unable to verify Signature Sendgrid Event Webhook?

Closed this issue · 2 comments

Webhook:

@ApiTags('SendGrid Event')
@Controller('/sendgrid-event')
export class SendGridEventController {
  constructor(
    private readonly appService: AppService,
    private readonly sendGridEventService: SendGridEventService,
    private readonly logger: Logger,
  ) {
    // For winston info.type
    this.logger.setContext('sendgrid-inbound-parse');
  }

  /**
   * Receive the Event Webhook from SendGrid
   *
   * @return {String} return 200
   */
  @Post('/webhook')
  eventHandler(
    @Req() request: Request,
    @Headers('x-twilio-email-event-webhook-signature') signature: string,
    @Headers('x-twilio-email-event-webhook-timestamp') timestamp: number,
    @Body() body: any,
  ): any {
    console.log(
      '--------------------------------------------------------------------------------------------',
    );
    this.logger.log(' /POST /sendgrid-event/webhook');
    this.logger.log(JSON.stringify(body));

    const reqHeader = request.rawHeaders.join(',');
    // Verify headers.
    if (!signature || !timestamp) {
      this.logger.error(`Forbidden: ${reqHeader}`);
      return {
        ok: false,
        message: '403, Forbidden',
      };
    }
    // Convert the public key string to a ECPublicKey.
    const ECPublicKey = this.sendGridEventService.convertPublicKeyToECDSA(
      process.env.SENDGRID_EVENT_PUBLICKEY_FOR_ENGAGEMENT,
    );
    // Verify the signature.
    //! Be sure to include the trailing carriage return and newline! - '\r\n'
    const payload = JSON.stringify(body) + '\r\n';
    const verifyResult = this.sendGridEventService.verifySignature(
      ECPublicKey,
      payload,
      signature,
      timestamp,
    );

    if (!verifyResult) {
      this.logger.error(`Unauthorized: ${reqHeader}`);
      return {
        ok: false,
        message: '401, Unauthorized',
      };
    }

    this.sendGridEventService.handleSendGridEvent(body);
  }
}

And the function convertPublicKeyToECDSA and verifySignature :

  /**
   * Convert the public key string to a ECPublicKey.
   *
   * @param {string} publicKey verification key under Mail Settings
   * @return {PublicKey} A public key using the ECDSA(Elliptic Curve Digital Signature Algorithm) algorithm
   */
  convertPublicKeyToECDSA(publicKey) {
    return PublicKey.fromPem(publicKey);
  }

  /**
   * Verify signed event webhook requests.
   *
   * @param {PublicKey} publicKey elliptic curve public key
   * @param {string|Buffer} payload event payload in the request body
   * @param {string} signature value obtained from the 'X-Twilio-Email-Event-Webhook-Signature' header
   * @param {string} timestamp value obtained from the 'X-Twilio-Email-Event-Webhook-Timestamp' header
   * @return {Boolean} true or false if signature is valid
   */
  verifySignature(publicKey, payload, signature, timestamp) {
    const timestampPayload = timestamp + payload;
    const decodedSignature = Signature.fromBase64(signature);
    return Ecdsa.verify(timestampPayload, decodedSignature, publicKey);
  }

But the same code, sometime it works, but sometime it doesn't work.

  1. Send Email to 1234@qq.com:
    request:
{
    "personalizations": [
        {
            "to": [
                {
                    "email": "1234@qq.com",
                    "name": "Weijia Liu"
                }
            ],
            "headers": {
                "Message-ID": "<xxx-test-message-id-3@hiretual.com>",
                "In-Reply-To": "<xxx-test-message-id-1@hiretual.com>",
                "References": "<xxx-test-message-id-1@hiretual.com>"
            }
        }
    ],
    "from": {
        "email": "from-xxx@xxxx.xxx.com",
        "name": "from-xxx"
    },
    "reply_to": {
        "email": "random-email-address-1@xx-xxx.testhtm.com ",
        "name": "from-xxxx"
    },
    "subject": "Test SG 1",
    "content": [
        {
            "type": "text/html",
            "value": "<p>Hello 3 from Twilio SendGrid!</p><p>%open-track%</p><a href='https://www.google.com'>Click here</a>"
        }
    ],
    "tracking_settings": {
        "click_tracking": {
            "enable": true,
            "enable_text": false
        },
        "open_tracking": {
            "enable": true,
            "substitution_tag": "%open-track%"
        }
    }
}

It works in webhook:

--------------------------------------------------------------------------------------------
[2021-12-06T09:43:03.724Z][info] service=standalone-webhooks|type=sendgrid-inbound-parse|content= /POST /sendgrid-event/webhook
[2021-12-06T09:43:03.725Z][info] service=standalone-webhooks|type=sendgrid-inbound-parse|content=[{"email":"1234@qq.com","event":"dropped","reason":"Bounced Address","sg_event_id":"ZHJvcC0yNDIzOTU2OC1XSEhiVzlUOVNsMjcyRFFBT0QwN3B3LTA","sg_message_id":"WHHbW9T9Sl272DQAOD07pw.filterdrecv-75ff7b5ffb-ktk29-1-61ADDB24-A.0","smtp-id":"<WHHbW9T9Sl272DQAOD07pw@geopod-ismtpd-3-0>","timestamp":1638783780}]
  1. But When I send to 12345@qq.com
    request:
{
    "personalizations": [
        {
            "to": [
                {
                    "email": "12345@qq.com",
                    "name": "Weijia Liu"
                }
            ],
            "headers": {
                "Message-ID": "<xxx-test-message-id-3@hiretual.com>",
                "In-Reply-To": "<xxx-test-message-id-1@hiretual.com>",
                "References": "<xxx-test-message-id-1@hiretual.com>"
            }
        }
    ],
    "from": {
        "email": "from-xxx@xxxx.xxx.com",
        "name": "from-xxx"
    },
    "reply_to": {
        "email": "random-email-address-1@xx-xxx.testhtm.com ",
        "name": "from-xxxx"
    },
    "subject": "Test SG 1",
    "content": [
        {
            "type": "text/html",
            "value": "<p>Hello 3 from Twilio SendGrid!</p><p>%open-track%</p><a href='https://www.google.com'>Click here</a>"
        }
    ],
    "tracking_settings": {
        "click_tracking": {
            "enable": true,
            "enable_text": false
        },
        "open_tracking": {
            "enable": true,
            "substitution_tag": "%open-track%"
        }
    }
}

It didn't work:

--------------------------------------------------------------------------------------------
[2021-12-06T09:46:20.898Z][info] service=standalone-webhooks|type=sendgrid-inbound-parse|content= /POST /sendgrid-event/webhook
[2021-12-06T09:46:20.899Z][info] service=standalone-webhooks|type=sendgrid-inbound-parse|content=[{"email":"12345@qq.com","event":"processed","send_at":0,"sg_event_id":"cHJvY2Vzc2VkLTI0MjM5NTY4LWxvb3ZzSXByVFUyZ0Z4aUprSHRHbVEtMA","sg_message_id":"loovsIprTU2gFxiJkHtGmQ.filterdrecv-7bc86b958d-gt6pz-1-61ADDBE3-E.0","smtp-id":"<xiao-test-message-id-3@hiretual.com>","timestamp":1638783971},{"email":"12345@qq.com","event":"delivered","ip":"50.31.49.42","response":"250 OK: queued as.","sg_event_id":"ZGVsaXZlcmVkLTAtMjQyMzk1NjgtbG9vdnNJcHJUVTJnRnhpSmtIdEdtUS0w","sg_message_id":"loovsIprTU2gFxiJkHtGmQ.filterdrecv-7bc86b958d-gt6pz-1-61ADDBE3-E.0","smtp-id":"<xiao-test-message-id-3@hiretual.com>","timestamp":1638783974,"tls":1}]
[2021-12-06T09:46:20.919Z][error] service=standalone-webhooks|type=sendgrid-inbound-parse|content=Unauthorized: Host,0b57-52-8-30-104.ngrok.io,User-Agent,SendGrid Event API,Content-Length,634,Accept-Encoding,gzip,Content-Type,application/json;charset=utf-8,X-Forwarded-For,167.89.119.29,X-Forwarded-Proto,https,X-Twilio-Email-Event-Webhook-Signature,MEUCIQD/rRwu4YRS5euzXaYfJl7gcekDNVSAhoIBoSFjb5BS1gIgG15aET6uZHax6pt8YloWv8spYFNLXqX2CT0t6LHF+mY=,X-Twilio-Email-Event-Webhook-Timestamp,1638783980

Looks like your payload is a multi event payload and sendgrid expects a \r\n after each event. Can you give this a try?

Closing due to inactivity. Please open a new Github issue if you still need help.