Slack Appに挑戦(4) - OAuthとトークン

Slack Botを真面目に学ぶ
今回はSlackのOAuthをやってみます。

#はじめに
SlackのWebAPIを使うときにはAPI用のTokenを求められます。Tokenを取るためにSlackではOAuthという仕組みを提供していますが、いまいち理解できていませんでした。ということで、SlackのOAuthをちゃんとやってみようと思います。
https://api.slack.com/docs/oauth に英語で書いてあることをPHP Laravel + Herokuでやっているだけです。

##環境

+ Heroku
  + PHP 7.3
  + Laravel 6.0.3
  
#OAuthとは
OAuthについては、詳しい資料がたくさんあるので省略します。

簡単に言ってしまうと、
Slackワークスペースに登録したSlack Appが、ワークスペースユーザのIDとパスワードを預からずに、ユーザの代理でSlackを操作する　ために、ユーザとSlackから許可を受ける仕組み、といいましょうか。
ユーザから正しく許可を得た結果、App作者はSlackからトークンを受け取ることになります。このトークンを使ってユーザの代わりにSlackを操作するわけです。

##勝手に誤解ポイント１
レガシートークンではない最近のトークンは、SlackAppを作成してApp経由でトークンを得る流れになっています。Slack Appを作ってみるとわかりますが、Appの`OAuth&Permission`のページに行くと、特に何もしていないのにトークンが表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/0e43483c-5022-c1b7-052c-57e5b600831d.png)

上が`User Token`で、下が`Bot Token`と区別されていますが、どちらもAPIを操作するのに使います（実際に使えます）。自分用のユーザートークンを手にしている状態なので、OAuthする必要性が分かりにくくなってるような気がします。トークンを取る仕組みがOAuth、でも、もうトークン持ってるけど、、、あれ？って感じです。

##勝手に誤解ポイント２
ユーザがOAhthをリクエストをするときに、利用可能な権限を絞ります。Slack Appの管理画面にもScopeを設定する画面があり、自分が作るフォームにも権限を設定するパラメータがあり、違いが分からないと混乱します。

**SlackのScope設定画面**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/a375de56-cd9c-c26b-099c-86e6d8446fe3.png)

この画面が、Appの設定画面の中にあるので、

 - Slack Appが必要とする権限をAppのコントロールパネルで設定をし、
 - そのScopeでOAuthの認可が行われて、
 - Appが必要なScopeが発行したトークンに設定される
 
と思っていましたが、違いました。

この画面で設定するのは、開発者自身のユーザトークンに対するScopeの設定だけです。他のユーザのトークンは全く関係がないことに気が付くのに、3日ほど掛かりました。


:sunny:2つの誤解点を簡単に言えば、
開発者自身のトークン発行と権限設定は、Slackが用意したGUIでやらせてあげるけど、ワークスペース内のユーザから権限をもらうときには手続きを踏んでちゃんとやるんだぞ、ということです。

#OAuthのトークン取得を実装
App開発者の場合、Appのコントロールパネルにアクセスできてしまうため、自分のトークンは簡単に知ることができます。これだとOAuthのありがたさもトークンの大切さも分かりません。Slackが推奨するOAuthのプログラムを作ってみます。

作るものは、

1. Slackに認可リクエストを送るためのFormを作る
2. ユーザがユーザの意思でFormを送信
3. Formの内容がSlackに届く
4. SlackからLaravelにコールバックが届く
5. コールバックに含まれる値を読みほどいて、Slack宛てにトークン発行のリクエストを送る
6. レスポンスとしてトークンが届く
7. トークンを表示


##フォームの実装

認可を実行するときに表示するフォームです。全てがHidden要素なので、ユーザがやることはありません。ボタンを押してもらうだけです。`client_id`はApp固有の値, `team`はワークスペース固有の値ですが、フォームに露出するのは問題ないです。

https://api.slack.com/docs/oauth#step_1_-_sending_users_to_authorize_and_or_install

```html:resources/oauth/index.blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Slalack Bot OAuth</title>
    </head>
    <body>
        <div class="flex-center position-ref full-height">

            <div class="content">
                <div class="title m-b-md">
                    slalack-bot OAuth
                </div>
                <form action="https://slack.com/oauth/authorize" method="GET">
                    <input type="hidden" name="scope" value="identity.basic">
                    <input type="hidden" name="client_id" value="{{config('slack.slalack.client_id')}}">
                    <input type="hidden" name="redirect_uri" value="{{config('slack.slalack.redirect_url')}}">
                    
                    <input type="hidden" name="state" value="slalalala">
                    <input type="hidden" name="team" value="{{config('slack.team')}}">
                    <input type="submit" value="認証ページへ">
                </form>
            </div>
        </div>
    </body>
</html>

```

