↪️

Slack API のトークンローテーション完全ガイド

2024/06/04に公開

こんにちは、Slack で公式 SDK 開発と日本の DevRel を担当しております @seratch と申します。

こちらの記事では、Slack アプリのトークンローテーションに関する詳細な説明を日本語でチュートリアル形式でやっていきたいと思います。

トークンローテーションとは

Slack の OAuth アクセストークンは、長らく refresh token がなく、一度発行した access token は明に無効化(auth.revoke APIワークスペースのアプリ管理画面から revoke できます)しない限り、無期限で使える仕様でした。

しかし、2021 年に refresh token とともに access token を発行する機能が提供されるようになりました。

https://api.slack.com/authentication/rotation

この記事ではこれを「トークンローテーション(Token Rotation)」と呼びます。なお、このトークンローテーションはデフォルトでは有効ではなく Slack アプリの管理画面から(または App Manifest の設定によって) opt-in する必要があります。この記事では、この辺の手順・注意点も改めて日本語で紹介していきます。

通常の Slack アプリのトークンローテーションを試してみる

通常の bot token / user token を持つアプリのトークンローテーションの利用法を説明していきます。

Slack アプリを作成〜設定する

こちらの URL にアクセスして、新しいアプリ設定をつくります。以下の動画のように、開発用に使用するホームのワークスペースを選んだら、自動的に設定が読み込まれているはずです。まずはそのままアプリを作ってみてください。

上のリンクには App Manifest という YAML 形式の設定がクエリストリングとして仕込んでありました。その内容を以下に貼っておきます。手動で設定するときは、これをベースに設定してみてください。各項目の説明はこちらを参考にしてください。

_metadata:
  major_version: 1
  minor_version: 1
display_information:
  name: token-rotation-test-app
features:
  bot_user:
    display_name: token-rotation-test-app
oauth_config:
  redirect_urls:
    - https://TOBEUPDATED.ngrok.io/slack/oauth_redirect
  scopes:
    user:
      - chat:write
    bot:
      - app_mentions:read
      - chat:write
settings:
  event_subscriptions:
    request_url: https://TOBEUPDATED.ngrok.io/slack/events
    bot_events:
      - app_mention
  token_rotation_enabled: true

画面から設定する場合は Settings > OAuth & Permissions のページで Opt in します。英語で書かれている通り OAuth の Redirect URL の設定が必須となります(つまり、この管理画面からのインストールではなく OAuth フローでのみ refresh token は発行されます)。

これでアプリの設定はできたので、実際に OAuth フローによる Slack ワークスペースへのインストールを実行してローテーション可能なアクセストークンを取得してみましょう。ここからは Python と Node.js でそれぞれサンプルアプリを動かしていきます。

なお、ここでの例ではソケットモードを使っていませんが、OAuth フロー以外のイベントを処理する部分などにはソケットモードを使用することも可能です。ソケットモードを使いたい場合は、以下のサンプルや記事を参考にしてみてください。

Python でアプリを実装する

Python で Slack の OAuth フロー(アクセストークンを発行して Slack ワークスペースでアプリを有効にする手順)Slack から来たイベントに対してトークンローテーションをしながら応答するアプリを動かしてみましょう。

プロジェクトの新規作成

まず使用する Python のバージョンが 3.6 以上であるかを確認してください。

最近では、システム標準の python3pip3 などのコマンドも 3.6 以上のバージョンだとは思いますが、常に最新のバージョン(この記事投稿時点で 3.10 です)を使用するために pyenv などのツールを使って Python のランタイムを管理することをお勧めします。

その 3.6 以上の Python で、以下のような依存ライブラリを解決したまっさらな環境を作ります。

echo 'slack-bolt>=1.10,<2' > requirements.txt
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Poetry を使うなら、以下のコマンドで同じことができます。

poetry init -n
poetry shell
poetry add slack-bolt

仮想環境を準備できたので、ここでアプリを実装して起動していきます。

環境変数を設定して、アプリケーションを起動

以下のような Python コードを app.py という名前で保存してください。

# より詳細なログを出力するためにログレベルを DEBUG に変更します
import logging
logging.basicConfig(level=logging.DEBUG)

