Slack Appに挑戦(2) - Request Verification

SlackaAppをまじめに学ぶ（2回目）

#はじめに
[前回](https://qiita.com/kanaxx/items/a12a523ca3143b5822b8)、Heroku上にウェブアプリケーションを構築し、Slackワークスペースからスラッシュコマンドを受け取るところまでやりました。インターネット上にSlackからのリクエストを受け付ける先を公開したわけですが、送信元がSlackに限られるわけではなく、悪意があれば誰でもリクエストを送ることができるので、怖いですね。
ということで、公開したAPIへのリクエストが、自分が作ったSlack Appから送られたものかを検証する方法を実装しておきます。

ここに英語で書いてあることを、PHPとLaravelでやっているだけです。
https://api.slack.com/docs/verifying-requests-from-slack

##今回使う環境

+ Heroku
  + PHP 7.3
  + Laravel 6.0.3
  + Clear DB
+ Windows
  + PHP 7.2.11
  + Laravel 6.0.3
  + Maria DB  Ver 15.1

前回どおり、基本的にはWindowsでの作業です。

#やってみよう
検証方法は2つあります。1つ目は推奨されていないのでさらっと行きます

##1. Verification Token

この方法は推奨されていません。
何もしないより良いかなと思いますが、in comming monthで完全になくなると言っているので危ないかもしれません。

>Verification token deprecation
We'll continue allowing apps to use verification tokens for now. However, we will retire them completely in coming months. We strongly recommend switching to request signing as soon as possible.

Basic InformationにあるVerification Tokenの値をメモします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/f2d7816f-786e-28a8-f187-2b200ca5d4da.png)

スラッシュコマンドのパラメータの一つとして、```Verification Token```の値がそのまま渡されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/7ba25590-e103-2e2f-2937-9f23ba3fa291.png)


自分で作ったアプリケーションは、Basic Informationに表示されているTokenの値と、リクエストで渡ってきたTokenの値が一致するかを調べるだけです。一致するときは、正しいSlackのサーバーからきたと見なすことになります。Verification Tokenの値を秘密にしておく必要があります。


##2. Signing Secret

こちらが新しく推奨されている```Signing Secret```という方法です。

検証手順は以下のとおりです。

1. ```Signing Secret```をBasic Informationから調べる
2. リクエストヘッダーの```X-Slack-Request-Timestamp```を取得する
3. リクエストボディを無加工で取得する
4. Slackが決めたルールで文字列を結合する
5. ```Signing Secret```と4で作った文字列から、sha256のハッシュ値をとる
6. リクエストヘッダーの```X-Slack-Signature```で渡ってきた値と比較する
7. 一致するときは正しいリクエストとみなす

これをLaravelで実装していきます。

###Controller
まずは、新しいContoroller(VerificationController.php)を作り、verifyメソッドとして実装します。


```php:app/VerificationController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Log;

class VerificationController extends Controller
{
    public function verify(Request $request){
        $timestamp = $request->header('x-slack-request-timestamp');
        $signature = $request->header('x-slack-signature');
        $requestBody = $request->getContent();
        $secret = config('app.slack.signing_secret');

        $sigBasestring = 'v0:' . $timestamp . ':' . $requestBody;
        $hash = 'v0=' . hash_hmac('sha256', $sigBasestring, $secret);

        $debug = ['Timestamp'=>$timestamp,
            'Body'=>$requestBody,
            'Sig'=>$sigBasestring,
            'Secret'=>substr_replace($secret,'*',5),
            'Hash'=>$hash,
            'Signature'=>$signature,];


        return response()->json($debug);
    }

}

```

特に解説は不要ですね。
取得した値と計算した結果をreturnするだけのものです。


###route設定

作ったメソッドを```/api/verify-test```でアクセスできるように登録します。エンドポイントは、https://slalack-bot.herokuapp.com/api/verify-test になります。

```routes/api.php
Route::any('/verify-test', 'VerificationController@verify');
```

###config/.env設定

.envにSigning Secretの値を登録する。
HerokuのConfigにセットするのを忘れずに。

```:.env
SLACK_SIGNING_SECRET=70cad**********
```

Laravelのconfig経由で、Signing Secretが取れるように登録します。

```php:config/app.php
    /*
    |--------------------------------------------------------------------------
    | Class Aliases
    |--------------------------------------------------------------------------
    |
    | This array of class aliases will be registered when this application
    | is started. However, feel free to register as many as you wish as
    | the aliases are "lazy" loaded so they don't hinder performance.
    |
    */

    'aliases' => [

        'App' => Illuminate\Support\Facades\App::class,
        //省略
    ],

    //ここ追加
    'slack' => ['signing_secret'=>env('SLACK_SIGNING_SECRET')],

];

```