`scope`パラメータは、コントロールパネルで設定したScopeと関係がありません。このフォームをPOSTするユーザが何を許可するか（トークンを知る人にどれだけの自由度を与えるのか）を、全部並べておく必要があります。

Scopeのリストはこちらにあります。複数のScopeを同時に指定する場合には、スペースで区切ります。
https://api.slack.com/docs/oauth-scopes


##コントローラーの実装

いつも通り、`artisan`コマンドでOAuth用のコントローラーを作り、

```bash:command
 php artisan make:controller OAuthController
```

コントローラーの実装をします。
初回表示時にSlackに送り込むフォームを表示するだけの`index`メソッドと、Slackからコールバックされる`auth`メソッドの2つを実装しています。

```php:OAuthController
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class OAuthController extends Controller
{
    const SLACK_OAUTH_URL = 'https://slack.com/api/oauth.access';

    //フォームを表示するだけ
    public function index(Request $request ){
        return view('oauth.index');
    }

    //Slackのredirect_urlからコールバックされるメソッド
    public function auth(Request $request){
        \Log::info($request);
        $code = $request->input('code');
        $state = $request->input('state');

        if($state != 'slalalala'){
            return response('Something Wrong!', 500);
        }
        if($request->filled('error')){
            return response('slack returned error.',500);
        }

        $guzzile = new \GuzzleHttp\Client();

        $params = [];
        $params['code'] = $code;
        $params['client_id'] = config('slack.slalack.client_id');
        $params['client_secret'] = config('slack.slalack.client_secret');

        $option = [];
        $option['form_params']=$params;

        $response = $guzzile->post(self::SLACK_OAUTH_URL, $option);
        $body = $response->getBody();
        \Log::info($body);

        $data = json_decode( (String)$body, true);
        if(!$data['ok']){
            return response('OAuth request returns error!', 500);
        }
        $token = $data['access_token'];
        $userName = $data['user']['name'];
        $userId = $data['user']['id'];
        $teamId = $data['team']['id'];
        \Log::info([$token, $userName, $userId, $teamId]);

        var_dump((String)$body);

        //この先でトークン情報をDBなどに保存しておくこと
        //失うともう一度OAuthする必要がでてくる

        //最後にLaravelのルールでViewの情報を返す。（今回はサンプルなのでOKだけ戻す）
        return "ok";
    }
}

```

`auth`メソッドはSlackから呼び出されます。渡された`code`の値と`client_secret`, `client_id`を使って、/oauth.accessにリクエストを送り、トークンの発行しています。stateの一致は適当な実装ですが、optionなので、やらなくてもOKです。細かい実装手順とパラメータはこちらに書いてあります。
https://api.slack.com/docs/oauth#step_3_-_exchanging_a_verification_code_for_an_access_token


##routesの設定

今回はHTMLでのウェブアプリケーションになるので、Laravelの`web.php`のルートに2つのコントローラーのメソッドを登録します。

```php:routes/web.php
Route::get('/oooindex', 'OAuthController@index');
Route::get('/oooauth', 'OAuthController@auth');
```

https://slalack-bot.herokuapp.com/oooindex が入力用ページ、
https://slalack-bot.herokuapp.com/oooauth がSlackからコールバックされるURLです。

##configの設定
configディレクトリ以下にslack用のコンフィグを作りました。Laravel的には、config('slack.slalack.client_id')のパスで参照します。

`client_id`はAppで固有な値で変わることがないので、設定ファイルに直接書きました。そのままGITにコミットしても問題ない値です。漏れると問題のある値`client_secret`は.envファイルから取るようにしておきます。
設定ファイルに`signing_secret`も記載がありますが、前回の名残でOAuthには関係ありません。

```php:config/slack.php
<?php
return [
    'team'=>'TDWRKFV0S',
    'slalack' =>[
        'signing_secret'=>env('SLACK_SIGNING_SECRET'),
        'client_secret'=>env('SLACK_CLIENT_SECRET'),
        'client_id'=>'472869539026.770655600150',
        'redirect_url'=>'https://slalack-bot.herokuapp.com/oooauth',
    ],
];

```

##.env
2行追加します