import os
from slack_bolt import App
from slack_bolt.oauth.oauth_settings import OAuthSettings
from slack_sdk.oauth.installation_store import FileInstallationStore
from slack_sdk.oauth.state_store import FileOAuthStateStore

oauth_settings = OAuthSettings(
    # ここの二つの環境変数は必須で、正しく対象のアプリのものが設定されている必要があります
    client_id=os.environ["SLACK_CLIENT_ID"],
    client_secret=os.environ["SLACK_CLIENT_SECRET"],
    # これは管理画面における Bot Scopes です
    scopes=["app_mentions:read", "chat:write"],
    # これは管理画面における User Scopes です
    user_scopes=["chat:write"],
    # Slack ワークスペースへのインストール情報を管理する実装、ここではローカルファイルに保存します
    installation_store=FileInstallationStore(base_dir="./data/installations"),
    # OAuth フローの state パラメーターの永続化の実装、ローカルファイルに保存します
    state_store=FileOAuthStateStore(expiration_seconds=600, base_dir="./data/states"),
    # 以下の設定は後ほど説明します
    # token_rotation_expiration_minutes=60 * 24,
)

app = App(
    # これは OAuth フローでは使いません、Slack からのイベントリクエストの検証に使います
    signing_secret=os.environ["SLACK_SIGNING_SECRET"],
    # OAuth 関連の設定をここで渡します
    oauth_settings=oauth_settings
)

# これはこのアプリの bot user をメンションしたときのイベントに対して応答するリスナーです
@app.event("app_mention")
def handle_app_mention_events(event, client, context, say):
    if context.user_token is not None:
        # インストールしたユーザー自身からのメンションだったとき
        client.chat_postMessage(
            token=context.user_token,
            channel=context.channel_id,
            text=f"<@{event['user']}> のユーザートークンをお預かりしているので、こんなことができます :nerd_face:",
        )
    else:
        say(f"<@{event['user']}> こんにちは!")

if __name__ == "__main__":
    # アプリを http://localhost:3000/ で起動します
    app.start(port=int(os.environ.get("PORT", 3000)))

    # このアプリは 3 つの URL をサーブします
    # - http://localhost:3000/slack/install
    # - http://localhost:3000/slack/oauth_redirect
    # - http://localhost:3000/slack/events

まずは、そのままの状態で動作させてみましょう。この状態で以下の環境変数を設定します。

Settings > Basic Information のページに App Credentials というセクションがありますので、

そこから Client IDClient SecretSigning Secret の値を、それぞれ環境変数 SLACK_CLIENT_IDSLACK_CLIENT_SECRETSLACK_SIGNING_SECRET に設定します。

# OAuth フローのために必要
export SLACK_CLIENT_ID=1234567890.1234567890123
export SLACK_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXX
# Slack からのリクエストか検証するために必要
export SLACK_SIGNING_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXX

そして、アプリを以下のコマンドで起動してみてください。

python app.py

以下のようなログがコンソールに出力されていれば、とりあえずは OK です!

INFO:slack_bolt.App:⚡️ Bolt app is running! (development server)

ngrok で公開された URL を用意して設定に反映

アプリ起動の最後のステップとして公開された URL を準備するために ngrok というツールを使います。使ったことがない方は https://ngrok.com/ でダウンロードして設定してみてください。

ngrok を使えるようになったら、以下のコマンドを実行してください。これによってインターネットに公開された URL に来たリクエストを http://localhost:3000/ で起動している上の Python アプリにフォーワードすることができるようになります。

ngrok http 3000

ngrok の有料プランのアカウントをお持ちの場合は

ngrok http 3000 --subdomain my-token-rotation-app

のように固定されたサブドメインを使うことが可能ですが、無料利用の場合は毎回ランダムに決まります。いずれにせよ、ngrok を起動して表示された https://{あなたのサブドメイン}.ngrok.io のドメイン名で App Manifest に埋め込まれている TOBEUPDATED.ngrok.io を差し替えてください。

YAML を直接編集するモードの場合は、以下のように Edit Manifest ボタンから編集モードに移動して

