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を操作するわけです。
勝手に誤解ポイント1
レガシートークンではない最近のトークンは、SlackAppを作成してApp経由でトークンを得る流れになっています。Slack Appを作ってみるとわかりますが、AppのOAuth&Permission
のページに行くと、特に何もしていないのにトークンが表示されています。
上がUser Token
で、下がBot Token
と区別されていますが、どちらもAPIを操作するのに使います(実際に使えます)。自分用のユーザートークンを手にしている状態なので、OAuthする必要性が分かりにくくなってるような気がします。トークンを取る仕組みがOAuth、でも、もうトークン持ってるけど、、、あれ?って感じです。
勝手に誤解ポイント2
ユーザがOAhthをリクエストをするときに、利用可能な権限を絞ります。Slack Appの管理画面にもScopeを設定する画面があり、自分が作るフォームにも権限を設定するパラメータがあり、違いが分からないと混乱します。
この画面が、Appの設定画面の中にあるので、
- Slack Appが必要とする権限をAppのコントロールパネルで設定をし、
- そのScopeでOAuthの認可が行われて、
- Appが必要なScopeが発行したトークンに設定される
と思っていましたが、違いました。
この画面で設定するのは、開発者自身のユーザトークンに対するScopeの設定だけです。他のユーザのトークンは全く関係がないことに気が付くのに、3日ほど掛かりました。
2つの誤解点を簡単に言えば、
開発者自身のトークン発行と権限設定は、Slackが用意したGUIでやらせてあげるけど、ワークスペース内のユーザから権限をもらうときには手続きを踏んでちゃんとやるんだぞ、ということです。
OAuthのトークン取得を実装
App開発者の場合、Appのコントロールパネルにアクセスできてしまうため、自分のトークンは簡単に知ることができます。これだとOAuthのありがたさもトークンの大切さも分かりません。Slackが推奨するOAuthのプログラムを作ってみます。
作るものは、
- Slackに認可リクエストを送るためのFormを作る
- ユーザがユーザの意思でFormを送信
- Formの内容がSlackに届く
- SlackからLaravelにコールバックが届く
- コールバックに含まれる値を読みほどいて、Slack宛てにトークン発行のリクエストを送る
- レスポンスとしてトークンが届く
- トークンを表示
フォームの実装
認可を実行するときに表示するフォームです。全てがHidden要素なので、ユーザがやることはありません。ボタンを押してもらうだけです。client_id
はApp固有の値, team
はワークスペース固有の値ですが、フォームに露出するのは問題ないです。
<!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用のコントローラーを作り、
php artisan make:controller OAuthController
コントローラーの実装をします。
初回表示時にSlackに送り込むフォームを表示するだけのindex
メソッドと、Slackからコールバックされるauth
メソッドの2つを実装しています。
<?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つのコントローラーのメソッドを登録します。
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
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行追加します
SLACK_SIGNING_SECRET=xxx000
SLACK_CLIENT_SECRET=xxx111
Herokuの場合は.envを使わないので、config:setするのを忘れなく。
heroku config:set SLACK_CLIENT_SECRET=xxx111
Herokuにデプロイしましょう。これでプログラム側は準備完了です。
Slack Appの設定
OAuth & Permission
メニューの中段くらいにあるRedirectURLにコールバックのURLを設定します。
Formから送ったredirect_uri
とAppに設定したRedirect URLのHostとPortが一致しているか確認されます。無難に完全一致させておくのがいいと思います。
これで準備完了です。
実行
トークン発行を実行
認可ページを表示
Slackにログインしていると動作がわかりにくくなるので、シークレットモードかプライベートウィンドウを開き、https://slalack-bot.herokuapp.com/oooindex ページを開きます。ボタン1つの画面が出ます。
ワークスペースを決める
ボタンを押すと、https://slack.com/oauth/authorize にリクエストが飛びます。ログイン前の場合、対象となるワークスペースを聞かれます。Slackが出している画面です。
ログインユーザを決める
そのあと、ワークスペースのユーザ情報を聞いてきます。これもSlackが出している画面です。ログイン情報を直接、アプリケーション開発者が受け取るわけではありません。
Slack Appとアカウントの連携を承認する
ログインが完了するとこの画面が出ます。これは、連携しようとしているSlack App(slalackという名前のApp)が、ログインしたユーザの代わりに何をやらかすのか、説明するための画面です。これもSlack側で出しているものです。
トークン発行
「許可する」ボタンを押すと、OAuthControllerのauthメソッドがSlackから呼び出されます。code
を取り出して、client_id
とclient_secret
でトークンの発行を行います。今回のプログラムはトークンを含めて受け取った値を表示しているだけです。
開発者自身のSlackアカウントでトークンの発行フローをやってみるとわかりますが、
OAuth & Permission
で表示されるトークンと同じものが手に入ります。つまり、開発者に限っては、トークン発行のフローを経由せずに(いちいちシステムを作らずに)トークンを入手できるってことです。
ワークスペースにいる別の人(Alice)のアカウントでやってみると、別のトークンが発行されます。あたりまえですね。
今回は実装していませんが、トークンを発行を受けたあとに、ユーザIDとトークンをDBなどの保存しておく必要があります。
発行済みトークンの確認
自分がどのアプリケーションにどの権限を渡しているのかを確認できます。
https://api.slack.com/tokens
下のほうにAppで許可した権限が一覧で出てきます。トークンの値を確認することはできません。
まとめ
ワークスペース内のユーザが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