```bash:.env
SLACK_SIGNING_SECRET=xxx000
SLACK_CLIENT_SECRET=xxx111
```

Herokuの場合は.envを使わないので、config:setするのを忘れなく。

```bash:command
heroku config:set SLACK_CLIENT_SECRET=xxx111
```

Herokuにデプロイしましょう。これでプログラム側は準備完了です。

##Slack Appの設定
`OAuth & Permission`メニューの中段くらいにあるRedirectURLにコールバックのURLを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/30160f8f-a127-a15c-148c-66bd3c55d6ad.png)

Formから送った`redirect_uri`とAppに設定したRedirect URLのHostとPortが一致しているか確認されます。無難に完全一致させておくのがいいと思います。

https://api.slack.com/docs/oauth#redirect_urls


これで準備完了です。

#実行
##トークン発行を実行
###認可ページを表示
Slackにログインしていると動作がわかりにくくなるので、シークレットモードかプライベートウィンドウを開き、https://slalack-bot.herokuapp.com/oooindex ページを開きます。ボタン1つの画面が出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/f0860e52-49eb-b265-d7a4-b8a87dbe4330.png)

###ワークスペースを決める
ボタンを押すと、https://slack.com/oauth/authorize にリクエストが飛びます。ログイン前の場合、対象となるワークスペースを聞かれます。Slackが出している画面です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/229ae58f-5d65-8ff7-6cf9-56097bdd75be.png)


###ログインユーザを決める
そのあと、ワークスペースのユーザ情報を聞いてきます。これもSlackが出している画面です。ログイン情報を直接、アプリケーション開発者が受け取るわけではありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/e7161cf9-aff5-ac1f-f9be-e83a3a663948.png)

###Slack Appとアカウントの連携を承認する
ログインが完了するとこの画面が出ます。これは、連携しようとしているSlack App(slalackという名前のApp)が、ログインしたユーザの代わりに何をやらかすのか、説明するための画面です。これもSlack側で出しているものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/6a5451f0-42a4-6ec4-3683-a25563d4402c.png)

###トークン発行
「許可する」ボタンを押すと、OAuthControllerのauthメソッドがSlackから呼び出されます。`code`を取り出して、`client_id`と`client_secret`でトークンの発行を行います。今回のプログラムはトークンを含めて受け取った値を表示しているだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/2e60500c-a6bb-3089-5e0b-a781b2410c87.png)

:white_check_mark: 開発者自身のSlackアカウントでトークンの発行フローをやってみるとわかりますが、`OAuth & Permission`で表示されるトークンと同じものが手に入ります。つまり、開発者に限っては、トークン発行のフローを経由せずに（いちいちシステムを作らずに）トークンを入手できるってことです。

---
ワークスペースにいる別の人（Alice）のアカウントでやってみると、別のトークンが発行されます。あたりまえですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/5f1d6c63-50fd-8869-86e1-8640fd0eecc1.png)

:warning: 今回は実装していませんが、トークンを発行を受けたあとに、ユーザIDとトークンをDBなどの保存しておく必要があります。

##発行済みトークンの確認
自分がどのアプリケーションにどの権限を渡しているのかを確認できます。
https://api.slack.com/tokens
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/a97c6198-2efc-fe40-efb1-a693b069c87a.png)

編集ボタンを押すと、Appの詳細が確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/3a98c6af-ceb2-71e5-93bd-c22fcbf591cc.png)

下のほうにAppで許可した権限が一覧で出てきます。トークンの値を確認することはできません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168315/4ba9c722-1f6e-c512-e610-cbf27289787f.png)


#まとめ
ワークスペース内のユーザがBotに話しかけても、/コマンドを実行しても、Botか開発者のアカウントが反応するアプリケーションの場合は、各ユーザのトークンを発行する必要はありません。Botに話かけたら、話しかけたユーザとして発言したりするときには必要になります。会社のワークスペースで遊ぶくらいの場合は必要ないかもですね。

現状のSlackのトークンには有効期限がありません。リフレッシュトークンによるトークン再発行の仕組みは現在開発中とのことです。（2019年10月14日現在、使うことはできませんでした）
https://api.slack.com/docs/rotating-and-refreshing-credentials


#参考資料
https://qiita.com/TakahikoKawasaki/items/e37caf50776e00e733be
https://qiita.com/subarunari/items/3e4c6060fcefd4c65257
https://qiita.com/dbgso/items/a95a3364d9a8c67f3387