oauth_config.redirect_urls[0]settings.event_subscriptions.request_url の二箇所を編集します。

settings.event_subscriptions.request_url の方は以下のようなワーニングメッセージが出てくるかと思います。

python app.py でアプリが起動していて、かつ ngrok も手元で動いている前提で Click here to verify というリンクを押すと、検証リクエストが送信されます。疎通ができたら設定完了です。

なお、この YAML 設定のエディターのやり方はちょっと慣れない・やりづらいという場合、左の方に「Revert to the old design」というリンクがありますので、ここから旧来の画面に戻すこともできます。

旧画面から設定する場合は、それぞれ Features > OAuth & Permissions > Redirect URLsFeatures > Event Subscriptions > Request URL を適切に設定してください。その際、URL の末尾が OAuth の Redirect URL は /slack/oauth_redirect で Event Subscriptions の Request URL は /slack/events であることに注意してください。

Slack アプリのインストールを実行

ここまでできたら、ブラウザーを開いて https://{あなたのサブドメイン}.ngrok.io/slack/install にアクセスしてみてください。

以下のようなシンプルなボタンだけの画面が表示されれば OK です。

「Add to Slack」ボタンからインストールを実行していきます。

「Allow」ボタンをクリックして、以下のような画面に遷移すれば成功です。

これでブラウザを使ったインストール作業は完了です。

では、実際インストールによってどのようなトークンを得られたかを見てみましょう。このサンプルアプリでは、インストール結果をローカルファイルに保存するようになっていますので、以下のようなファイルがつくられているはずです。

$ tree data
data
├── installations
│   └── none-T12345678
│       ├── bot-1637909589.684739
│       ├── bot-latest
│       ├── installer-1637909589.684739
│       ├── installer-U12345678-1637909589.684739
│       ├── installer-U12345678-latest
│       └── installer-latest
└── states

これらのファイルについて軽く説明しておきます。none-T12345678 はワークスペースの階層(none のところは Enterprise Grid の場合に enterprise_id が入ります)で、その下のファイルは installer は一度のインストールに含まれる bot やそのユーザー固有の設定、Incoming Webhooks など全てが含まれるファイルで bot は bot に関するものに絞ったものです。履歴の保持がデフォルトでオンになっているので -latest とタイムスタンプごとのファイルがそれぞれ同じ内容で保存しされています。もう一度インストールすると -latest の方は上書きとなります。

インストール情報の中身を見てみてましょう。 cat data/installations/none-T12345678/installer-latest | jq のようにして表示してみてください。ここでの説明に関係ない null の値は削っていますが、以下のようなものが表示されるはずです。

{
  "app_id": "A1234567890",
  "team_id": "T1234567890",
  "team_name": "Acme Corp",
  "bot_token": "xoxe.xoxb-1-xxx",
  "bot_id": "B1234567890",
  "bot_user_id": "U1234567890",
  "bot_scopes": [
    "app_mentions:read",
    "chat:write"
  ],
  "bot_refresh_token": "xoxe-1-xxx",
  "bot_token_expires_at": 1637952233,
  "user_id": "U2222222222",
  "user_token": "xoxe.xoxp-1-xxx",
  "user_scopes": [
    "chat:write"
  ],
  "user_refresh_token": "xoxe-1-xxx",
  "user_token_expires_at": 1637952232,
  "token_type": "bot",
  "installed_at": 1637909589.684739
}

以下のテーブルは、トークンローテーションにおいて知っておくべき項目について簡単に解説しています。

項目 説明
bot_token bot のアクセストークンです。一定時間が経過すると(デフォルトでは 12 時間)期限切れになります
bot_refresh_token bot のアクセストークンを更新するための refresh token です
bot_token_expires_at token が発行されたときの Unix time に oauth.v2.access API から返された expires_in (秒) を加算したものを保持しています
user_token アプリのインストールを実行したユーザー個人のアクセストークンです。一定時間が経過すると(デフォルトでは 12 時間)期限切れになります
user_refresh_token ユーザー個人のアクセストークンを更新するための refresh token です
user_token_expires_at token が発行されたときの Unix time に oauth.v2.access API から返された expires_in (秒) を加算したものを保持しています