Herokuにデプロイすると、以下のURLでアクセスできるようなります。
https://slalack-bot.herokuapp.com/api/verify-test


###スラッシュコマンド登録
Slackから/verify-testで呼び出すコマンドを登録します。RequestURLは https://slalack-bot.herokuapp.com/api/verify-test に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/51dce150-13a3-33de-4df5-3e1d37409696.png)


###動作確認

slackのメッセージボックスから/verify-testを呼びます。追加のメッセージは何でも構いません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/74f010d6-0ae7-ed34-f04d-e64df42b8de7.png)

呼びかけに応じて、リプライが来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/289dafe7-0974-3248-34a8-4e534b453cdf.png)

だらだらとJSONがかえってきていますが、大事なのは最後の2つだけです。Hashが自分で計算して作った値、SignatureがSlackから送られてきた値です。両者が一致しているので実装方法に誤りはなさそうです。
一致しているので、このリクエストはSlackから送られてきた正規なリクエストとみなして良いということです。

```json
"Hash":"v0=098ea4338850dc6004b89d24f2a2d06ac12f6f138121df9ad0a74bec8db32521"
"Signature":"v0=098ea4338850dc6004b89d24f2a2d06ac12f6f138121df9ad0a74bec8db32521"
```


#LaravelのMiddleware化する
各コントローラのメソッドでverifyメソッドと同じことをするのは面倒なので、Laravelのmiddlewareに実装して自動適用するように変えてみます。

##Middlewareクラスの生成
artisanコマンドでひな型を作ります。

```
php artisan make:middleware SlackRequestVerification
Middleware created successfully.
```

###検証ロジックを実装
作られたひな形の```handle```メソッドに検証ロジックを実装します。VerificationControllerに書いた処理をそのまま持ってくるだけです。

```php:App\Http\Middleware\SlackRequestVerification
<?php

namespace App\Http\Middleware;

use Closure;
use Log;

class SlackRequestVerification
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $timestamp = $request->header('x-slack-request-timestamp');
        $signature = $request->header('x-slack-signature');
        $requestBody = $request->getContent();
        $secret = config('app.slack.signing_secret');

        $sigBasestring = 'v0:' . $timestamp . ':' . $requestBody;
        $hash = 'v0=' . hash_hmac('sha256', $sigBasestring, $secret);

        $debug = ['Timestamp'=>$timestamp,
            'Body'=>$requestBody,
            'Sig'=>$sigBasestring,
            'Secret'=>substr_replace($secret,'*',5),
            'Hash'=>$hash,
            'Signature'=>$signature,];
        
        \Log::debug($debug);

        //不一致なら500で終わり
        if( $hash !== $signature ){
            return response()->json([
                'ok'=>false,
                'error' => 'signature error',
            ], '500');
        }

        //念のため、verificationをした証拠を入れておく
        $verified = [
            'slack_signing_verification'=>true, 
            'slack_use_secret'=>substr_replace($secret,'*',5),
            'slack_verify_hash'=>$hash,
        ];
        $request->merge($verified);

        //次の処理へ
        return $next($request);
    }
}

```

###Middlewareの登録
HttpのKernelに登録します。今回はSlackからのリクエストはすべて```/api/*```で受けるので、apiのmiddlewareグループに登録しました。

```php:app/Http/Kernel.php
    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            //ここに追加
            \App\Http\Middleware\SlackRequestVerification::class,

            'throttle:60,1',
            'bindings',
        ],
    ];
```

ここまでの変更をHerokuにデプロイします。


##動作確認
###正常動作
前回作った```/debug```を呼び出ししてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/ce3c9ee1-da10-ffe1-4278-2c1d90fe68c9.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/86ef8916-0989-28d7-3241-7a32e2eaa266.png)


無事、middlewareを経由して処理されて、Verificationが正常に終わり、Controllerに処理が伝わりました。middlewareでrequestに追加した3つの値も確認できました。

###エラー
ブラウザでGETのリクエストを送ってみると、500 HTTPレスポンスとJSONエラーメッセージが返ってきます。
https://slalack-bot.herokuapp.com/api/debug

```json
{"ok":false,"error":"signature error"}
```

前回公開した/api以下のエンドポイントは、Slack経由の正しいリクエスト以外は全て500エラーになります。

#まとめ
Slack Signing VerificationをLaravelアプリに実装しました。Slackのサーバーから正しく届いたメッセージであることを確認する方法を入れたので、安心感が増しました。

#参考資料
https://qiita.com/girlie_mac/items/21fedcc6ce07aa44a670

https://api.slack.com/docs/verifying-requests-from-slack