これ見て半日もかからないだろうとたかをくくっていたら罠だらけでした…
- Zoomのapp作成時に要求されるvalidationが通過しない。
- メッセージの検証に必要なHTTPヘッダがGASまで届かない。
前者は失敗したということしか表示されず、GASで書いた同等計算とNode.jsのハッシュが同じ対象で同じ値を算出していることを確認したりしてあっという間に半日が溶けました。Zoom – Google なので状況の把握も部分的かつ面倒。確認用に動かしていたNode.jsをproxyにしてしまうのがマシそうということで簡単に実装して動作を眺めていてようやく問題はハッシュではないと確信し、前出のValidate your webhook endpointであてはまるのは3秒ルールくらいですが、後にredirectionだという説明をしてくださってる方がいて実際にproxyをリダイレクトするように仕込んでみると確かに301や302返してもZoom側はPOSTしなおしてくれないことが確認できました。厳格。Google側は確認するまでもなくPOSTで返答にコンテンツがある場合は302を返すという記述が散見されました。なるほど。失敗の理由を簡単でよいので表示してもらえるとありがたかったです。後でfeature requestの仕方探します。動作する簡易Webhook proxyを作る動機となったのでよしとします。
というわけでvalidationの返事はproxyがすることにします。その後同じrequestをGASにforwardし、その結果が合わない場合は再度endpoint validationが通過するまではforwardしないようにしてみました。
問題はそれ以外のイベントで、GAS環境は認証なしでリクエストを受け付けるように設定しないといけないので、リクエストの正当性の確認が必須です。まずはリモートのIPアドレスのCIDRリストを、と仕込み始めてしまいそうですがなんと必要なHTTPヘッダフィールドへのアクセス方法がなくリモート情報がありません。残すところリクエストのハッシュの検証だけでもしないといけないですがZoomはハッシュと味付けのタイムスタンプをHTTPヘッダに載せてきます。もし Zoom – Google 直で先のvalidationが通過するようになったとしてもこれでは各メッセージの検証はどうやってもできずにとても危険です。Zoomの仕組みをそのまま利用して必要なHTTPフィールドの値をURLに載せて送ることにしてみます。リプレイ攻撃の耐性のためにtimestampの許容範囲はproxyでは15秒前、GAS環境では30秒前をデフォルトとしました。後者はcold startしたとおぼしきときに10秒以上かかるケースが頻繁にありました。
というわけで出来上がりがこれです。GASは参加者の記録をスプレッドシートに追記していくものを書いてみました。proxyから同時にSlackに通知するとかもすぐに仕込めるなあというところでいったん満足です。
余談ですが公式サンプルのハッシュ算出部分、いきなり
// construct the message string
const message = v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}
とJSONをシリアライズしなおして算出しています。破綻しないことをお祈りしております…