なお、コマンドラインで現在の Unix time (秒)を手軽に知りたい場合、以下のコマンドで値を取得することができます。

$ python3 -c 'import time; print(int(time.time()))'
1637916854

トークンローテーションの様子を確認

それでは、このアプリをインストールした Slack ワークスペースの画面を開いてください。テスト用のチャンネルにこのアプリの bot user を招待してください。@token-rotation-test-app をメンションしたら、「招待しますか?」と聞かれますので、そのまま招待してあげてください。

招待したら、そのボットユーザーを再度メンションしてみてください。すると、以下のように自分自身から返信が来るはずです。これは先程発行したトークンのうち、ユーザートークン側を使っているためです。


コードを以下のようにシンプルなものにして起動し直してみてください。

# これはこのアプリの bot user をメンションしたときのイベントに対して応答するリスナーです
@app.event("app_mention")
def handle_app_mention_events(event, say):
    say(f"<@{event['user']}> こんにちは!")

今度は bot からの返事に変わります。

ともあれ、アプリは正常に動いているようです。ここまでのアプリのコンソールログを見てみてください。トークンはローテーションされているのでしょうか?

いえ、デフォルトでは、毎回ローテーションはしない挙動になっています。上の各項目を説明するテーブルでも書いた通り、発行されたトークンは 12 時間程度有効なので、それよりは少し短い時間の間、リフレッシュせずにそのまま使う挙動になっています。これはアプリの実行パフォーマンスにオーバーヘッドを与えないための配慮です。

しかし、(ローテーションの挙動を確認したいときなどのために)この設定をカスタマイズできるようになっています。最初に貼ったコードの以下の部分のコメントアウトを外して起動し直してみてください。

    # 以下の設定は後ほど説明します
    token_rotation_expiration_minutes=60 * 24,

この設定にするとアクセストークンの期限切れ 24 時間前になったら、リフレッシュするようになります。つまり、常にトークンをリフレッシュするようになるということです。

この状態でまた app_mention のイベントを送信すると、先ほどのログとは違う点として、以下の二つの API コールが追加されていることに気づくはずです。

これらがやっていることは、このワークスペースの bot token とこのアクセスユーザーに紐づく user token 両方を oauth.v2.access API を使って refresh しています。その結果は Bolt の内部実装によって自動的に InstallationStore が管理するデータに反映されます。

DEBUG:slack_sdk.web.base_client:Sending a request - url: https://www.slack.com/api/oauth.v2.access, query_params: {}, body_params: {'grant_type': 'refresh_token', 'refresh_token': 'xoxe-1-xxx'}, files: {}, json_body: None, headers: {'Authorization': '(redacted)'}
DEBUG:slack_sdk.web.base_client:Received the following response - status: 200, headers: {}, body: {"ok":true,"access_token":"xoxe.xoxb-1-xxx","expires_in":43200,"refresh_token":"xoxe-1-xxx","token_type":"bot","app_id":"A1234567890","scope":"app_mentions:read,chat:write","bot_user_id":"U1234567890","team":{"id":"T1234567890","name":"Acme Corp"},"enterprise":null,"is_enterprise_install":false}

DEBUG:slack_sdk.web.base_client:Sending a request - url: https://www.slack.com/api/oauth.v2.access, query_params: {}, body_params: {'grant_type': 'refresh_token', 'refresh_token': 'xoxe-1-xxx'}, files: {}, json_body: None, headers: {'Authorization': '(redacted)'}
DEBUG:slack_sdk.web.base_client:Received the following response - status: 200, headers: {}, body: {"ok":true,"access_token":"xoxe.xoxp-1-xxx","expires_in":43200,"refresh_token":"xoxe-1-xxx","token_type":"user","app_id":"A1234567890","scope":"identify,chat:write","user_id":"U2222222222","team":{"id":"T1234567890","name":"Acme Corp"},"enterprise":null,"is_enterprise_install":false}

