ソーシャルPLUS の id:mashabow です。今回は、個人で開発している Slack アプリ Rota を OAuth に対応させたときに、へーっと思った話の紹介です。
ちなみにこの Rota は、指定した曜日・時間が来るたびに、ローテーションを順次お知らせしてくれる Slack アプリです。
経緯
この Slack アプリ Rota は、Slack 公式が出している Bolt というフレームワークを使っています。
Slack アプリを任意のワークスペースにインストールしてもらうためには、OAuth に対応させる必要があります。今回は Bolt のおかげで比較的簡単に対応できたのですが、その中で出てきた stateSecret
というオプションがどう使われるのか、いまいちわかりません。Bolt のリファレンスには
CSRF 攻撃を防ぐために OAuth の設定時に渡すことができる、推奨のパラメーター(文字列)。
と説明があるので、OAuth の state
パラメータに関係がありそうですが……。
というわけで、気になって中身をのぞいてみました。
state
パラメータのおさらい
その前に、そもそも OAuth の state
パラメータって何?という方もいるかもしれません。こちらの記事で非常にわかりやすく図解されているのですが、要は CSRF 対策のためのものです。
この記事に倣って図解すると、Slack アプリの場合はこういう関係ですね。
state
パラメータを使うと 3. の前でチェックが入るため、このような CSRF 攻撃を防ぐことができます。
保存をしない state store…?
話を戻して、Bolt の stateSecret
オプションを追いかけてみます。Bolt の OAuth まわりの処理は、@slack/oauth というライブラリで実装されています。@slack/oauth の README を読んでみると、Using a custom state store に説明がありました。
A state store handles generating the OAuth
state
parameter in the installation URL for a given set of options, and verifying thestate
in the OAuth callback and returning those same options.
ふむふむ。ここまでは上で確認したような話ですね。state
パラメータを生成しておいて、コールバック(認可画面からのリダイレクト)のときにその値が正しいかチェックする。そして、@slack/oauth が options と呼んでいるオブジェクト(Slack ワークスペースの ID やスコープの情報が入る)を返します。このようなインターフェイスを、state store と言っているようです。
次に行きます。state store のデフォルトの実装である ClearStateStore
の説明です。
The default state store,
ClearStateStore
, does not use any storage. Instead, it signs the options (using thestateSecret
) and encodes them along with a signature intostate
. Later during the OAuth callback, it verifies the signature.
なんと。state store という名前から「どこかに state
パラメータと options の組を保存するんだろうな」と思っていたんですが、ClearStateStore
ではどちらも保存していないようです。
JWT を使った ClearStateStore
では、ClearStateStore
の実装を見てみましょう。以下に出てくる sign()
と verify()
は、JWT を扱うための npm ライブラリ jsonwebtoken の関数です。
node-slack-sdk/index.ts at @slack/oauth@2.3.0 · slackapi/node-slack-sdk · GitHub
public async generateStateParam(installOptions: InstallURLOptions, now: Date): Promise<string> { return sign({ installOptions, now: now.toJSON() }, this.stateSecret); }
認可画面に遷移する際、generateStateParam()
で state
パラメータを生成します。ここでは、options (上のコードでは installOptions
)と時刻をペイロードとする JWT を生成して、返しています。冒頭で「なにこれ?」と思った stateSecret
は、JWT の署名に使う共通鍵だったんですね。
public async verifyStateParam(_now: Date, state: string): Promise<InstallURLOptions> { // decode the state using the secret const decoded: StateObj = verify(state, this.stateSecret) as StateObj; // return installOptions return decoded.installOptions; }
認可後のリダイレクトでは、verifyStateParam()
で state
パラメータを検証します。ここでは、共通鍵 stateSecret
で JWT の署名を検証しています。ペイロードが改竄されていればここでエラーになり、処理が中断されます。検証が通ったら、ペイロードの options を取り出して返します。
CSRF 対策になっていることを確認
この ClearStateStore
で本当に CSRF 攻撃が防げるのか、確認してみましょう。
まずは通常の攻撃シナリオ。悪い人が自分のワークスペースで認可を行い、リダイレクト URL を取得します(1.)。このリダイレクト URL には、state
パラメータが含まれています。このリダイレクト URL をそのまま A さんに送りつけ、 A さんに踏ませる(2.)とどうなるでしょうか?
Slack アプリの ClearStateStore
は、まず state
パラメータの有効性を検証します(3.)。JWT の署名を検証するわけですが、このシナリオでは、悪い人の JWT は何も改竄されていません。したがって、JWT は有効だと判断され、処理は続行されます。次に、JWT から options を取り出し(4.)、これを使って Slack からアクセストークンを取得します(5.)。Slack からは悪い人のアクセストークンが返ってきますが、Slack アプリはそれを悪い人のアクセストークンだとして(正しく)格納します(6.)。したがって、A さんには何も影響ありません。
別のシナリオも検討してみます。先ほどのシナリオで見たように、Slack アプリは JWT から options を取り出し、アクセストークンを取得・格納していました。JWT の中の options を書き換えれば、悪い人のアクセストークンを A さんのものとして格納できないでしょうか?
はい、これは JWT の署名検証で引っかかるので、不可能ですね。
以上のことから、一般的な state
パラメータの実装と同じく、ClearStateStore
でも CSRF 対策になっていることがわかりました。
まとめ
というわけで、JWT をうまいこと使うと、state
パラメータを OAuth クライアントに保存しておく必要がなくなります。通常の実装だと、生成した state
パラメータを適当な key-value ストアなどに保存しますが、これが不要になるのは楽ですね。ステートレスになるので FaaS との相性もよくなります(とはいえもちろん、取得したアクセストークンはどこかに保存しておく必要があります)。
今回は Bolt を使って Slack アプリのインストール処理を実装しましたが、OAuth まわりの詳細についてはほとんど意識することなく実現でき、かなり楽ちんで快適でした。よく考えられてますね。
おまけ
ソーシャルPLUS では、現在フロントエンドエンジニア・バックエンドエンジニアを募集中です。Shopify アプリのリリースや LINE 社との業務提携を経て、やりたいことがますます盛りだくさんな状況です。
ご興味ありましたら、カジュアル面談にぜひどうぞ!