Feedforce Developer Blog

フィードフォース開発者ブログ

OAuth の state パラメータに JWT を使ってステートレスにする

ソーシャルPLUSid:mashabow です。今回は、個人で開発している Slack アプリ Rota を OAuth に対応させたときに、へーっと思った話の紹介です。

ちなみにこの Rota は、指定した曜日・時間が来るたびに、ローテーションを順次お知らせしてくれる Slack アプリです。

f:id:mashabow:20211025113857p:plain

github.com

経緯

この Slack アプリ Rota は、Slack 公式が出している Bolt というフレームワークを使っています。

slack.dev

Slack アプリを任意のワークスペースにインストールしてもらうためには、OAuth に対応させる必要があります。今回は Bolt のおかげで比較的簡単に対応できたのですが、その中で出てきた stateSecret というオプションがどう使われるのか、いまいちわかりません。Bolt のリファレンスには

CSRF 攻撃を防ぐために OAuth の設定時に渡すことができる、推奨のパラメーター(文字列)。

と説明があるので、OAuth の state パラメータに関係がありそうですが……。

というわけで、気になって中身をのぞいてみました。

state パラメータのおさらい

その前に、そもそも OAuth の state パラメータって何?という方もいるかもしれません。こちらの記事で非常にわかりやすく図解されているのですが、要は CSRF 対策のためのものです。

tech-lab.sios.jp

この記事に倣って図解すると、Slack アプリの場合はこういう関係ですね。

f:id:mashabow:20211022222035p:plain

state パラメータを使うと 3. の前でチェックが入るため、このような CSRF 攻撃を防ぐことができます。

f:id:mashabow:20211022222117p:plain

保存をしない 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 the state in the OAuth callback and returning those same options.

ふむふむ。ここまでは上で確認したような話ですね。state パラメータを生成しておいて、コールバック(認可画面からのリダイレクト)のときにその値が正しいかチェックする。そして、@slack/oauth が options と呼んでいるオブジェクト(Slack ワークスペースの ID やスコープの情報が入る)を返します。このようなインターフェイスを、state store と言っているようです。

f:id:mashabow:20211022222224p:plain

次に行きます。state store のデフォルトの実装である ClearStateStoreの説明です。

The default state store, ClearStateStore, does not use any storage. Instead, it signs the options (using the stateSecret) and encodes them along with a signature into state. 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 の署名に使う共通鍵だったんですね。

f:id:mashabow:20211022222430p:plain

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 を取り出して返します。

f:id:mashabow:20211022222510p:plain

CSRF 対策になっていることを確認

この ClearStateStore で本当に CSRF 攻撃が防げるのか、確認してみましょう。

まずは通常の攻撃シナリオ。悪い人が自分のワークスペースで認可を行い、リダイレクト URL を取得します(1.)。このリダイレクト URL には、state パラメータが含まれています。このリダイレクト URL をそのまま A さんに送りつけ、 A さんに踏ませる(2.)とどうなるでしょうか?

f:id:mashabow:20211022222531p:plain

Slack アプリの ClearStateStore は、まず state パラメータの有効性を検証します(3.)。JWT の署名を検証するわけですが、このシナリオでは、悪い人の JWT は何も改竄されていません。したがって、JWT は有効だと判断され、処理は続行されます。次に、JWT から options を取り出し(4.)、これを使って Slack からアクセストークンを取得します(5.)。Slack からは悪い人のアクセストークンが返ってきますが、Slack アプリはそれを悪い人のアクセストークンだとして(正しく)格納します(6.)。したがって、A さんには何も影響ありません。

別のシナリオも検討してみます。先ほどのシナリオで見たように、Slack アプリは JWT から options を取り出し、アクセストークンを取得・格納していました。JWT の中の options を書き換えれば、悪い人のアクセストークンを A さんのものとして格納できないでしょうか?

f:id:mashabow:20211022222547p:plain

はい、これは JWT の署名検証で引っかかるので、不可能ですね。

以上のことから、一般的な state パラメータの実装と同じく、ClearStateStore でも CSRF 対策になっていることがわかりました。

まとめ

というわけで、JWT をうまいこと使うと、state パラメータを OAuth クライアントに保存しておく必要がなくなります。通常の実装だと、生成した state パラメータを適当な key-value ストアなどに保存しますが、これが不要になるのは楽ですね。ステートレスになるので FaaS との相性もよくなります(とはいえもちろん、取得したアクセストークンはどこかに保存しておく必要があります)。

今回は Bolt を使って Slack アプリのインストール処理を実装しましたが、OAuth まわりの詳細についてはほとんど意識することなく実現でき、かなり楽ちんで快適でした。よく考えられてますね。

おまけ

ソーシャルPLUS では、現在フロントエンドエンジニア・バックエンドエンジニアを募集中です。Shopify アプリのリリースや LINE 社との業務提携を経て、やりたいことがますます盛りだくさんな状況です。

open.talentio.com

open.talentio.com

ご興味ありましたら、カジュアル面談にぜひどうぞ!