先程の data/installations が管理するデータを見ると refresh される度に -latest が更新され、また履歴データが増えていくことがわかります(もしこの挙動を変えたい場合は、履歴を全て取る実装をオフにする設定にしてください)。

実装に興味がある方は以下のコードを見てみてください。

今回の例では、一度にデモするために bot token と user token を両方使っていますが、もちろん bot token だけ、user token だけを利用する場合にも TokenRotator はそのまま使用することができます。

また、実際に運用するアプリであればデータベースやより安全な場所にトークンを保持することになるかと思います。Python SDK では、組み込みでファイルに加えて SQLAlchemy、Amazon S3、SQLite3 に対応しています。これらのモジュールのソースコードはこちらを参照してください。Django のアプリケーションでトークンローテーションしたい場合は、こちらの Bolt for Python を使ったサンプルを参考にしてみてください。

Node.js でアプリを実装する

繰り返しを避けるために、上記の Python との差分のところのみ紹介しておきます。

Node.js の場合、プロジェクト自体は

npm init -y
npm install @slack/bolt

でつくります。Node.js の利用可能バージョンは、こちらのバージョン指定で確認してください。

そして index.js として以下のコードを保存します。

const { LogLevel } = require("@slack/logger");
const { App, FileInstallationStore } = require("@slack/bolt");

const app = new App({
  // Basic Information のページから設定してください
  clientId: process.env.SLACK_CLIENT_ID,
  // Basic Information のページから設定してください
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  // これは管理画面における Bot Scopes です
  scopes: ['commands', 'chat:write'],
  // これは管理画面における User Scopes です
  userScopes: ['chat:write'],
  // Slack ワークスペースへのインストール情報を管理する実装、ここではローカルファイルに保存します
  installationStore: new FileInstallationStore({
    baseDir: './data/installations',
    clientId: process.env.SLACK_CLIENT_ID,
  }),
  // これは OAuth フローの state パラメーターの値を生成する際に使われます
  stateSecret: 'my-state-secret',
  // これは OAuth フローでは使いません、Slack からのイベントリクエストの検証に使います
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  // ログレベルを変更しています、指定しない場合はより詳細なログを出力するために DEBUG
  logLevel: process.env.SLACK_LOG_LEVEL || LogLevel.DEBUG,
});

// これはこのアプリの bot user をメンションしたときのイベントに対して応答するリスナーです
app.event("app_mention", async ({ logger, event, say }) => {
  logger.debug("app_mention event payload:\n\n" + JSON.stringify(event, null, 2) + "\n");
  await say(`<@${event.user}> こんにちは!`);
});

(async () => {
  // アプリを http://localhost:3000/ で起動します
  await app.start(process.env.PORT || 3000);
  console.log("⚡️ Bolt app is running!");
})();

// このアプリは 3 つの URL をサーブします
// - http://localhost:3000/slack/install
// - http://localhost:3000/slack/oauth_redirect
// - http://localhost:3000/slack/events

上の Python と同様に環境変数を設定した上で

# OAuth フローのために必要
export SLACK_CLIENT_ID=1234567890.1234567890123
export SLACK_CLIENT_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXX
# Slack からのリクエストか検証するために必要
export SLACK_SIGNING_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXX

以下のコマンドでアプリを起動したら

npx node index.js

Python と同様に ngrok で公開 URL からリクエストがフォーワードされるようにします。インストールが完了すると、若干命名規則は異なるものの、同様にインストール情報が保存されます。

$ tree data
data
└── installations
    └── 3485157640.2764408644502
        └── T1234567890
            ├── app-1637925605512
            ├── app-latest
            ├── user-U1234567890-1637925605512
            └── user-U1234567890-latest

3 directories, 4 files

Node の SDK での rotation の実装は、こちらのコードを参考にしてみてください。

Sign in with Slack のトークンローテーションを試してみる

OpenID Connect 互換の Sign in with Slack のフローで発行されたトークンもトークンローテーションに対応しています。こちらについてもどのように処理すべきか、簡単に紹介します。

以下のような App Manifest で Slack アプリの設定を作ってください。

_metadata:
  major_version: 1
  minor_version: 1
display_information:
  name: token-rotation-siws-test-app
