PHP
laravel
twilio

TwilioからのリクエストであるかチェックするのはFormValidation上で行うとスマートな感じがする

このページについて

Twilioからのwebhookが、本当にTwilioサーバーから来たのか検査するのは、
FormValidationで行うのがイイんじゃない?という話。

が、何でこれを書こうと思ったのか覚えていない。
下書き漁ったらあったので、とりあえず書いてみます。

環境

  • PHP 7.1
  • Laravel 5.5
  • Twilio SDK 5.16

Twilioサーバーから来たリクエストか検査する方法

本家:https://jp.twilio.com/docs/api/security

バリデーションの手順

  1. 対象の完全URL、プロトコルから始まり、全クエリーストリングを含むもの (https から始まり、 ? で指定するものを全て含む) を取得します。
  2. もしリクエストがPOSTの場合、全てのPOSTパラメータをUnix形式の大文字小文字を区別するソート方法で、アルファベット順に並び替えます。
  3. ソートしたPOSTパラメータに、URLの最後まで繰り返しパラメータ名と値(区切り文字を使わない)を当て込んでいきます。
  4. 最終URLの文字列に、ユーザの AuthToken を鍵として(大文字小文字を区別します)、HMAC-SHA1 形式で署名します。
  5. 結果のハッシュ値をBase64でエンコードしてください。
  6. 結果のハッシュ値とTwilio側が署名した X-Twilio-Signatureヘッダを比較します。 これらが一致すれば問題ありません。

だそうです。
めんどくさくてやる気にならない・・・

でも大丈夫、僕たちにはtwilio-sdkがある

Twilio-SDKを使った実装

\Twilio\Security\RequestValidatorクラスを使う

validation.php
public function twilioValidation()
{
    $token = '12345';

    $validator = new RequestValidator($token);
    return $validator->validate($_SERVER['HTTP_X_TWILIO_SIGNATURE'], url()->current(), $_POST);
}

$tokenはTwilioのトークン情報で、本来はDBや設定ファイルに持ってる内容です。
ちなみにこのメソッド、GETでwebhook受けるようにしてると動かないので、
Twilioサーバーからのリクエストかを検査したい箇所は、全てPOSTで受ける必要があります。

RequestValidator::validateについて、
* 第1引数:固定
* 第2引数:webhookのurl 検査という意味だと固定値持たせた方が良いのかもしれないけど、受けたリクエストのURLを突っ込む
* 第3引数:POSTパラメータは全て渡す必要があるため、$_POSTで固定

実装自体はこれだけでOKなのですが、これをどこに置くのが良いか?
というのがこのページの主題です。

バリデーションの置き場

ミドルウェア

最初に思ったのはここ。
Twilioとやり取りするルーター定義に、ミドルウェアで設定して一律やったら楽じゃないかと。
設定はルーティングファイルに書けば良いから、バリデーション目的ならここでも構わなかったりする。
が、ここに書くとPOSTされたデータの吸い上げを、コントローラ側の各処理でやらないとイケないから、
それは案外イケてないのではないかと思って辞めました。

バリデーション(独自ルール)

じゃあ、ということでバリデーションに実装することにしました。
独自のルールとして作るかなぁ、と。
これはあっさり辞めました。
だって、特定のデータに依存しないから。

バリデーション(FormValidation::autorize)

で、辿り着いたのがここ。
Laravelの認可の標準的な考え方とはずれはするけど、
Twilioサーバーからのリクエストなら認可する、という考え方をするならここだな、と。
ここに置けば、コントローラに渡すRequestの属性値も同時に設定できるし、割とスマートかな、と。

TwilioRequest.php
use Illuminate\Foundation\Http\FormRequest;
use Twilio\Security\RequestValidator;

class TwilioRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     * @throws \Exception
     */
    public function authorize()
    {
        $token = '12345';

        $validator = new RequestValidator($token);
        return $validator->validate($_SERVER['HTTP_X_TWILIO_SIGNATURE'], url()->current(), $_POST);
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'CallSid' => 'required',
            'Caller' => 'required',
            'Digits' => '',
            'From' => 'required',
            'To' => 'required',
        ];
    }
}

全容はこんな感じ。

rulesで返している値を見れば、コントローラ側でTwilioのどのパラメータを使っているか判るのがミソ。
使い方は、コントローラのメソッドの引数にこのクラスを書いとくだけ。
webhookで受けるメソッド全てに上記を設定しないといけないけど、
使用しているTwilioのパラメータが何か判ることの方がメリットが大きいと判断しました。

おわりに

Twilioとのやり取りではなくて、
バリデーションの方法について記載してみました。