Yoctol / bottender

⚡️ A framework for building conversational user interfaces.
https://bottender.js.org
MIT License
4.2k stars 333 forks source link

Failed WhatsApp Signature Validation #815

Open florianmaxim opened 4 years ago

florianmaxim commented 4 years ago

Hey there! Thank you for this great project! Unfortunalety I'm having trouble connecting twilio (live credentials) to my whatsapp webhook (other webhooks work great).. Twilio is trowing an "11200" errorCode containing this body:

{ "error": { "message": "WhatsApp Signature Validation Failed!", "request": { "rawBody": "xxx", "headers": { "x-twilio-signature": "xxxLzNwapQY4rGkb+mVFYO2I2jI=" } } } }

Any help would would be appreciated!

chentsulin commented 4 years ago

I'm afraid there're some cases that I didn't find in the Twilio official docs. For example:

This bodySHA256 handling:

https://github.com/twilio/twilio-node/blob/565cc4cc523ab7fe7efa3a9eb2dcc73bfa55e759/lib/webhooks/webhooks.js#L170-L184 https://github.com/twilio/twilio-node/blob/565cc4cc523ab7fe7efa3a9eb2dcc73bfa55e759/lib/webhooks/webhooks.js#L62-L64

This port handling:

https://github.com/twilio/twilio-node/blob/565cc4cc523ab7fe7efa3a9eb2dcc73bfa55e759/lib/webhooks/webhooks.js#L100-L110

Is there any port or bodySHA256 stuff on your received request?

florianmaxim commented 4 years ago

Hm, do you maybe see any issue in my nginx handler ?

`location /api/ {

  proxy_pass_header Server;

  proxy_set_header Host $http_host;

  proxy_set_header X-Scheme $scheme;

  proxy_set_header X-Real-IP    $remote_addr;

  proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;

  proxy_redirect off;

  proxy_connect_timeout 20;

  proxy_read_timeout 20;

  proxy_pass https://nodejs:3000/;

}

`

florianmaxim commented 4 years ago

@chentsulin Ah, wait it always comes with a second error:

ErrorUrl "https://xxxxxx.dev/api/webhooks/whatsapp"
SmsMessageSid "SM849483eda06exxxxxx"
ErrorCode "11200"
NumMedia "0"
SmsSid "SM849483eda06e945xxxxxx"
SmsStatus "received"
Body "hallo"
To "whatsapp:+14155xxxxxx"
NumSegments "1"
MessageSid "SM849483eda06e945d69exxxxxx"
AccountSid "ACd5227f8efd4687bb2xxxxxx"
From "whatsapp:+491764xxxxxx"
ApiVersion "2010-04-01"
sourceComponent "14100"
url "null"
ErrorCode "11200"
LogLevel "ERROR"
Msg "The URI scheme, of the URI null, must be equal (ignoring case) to 'http', 'https', 'ws', or 'wss'"
EmailNotification "false"
chentsulin commented 4 years ago

For the first error, if you're using express behind nginx you may want to enable('trust proxy'), so your server can use the X-Forwarded-* headers to determine the connection and the IP address of the client: https://stackoverflow.com/questions/39930070/nodejs-express-why-should-i-use-app-enabletrust-proxy

For the second error, are you sending any urls (for example, media url) in your app? could you show me the url or the function call?

chentsulin commented 4 years ago

Oh, maybe I should make some changes to the inferred url: https://github.com/Yoctol/bottender/blob/ce0f13c78dd0d4cdf462b2e9316ef7e1a9278bbe/packages/bottender/src/server/Server.ts#L136

And still need another two nginx configs to make it works:

https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/

X-Forwarded-Host: example.com
X-Forwarded-Proto: https
florianmaxim commented 4 years ago

The server app is as reduced as this:
` const server = express();

server.set('trust proxy');

const verify = (req, _, buf) => { req.rawBody = buf.toString(); }; server.use(bodyParser.json({ verify })); server.use(bodyParser.urlencoded({ extended: false, verify }));

server.all('*', (req, res) => { return handle(req, res); });`

florianmaxim commented 4 years ago

Thank you so much for the update!

I updated bottender to 1.4.7 as well as the NGINX server block:

` proxy_pass http://nodejs:3000/;

  proxy_set_header    X-Real-IP           $remote_addr;

  proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;

  proxy_set_header    X-Forwarded-Proto   $scheme;

  proxy_set_header    Host                $host;

  proxy_set_header    X-Forwarded-Host    $host;

  proxy_set_header    X-Forwarded-Port    $server_port;

`

But still getting the same error from f*** Twilio :( This is frustrating since everthing is working so well with th eother platforms!

Any other hints?..

chentsulin commented 4 years ago

Are you able to print this out on your express server? This might helps a lot.

const server = express();

server.use((req, res, next) => {
  console.log('headers', JSON.stringify(req.headers));
  console.log('hostname', req.hostname);
  console.log('protocol', req.protocol);
  console.log('requestUrl', `${req.protocol}://${req.hostname}${req.url}`);

  next();
});

We should make sure the requestUrl here in your server match the url you registered on Twilio.

florianmaxim commented 4 years ago

here is the log:

`"headers": { "x-real-ip":"54.242.136.XXX", "x-forwarded-for":"54.242.136.XXX", "x-forwarded-proto":"https", "host":"hn-bot.dev", "x-forwarded-host":"XXX.dev", "x-forwarded-port":"443", "connection":"close", "content-length":"318", "content-type":"application/x-www-form-urlencoded", "x-twilio-signature":"9gsseAIhctS18mbe7dErVXXXXXXXXX", "i-twilio-idempotency-token":"8f13d9ea-4884-4b81-8dd9-7XXXXXXXXX", "accept":"/", "cache-control":"max-age=259200", "user-agent":"TwilioProxy/1.1" }"

hostname: XXX.dev protocol: http requestUrl http://XXX.dev/webhooks/whatsapp`

So I guess "x-forwarded-proto":"https" and protocol: http should match, right?

chentsulin commented 4 years ago

It's a little bit weird. I expected express req.protocol should return the value of x-forwarded-proto when trust proxy mode is on.

I see your comment calling server.set('trust proxy'); here: https://github.com/Yoctol/bottender/issues/815#issuecomment-647446220

If you did that, you should change that line to one of the following call:

server.set('trust proxy', true);

// or
server.enable('trust proxy');

Those two do the exactly same thing. See: https://github.com/expressjs/express/blob/18da651c5b57431f61391d9de2518a8c2ef88b2c/lib/application.js#L452-L454

But you can't call

server.set('trust proxy');
florianmaxim commented 4 years ago

@chentsulin Ah, damn it, my bad! I changed it and now it matches:

"x-forwarded-proto":"https" and protocol: https matches.

But I just realized the url ending /api/ is getting lost on it's way!

requestUrl http://XXX.dev/webhooks/whatsapp should actually be requestUrl http://XXX.dev/api/webhooks/whatsapp

Any idea where this is happening?

chentsulin commented 4 years ago

Could req.originalUrl help in this case?

const server = express();

server.use((req, res, next) => {
  console.log('originalUrl', req.originalUrl);
  console.log('url', req.url);
  console.log('requestUrl', `${req.protocol}://${req.hostname}${req.originalUrl}`);

  next();
});

If /api is included in req.originalUrl, we should switch to use it.

florianmaxim commented 4 years ago

I already tried. originalUrl /webhooks/whatsapp doesnt include the path as well..

It is not supposed to be included in the bottender.config.js, or is it?

whatsapp: { enabled: true, path: '/webhooks/whatsapp',

tw0517tw commented 4 years ago

How about adding $request_uri in your nginx config proxy_pass part? http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass 圖片

tw0517tw commented 4 years ago

Note: you might need to modify path in your bottender.config.js accordingly

florianmaxim commented 4 years ago

How about adding $request_uri in your nginx config proxy_pass part? http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass 圖片

It's already inside a location block:

location /api/ { proxy_pass http://nodejs:3000;

So I'm relatively sure I don't need the $request_uri in here, or do I?

florianmaxim commented 4 years ago

@tw0517tw

Ah, it has to be included on the proxy header as well, ofc!

proxy_set_header Host $host$request_uri; proxy_set_header X-Forwarded-Host $host$request_uri;

EDIT: Which is actually not a soluction, since modifiying the host like that doesnt realy make sense...

florianmaxim commented 4 years ago

Okay, maybe this helps:

Since it's all working FINE for other messengers here's the slack config:

The Slack request URL is set to https://xxx.dev/api/webhooks/slack.

Path in bottender.config.js is set to /webhooks/messenger'.

Enabled trust proxy server.enable('trust proxy');.

Log of a slack request:

hostname xxx.dev
protocol https
originalUrl /webhooks/slack
requestUrl https://xxx.dev

NGINX block:

location /api/ {

      proxy_pass http://nodejs:3000/;

      proxy_set_header    X-Real-IP                   $remote_addr;

      proxy_set_header    X-Forwarded-For      $proxy_add_x_forwarded_for;

      proxy_set_header    X-Forwarded-Proto   $scheme;

      proxy_set_header    Host                          $host;

      proxy_set_header    X-Forwarded-Host    $host;

      proxy_set_header    X-Forwarded-Port    $server_port;

}

Why is this not working for twilio?

chentsulin commented 4 years ago

@florianmaxim

Twilio is the only provider that uses HTTP URL to sign the signature. I'm thinking the graceful way to solve this url behind proxy issue, but if you want to work around this, you may try this for now:

const server = express();

server.use((req, res, next) => {
  req.url = `/api${req.url}`; // conditionally apply this on your proxy env

  next();
});

Edit: Sorry my bad. Should be /api${req.url} instead of /api/${req.url}

chentsulin commented 4 years ago

Not everyone have permission to change the settings of nginx, so maybe we should support some other options to config host, protocol and url just like twilio-node does:

https://github.com/twilio/twilio-node/blob/565cc4cc523ab7fe7efa3a9eb2dcc73bfa55e759/lib/webhooks/webhooks.js#L211-L214

florianmaxim commented 4 years ago

@florianmaxim

Twilio is the only provider that uses HTTP URL to sign the signature. I'm thinking the graceful way to resolve this url behind proxy issue, but if you want to work around this, you may try this for now:

const server = express();

server.use((req, res, next) => {
  req.url = `/api${req.url}`; // conditionally apply this on your proxy env

  next();
});

Edit: Sorry my bad. Should be /api${req.url} instead of /api/${req.url}

That indeed turn the log console.log('requestUrl',${req.protocol}://${req.hostname}${req.url}); into https://xxx.dev/api/webhooks/whatsapp.

But results into a 502 on Twilio:

Twilio was unable to fetch content from: http://xxx.dev/api/webhooks/whatsapp
Error: Total timeout is triggered. Configured tt is 15000ms and we attempted 1 time(s)
Account SID: ACd5227f8efd4687bb211da6e5xxxxxx
SID: SM5f8958810d5a3c070db2f889fxxxxxx
Request ID: 5864b582-3ca8-41bd-923e-5ed38xxxxxx
Remote Host: xxx.dev
Request Method: POST
Request URI: http://xxx.dev/api/webhooks/whatsapp
URL Fragment: true

:/ :(

chentsulin commented 4 years ago

After adding that path patch, you need to modify path in your bottender.config.js accordingly. For example:

whatsapp: { enabled: true, path: '/webhooks/whatsapp',

to

whatsapp: { enabled: true, path: '/api/webhooks/whatsapp',

Note: This affects every channel (messenger, whatsapp...).

florianmaxim commented 4 years ago

After adding that path patch, you need to modify path in your bottender.config.js accordingly. For example:

whatsapp: { enabled: true, path: '/webhooks/whatsapp',

to

whatsapp: { enabled: true, path: '/api/webhooks/whatsapp',

Note: This affects every channel (messenger, whatsapp...).

Right, of course! This works.

So briefly summarized:

server.use((req, res, next) => {
    if(req.originalUrl === '/webhooks/whatsapp') {
      req.url = `/<url_path>${req.url}`; 
    }
}):
whatsapp: {
      enabled: true,
      path: '/<url_path>/webhooks/whatsapp',
}

Thanks a lot to @tw0517tw @chentsulin for this quick workaround!!