features:
  bot_user:
    display_name: token-rotation-siws-test-app
oauth_config:
  redirect_urls:
    - https://TOBEUPDATED.ngrok.io/slack/oauth_redirect
  scopes:
    user:
      - openid
      - email
      - profile
    bot:
      - commands
settings:
  token_rotation_enabled: true

以下は、Sing in with Slack をハンドリングする Flask の Web アプリケーション例です。必要な依存ライブラリは以下の 2 つです。

pip install flask slack-sdk

大きな違いとして、リフレッシュするための API が openid.connect.token であること、auth.test などのメソッドは使えず openid.connect.userInfo だけを使える点が異なります。ですが、基本的には同じようなフローでトークンを管理するだけで OK です。

なお、TokenRotatorSign in with Slack (OpenID Connect) には対応していないので、以下のコード例のように直接 API を呼び出すコードを必要に応じて書いてください。

# より詳細なログを出力するためにログレベルを DEBUG に変更します
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

import json
import os

# 必要な設定をあらかじめ環境変数に設定しておきます
client_id = os.environ["SLACK_CLIENT_ID"]
client_secret = os.environ["SLACK_CLIENT_SECRET"]
redirect_uri = os.environ["SLACK_REDIRECT_URI"]
# 最低限必要なのは "openid" です
scopes = ["openid", "email", "profile"]

from slack_sdk.web import WebClient
from slack_sdk.oauth import OpenIDConnectAuthorizeUrlGenerator, RedirectUriPageRenderer
from slack_sdk.oauth.state_store import FileOAuthStateStore

state_store = FileOAuthStateStore(expiration_seconds=300)

# https://slack.com/openid/connect/authorize?... な URL を生成します
authorization_url_generator = OpenIDConnectAuthorizeUrlGenerator(
    client_id=client_id,
    scopes=scopes,
    redirect_uri=redirect_uri,
)

# Flask のアプリケーションとして実装していますが、他のフレームワークでももちろん OK です
from flask import Flask, request, make_response

app = Flask(__name__)
app.debug = True

# Sign in with Slack の画面を表示します
@app.route("/slack/install", methods=["GET"])
def oauth_start():
    state = state_store.issue()
    url = authorization_url_generator.generate(state=state)
    return (
        '<html><head><link rel="icon" href="data:,"></head><body>'
        f'<a href="{url}" style="align-items:center;color:#000;background-color:#fff;border:1px solid #ddd;border-radius:4px;display:inline-flex;font-family:Lato, sans-serif;font-size:16px;font-weight:600;height:48px;justify-content:center;text-decoration:none;width:256px"><svg xmlns="http://www.w3.org/2000/svg" style="height:20px;width:20px;margin-right:12px" viewBox="0 0 122.8 122.8"><path d="M25.8 77.6c0 7.1-5.8 12.9-12.9 12.9S0 84.7 0 77.6s5.8-12.9 12.9-12.9h12.9v12.9zm6.5 0c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9v32.3c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V77.6z" fill="#e01e5a"></path><path d="M45.2 25.8c-7.1 0-12.9-5.8-12.9-12.9S38.1 0 45.2 0s12.9 5.8 12.9 12.9v12.9H45.2zm0 6.5c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H12.9C5.8 58.1 0 52.3 0 45.2s5.8-12.9 12.9-12.9h32.3z" fill="#36c5f0"></path><path d="M97 45.2c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9-5.8 12.9-12.9 12.9H97V45.2zm-6.5 0c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V12.9C64.7 5.8 70.5 0 77.6 0s12.9 5.8 12.9 12.9v32.3z" fill="#2eb67d"></path><path d="M77.6 97c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9-12.9-5.8-12.9-12.9V97h12.9zm0-6.5c-7.1 0-12.9-5.8-12.9-12.9s5.8-12.9 12.9-12.9h32.3c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H77.6z" fill="#ecb22e"></path></svg>Sign in with Slack</a>'
        "</body></html>"
    )

# デフォルトのエラー画面を表示するために使います
redirect_page_renderer = RedirectUriPageRenderer(
    install_path="/slack/install",
    redirect_uri_path="/slack/oauth_redirect",
)

