Sendbird WebhookリクエストをCloud Functionsで受信する際にハマった事

# はじめに
* SendbirdのWebhookをCloudFunctionsで受け付けたい
* リクエストがSendbirdからのリクエストであるか、または改ざんされていないかを確認するために署名を検証する必要がある

* 上記をやろうとした際にハマったこととその解決策を共有します

https://sendbird.com/docs/chat/platform-api/v3/webhook/webhook-overview#2-headers-3-x-sendbird-signature

# 実装した内容
Cloud FunctionsにてSendbirdからのWebhookリクエストを受け付ける関数を実装する中で、データが改ざんなどされていないかを検証する必要がある

具体的には...

リクエストされた際にSendbirdサーバー側にてPOSTリクエスト本文とAPIトークンを利用して暗号化を行いハッシュ値を生成します
生成されたハッシュ値は `x-sendbird-signature` に設定されて送られてきます

それを、Cloud Functions側で同じAPIトークンとPOSTリクエスト本文でハッシュ値を生成して内容が同じかどうかを比較し改ざんを検証します


 **Sendbird公式で紹介されている実装方法**
```javascript
const express = require('express');
const crypto = require('crypto');

const app = express();
const PORT = 5001;
const API_TOKEN = 'SENDBIRD_MASTER_API_TOKEN';

// The body should be a string.
// Validation fails if the body is passed as a JSON object and then stringified.
app.post('/sendbird-xsig', express.text({ type: 'json' }), (req, res) => {
  const body = req.body;
  const signature = req.get('x-sendbird-signature');
  const hash = crypto.createHmac('sha256', API_TOKEN).update(body).digest('hex');

  req.body = JSON.parse(req.body);
  signature == hash ? res.send(200) : res.send(401);
});

app.listen(PORT, () => {
  console.log(`App is listening on port ${PORT}`);
});
```

https://sendbird.com/docs/chat/platform-api/v3/webhook/webhook-overview#2-headers-3-x-sendbird-signature
# 期待した値でリクエストが飛んでこない
APIを呼び出す側の引数にはしっかりとJSON文字列を設定して呼び出しており、
Cloud Functions側ではその値が飛んでくるはずですが、飛んでこない...

`console.log()`など仕込んでデバッグしてみると、`JSONオブジェクト`で来ているっぽかった
ここはなんでなのかはわかりませんでしたが、とりあえずどうするか...？
(わかる方いたら教えて頂きたいです...🙇‍♂️)

```
TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object
```

```javascript
const express = require('express');
const crypto = require('crypto');

const app = express();
const PORT = 5001;
const API_TOKEN = 'SENDBIRD_MASTER_API_TOKEN';

app.post('/sendbird-xsig', express.text({ type: 'json' }), (req, res) => {
  const body = req.body;
  const signature = req.get('x-sendbird-signature');
  const hash = crypto.createHmac('sha256', API_TOKEN).update(body).digest('hex');
    // update()メソッドにて以下のエラーが発生する
    // TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or 
    // an instance of Buffer, TypedArray, or DataView. Received an instance of Object
  req.body = JSON.parse(req.body);
  signature == hash ? res.send(200) : res.send(401);
});

app.listen(PORT, () => {
  console.log(`App is listening on port ${PORT}`);
});
```

## JSON文字列で受け付けているが...
`express.text({ type: ~ })` というミドルウェア関数の一つであり、
`type: ~`に設定したContent-Typeの値を文字列として扱う挙動をするがそれが、
機能していないのか他要素で邪魔されているのかが不明

今回は、`express.text({ type: json })`となっており 
`Content-Type: application/json`は文字列で受け取るようになっているはず

https://expressjs.com/ja/api.html#express.text


# 対策として、`verify` オプションを使用すると良い
一旦対策としては、Express側では`verify`オプションを使用するといいらしい

https://qiita.com/kunihiros/items/d1dd0c5185417746a56b

`verify`では、`verify(req, res, buf, encoding)`のように、
引数にbufferデータを持つことができるので、
`update()` メソッドは `string` か `buffer` を引数で受け付けているので今回は `buffer` を渡すことに切り替えました

```javascript
app.use(express.json({
  verify: (req, _res, buf, _encoding) => {
    const signature = req.headers['x-sendbird-signature']
    const hash = crypto.createHmac('sha256', apiToken).update(buf).digest('hex')
    if (signature !== hash) {
      console.log('unmatched!')
    }
  }
}))
```

無事、ハッシュ値を検証できてデータ改ざんチェックを行うことができました！🎉

# 宣伝
自分が所属している会社についても紹介させてください 🙇‍♂️
弊社では物流課題解決や荷物を配送するドライバーの価値を向上させるために
「ピックゴー」 というサービスを運用しています

(ファイルアップロード制限？かかって画像アップロードできない...！ごめんなさい...🙇‍♂️)

代わりに弊社HPを...

https://cb-cloud.com/

# あと、那覇Baseが本店になりました
私が在籍している那覇Baseが本店に移転しました🎉
エンジニアも募集中です！
気になる方は会社説明会も実施してますので気軽にお話聞きに来てくれたら嬉しいです😎

https://recruit-okinawa.cb-cloud.com/?utm_source=web_recruite_tokyo&utm_medium=pcheader&utm_campaign=okinawarecruitecm