ShopifyのWebhookをStrapiで受け取るまで

Shopifyでは、外部サービスへの通知として様々なWebhookが用意されています。（カートへの追加、アカウントの追加、注文作成、商品作成...etc)
-> [公式ドキュメント: Events > Webhook](https://help.shopify.com/en/api/reference/events/webhook#events) もご参考に...

Webhookをうまく使えば、Shopifyから他のサービスへの連携も可能になることでしょう。
というわけで、ShopifyからのWebhookを`Strapi`で受け取るまでを試してみました。

※ShopifyのWebhookについては、こちらの記事も参考になると思います。

* [Shopify の Webhook を試してみる](https://qiita.com/kumatronik/items/982d4c5faa1c8a4d41ae)
* [GoとGAEでWebhookを使ったプライベートなShopifyアプリを作る](https://qiita.com/tao_s/items/4443527f015e4b979bcb)

### ShoipfyのWebhook設定

今回は、Shopifyのストアに通知を設定します。
※Shopifyのストアは、パートナーアカウントの管理画面から「開発」用のストアを用意しました。

#### 1. 設定画面へ移動

* ストアの管理画面にある左側メニューの最下部に「設定」のリンクから設定画面に移動します

![shopify_app_config.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840/ccdb8cd4-4573-c6df-0357-dcaa17989215.png)

#### 2. 通知設定へ移動

* 設定画面の真ん中あたりにある「通知」のリンクから通知用の設定画面に移動します

![shopify_app_config_notification.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840/cc4f194d-4452-31d0-8302-7bba3480c94d.png)

#### 3. Webhookを作成

* 最下部にWebhookに関する設定があります。そこからWebhookを作成します

![shopify_app_config_webhook_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840/269047ef-df44-78cf-9d74-407fffaed304.png)

![shopify_app_config_webhook_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840/e56c8ede-b0bd-e75e-61f7-b006c88b999d.png)

イベントの選択、コールバックURLの指定などを経て、Webhookを作成します。
コールバックURLは後で変更可能ですので、現時点では適当なURLを指定しておきます。
※作成したWebhookはテスト通知を送信できますので、以降で確認したいと思います。

また、「すべてのWehookは...」と表示されている箇所で、ランダムな文字列が表示されます。この文字列はWebhook検証時に利用します。

### ShopifyのWebhook検証方法

公式ドキュメントに[webhookの検証に関する記載](https://help.shopify.com/en/api/getting-started/webhooks#verify-webhook)があります。基本的にはこの内容に沿って実装する感じです。

具体的な検証ロジックとしては、リクエストヘッダーにあるShopifyからの`HAMC (X-Shopify-Hmac-SHA256)`とリクエストボディとシークレットキー(Webhook作成時に生成された文字列)を使って、算出した`HMAC`が同じ値になるかを比較する、という具合です。

なお、今回はStrapiというheadless CMSを使って、Webhookを受け取ることを試しました。
※Strapi -> https://strapi.io/

#### 1. ctx.request.bodyに、RawBodyを保持するための設定を追加

（すみません。詳細は割愛しますが...）
StrapiがHTTPリクエストを受け取ってカスタマイズするコード内で処理するまでの間に、様々な`middleware`がリクエストを処理します。
その中の1つに`koa-body`(https://github.com/dlau/koa-body) を使ってパースしている処理があります。
このパーサーに、RawBodyを含めるよう設定しないと後述するWebhook検証ができなくなるので、ここで設定を一部変更します。

具体的には `config > environments > (development | staging | production) > request.json` を変更します。
※request.jsonの中で、 `"parser"`に`"includeUnparsed": true`を追加します

```json
{
  "session": {
    "enabled": true,
    "client": "redis",
    "connection": "redis",
    "key": "strapi.sid",
    "prefix": "strapi:sess:",
    "secretKeys": ["mySecretKey1", "mySecretKey2"],
    "httpOnly": true,
    "maxAge": 86400000,
    "overwrite": true,
    "signed": false,
    "rolling": false
  },
  "logger": {
    "level": "debug",
    "exposeInContext": true,
    "requests": true
  },
  "parser": {
    "includeUnparsed": true, <--(ココ)
    "enabled": true,
    "multipart": true
  }
}

```

#### 2. APIの作成

Webhookを受け取るためのAPIを作成します。

```bash

$ strapi generate:api webhook
```

`api`ディレクトリ直下に一式作成されます。

<img width="214" alt="strapi_api_webhook.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840/64ee66d0-5bc6-df7d-9d67-a4801ff404a2.png">

#### 3. Webhook検証処理の実装

だいたいこんな感じです。

* 環境変数`SHOIPFY_WEBHOOK_SECRET`に、Webhook作成時に生成された文字列を持たせておきます

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

module.exports = async (ctx, next) => {

  // ctx.request.header の中身
  // {
  //   'content-type': 'application/json',
  //   'x-shopify-topic': 'customers/create',
  //   'x-shopify-shop-domain': (ストアのドメイン),
  //   'x-shopify-hmac-sha256': (shopifyで算出したhmac),
  //   'x-shopify-api-version': '2020-01',
  //   'accept-encoding': 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
  //   accept: '*/*',
  //   'user-agent': 'Ruby',
  //   'content-length': '552',
  //   connection: 'close',
  //   'x-forwarded-proto': 'https',
  //   'x-forwarded-for': '35.196.97.22'
  // }
  const hmac_header = ctx.request.header['x-shopify-hmac-sha256'];

  // rawBodyとしてセットされているbodyを取得する
  //  request.json内で、 "parser" に "includeUnparsed": true を追加すると取得できるようになる
  const unparsedSymbol = Symbol.for('unparsedBody');
  const rawBody = ctx.request.body[unparsedSymbol];
  const calculated_hmac = crypto.createHmac('sha256', process.env.SHOIPFY_WEBHOOK_SECRET)
    .update(rawBody, 'utf8')
    .digest('base64');

  console.log('------');
  console.log('hmac_header     >> ' + hmac_header);
  console.log('calculated_hmac >> ' + calculated_hmac);
  const isvalid = crypto.timingSafeEqual(Buffer.from(hmac_header), Buffer.from(calculated_hmac));
  if (!isvalid) {
    return ctx.unauthorized('Invalid Request');
  }
  console.log('shopify webhook: ok.');
  
  // 何がしかの処理...

  ctx.send(200);
};
```

#### 4. 実際に試してみる

Strapiをローカルで起動して確認する際には、ローカルで起動したアプリにインターネットからアクセスできるようにする必要があります。
そこで今回は、[ngrok](https://ngrok.com/)というトンネルサービスを利用して、ローカルで立ち上げたサーバーを一時的に、インターネットからアクセスできるようにします。

Strapiは通常ポート番号:1337で起動しますので、こんな感じでngrokを起動させておきます。

```bash

$ ngrok http 1337
```

インターネットで公開されたURLと合わせて、コールバックURLをShopifyのストアで作成したWebhookに設定しておきましょう。
WebhookのコールバックURLは後から変更可能です。

![shopify_app:config_webhook_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1840/726f15ad-183c-0cc5-4636-827a629cb8a0.png)

そして、「テスト通知を送信する」というリンクからコールバックURLにテストをPOSTしますので、結果をローカルのコンソールで確認しましょう。
こんな感じで算出された値が一致すれば成功です！

```
------
hmac_header     >> pl6HMcWpl4f0OAmDsdv83LX1RSmf7PX6ghKOV76VkUo=
calculated_hmac >> pl6HMcWpl4f0OAmDsdv83LX1RSmf7PX6ghKOV76VkUo=
shopify webhook: ok.
```

### (その他)

本題から外れますが、Strapiをどこにホストするか？というあたりで、EC2を用意するなど色々な選択肢があると思いますが、個人的には[Heroku](https://jp.heroku.com/)推しです（笑

もちろん、Heroku上にStrapiをデプロイすることは可能です。セッションの管理にredisを使用するとか物理的なファイルアップロード先にcloudinaryやS3を設定するなど、Heroku特有の設定を施すことは必要ですが、サーバのメンテナンスにかける工数が確保できないような状況では__非常__に心強いと思います。

Shopify -> Heroku -> ?(Salesforceなど)

と、うまい具合に連携できるようになると、可能性はまた一段と広がるかと思います。