# Slack の確認画面から遷移してきたときの URL です
@app.route("/slack/oauth_redirect", methods=["GET"])
def oauth_callback():
    # クエリストリングから code, state パラメーターを取得してチェックします
    if "code" in request.args:
        state = request.args["state"]
        if state_store.consume(state):
            code = request.args["code"]
            try:
                client = WebClient()
                # code パラメーターを使って access_token / refresh_token を取得します
                token_response = client.openid_connect_token(
                    client_id=client_id,
                    client_secret=client_secret,
                    code=code,
                )

                # refresh token を使ってリフレッシュを実施します
                refreshed_token_response = client.openid_connect_token(
                    client_id=client_id,
                    client_secret=client_secret,
                    token=token_response.get("access_token"),
                    refresh_token=token_response.get("refresh_token"),
                    grant_type="refresh_token",
                )
                # リフレッシュされた token が問題ないか openid.connect.userInfo API を呼び出してテスト
                refreshed_user_token = refreshed_token_response.get("access_token")
                user_info_response = client.openid_connect_userInfo(token=refreshed_user_token)
                logger.info(f"openid.connect.userInfo response: {user_info_response}")

                # 実際にはここで access_token / refresh_token を何らかの形で保存することになります

                # あくまでデモとして結果を Web ページに表示しています
                return f"""
            <html>
            <head>
            <style>
            body h2 {{
              padding: 10px 15px;
              font-family: verdana;
              text-align: center;
            }}
            </style>
            </head>
            <body>
            <h2>openid.connect.userInfo response</h2>
            <pre>{json.dumps(user_info_response.data, indent=2)}</pre>
            </body>
            </html>
            """

            except Exception:
                logger.exception("Failed to perform openid.connect.token API call")
                return redirect_page_renderer.render_failure_page(
                    "Failed to perform openid.connect.token API call"
                )
        else:
            return redirect_page_renderer.render_failure_page(
                "The state value is already expired"
            )

    error = request.args["error"] if "error" in request.args else ""
    return make_response(
        f"Something is wrong with the installation (error: {error})", 400
    )


if __name__ == "__main__":
    # export SLACK_CLIENT_ID=111.222
    # export SLACK_CLIENT_SECRET=xxx
    # export FLASK_ENV=development
    # export SLACK_REDIRECT_URI=https://{your-domain}/slack/oauth_redirect
    # python3 app.py

    app.run("localhost", 3000)

    # このアプリは 2 つの URL をサーブします
    # - http://localhost:3000/slack/install
    # - http://localhost:3000/slack/oauth_redirect

Node.js の実装についてはこちらのコードなどを同様に参考にしてみてください。

その他の FAQ

Java のサンプルもありますか?

はい、あります!以下のサンプルコードを確認してみてください。

Bolt を使っていないアプリやスクリプトではどうすればよいですか?

なお、Bolt を使っていないアプリやスクリプト内での利用の場合は、この TokenRotator だけをアプリケーション内で利用することも可能です。

その場合は何らかの InstallationStore の実装find_bot() / find_installation() でトークンを取得し、それを使う前に TokenRotator を呼び出して InstallationStore#save() でリフレッシュしたものを保存しておくという流れになります。

既存のアプリを切り替えるにはどうすればよいですか?

英語の方のドキュメントに詳細がありますが、refresh token とペアになっていない access token を oauth.v2.exchange という API を使って切り替えるという流れになります。この API に client_id、client_secret、token をパラメーターとして渡すと access token + refresh token がレスポンスで返ってきます。

上の Python のコード例で言うと InstallationStore から存在する access token を全部持ってきて、一つずつ切り替えて、それを保存するということになります。

トークンローテーションに opt-in したらすぐに一気にやってしまえるよう、あらかじめスクリプトを用意しておくとよいでしょう。

最後に

トークンローテーションは Slack の OAuth フローを実装しないと試せない機能なので、実際に検証したことがない方も多かったのではないかと思います。

こちらの記事の手順通りにやれば簡単に試せるかと思いますので、ぜひやってみてください 👋

Slack

Discussion