Slack連携アプリのセキュリティを高めたくて、リクエストの署名検証に関して調べてみました。また、調べた内容を元にサンプルアプリを作ってみました。
モチベーション
コロナを機にリモートワークが普及してから、仕事でのコミュニケーションにSlackを使うようになり、便利なSlack連携アプリの使用頻度が上がっています。ただ、Slack連携アプリが公開しているリクエストURLはどこからでもアクセスできるため、リスク対策を十分にする必要性を感じています。
何も対策をしなければ第三者からの攻撃に対して無防備になってしまうため、Slackのリクエスト検証の仕組みに関して正しく知っておきたいというのがモチベーションです。
全体像
この記事では、SlackからのHTTPリクエストを署名検証する処理を含む以下のアプリを実装していきます。
Slackメッセージのショートカットを実行すると、Slackから送られてくるペイロードを受け取り、署名を検証し、問題がなければSlackチャネルにメッセージ投稿する、というシンプルなアプリです。
参考までに、SlackからのHTTPリクエストに含まれるペイロードは以下参照先のデータ形式になります。
Making messages interactive | Slack
Slackでのアプリ設定
Slackチャネルにショートカットを追加し、アプリと連携できるように設定を行なっていきます。
ショートカット設定
Slack APIからアプリを作成
アプリが作成できたら左側メニューの「Interactivity & Shortcuts」を選択し、下図中の①〜③をそれぞれ設定します。
- ①InteractivityをOnにする
- ②Request URLを入力
- ③「Create New Shortcut」を押す
ショートカットの設定ダイアログで「On message」を選択し、メッセージショートカットを設定していきます。
ショートカットの名前、説明、Callback IDにそれぞれ任意の値を設定します。
[n:alt=Slackのショートカット作成-詳細画面]
スコープ追加
画面左メニューの「OAuth & Permissions」を押し、Bot Token Scopesに「chat:write」を追加し、連携アプリの処理結果をSlackチャネルに通知できるようにします。
ワークスペースとチャネルにアプリを追加
「OAuth & Permissions」の「Install to workspace」を押します。
以下2点を控えたら、チャネルのIntegrationの設定から作成したアプリを追加します。
- Basic Information - App Credentials - Signing Secret
- 署名を検証するためのシークレット
- OAuth & Permissions - OAuth Tokens for Your Workspace - Bot User OAuth Token
- Slackチャネルにメッセージを投稿するためのOAuthトークン
試しに追加したショートカットを実行してみる
Slackチャネルの任意のメッセージに対して先ほど追加した「Verify Request」というショートカットを実行できます。
ただ、今はサーバー側のGoアプリが動いていないので、HTTPリクエストが処理されず、エラーが発生します。
アプリの実装(Goのコード)
コードはmain.goとmiddleware.goの2ファイルに分けて記載しています。
これらはそれぞれ以下の役割を担っています。
main.go
- HTTP80番ポートを待ち受ける
- Slackからパス
/verifing-request
にHTTPリクエストがあれば、handler関数(handleInteraction
)を実行する - ショートカット独自の処理を行うhandler関数(
handleInteraction
)を定義
middleware.go
- handlerの処理に介入して、署名検証する
使用ライブラリ
slack-go
というライブラリを使用して実装しています。
また、今回作成したサンプルアプリは slack-go
の以下のexampleコードを参考にして作りました。署名検証部分をmiddleware
共通処理として切り出していて、保守しやすい綺麗な実装になっていて、参考にするのに良いサンプルのように感じています。
main.go
ショートカット独自の処理を行う handler(handleInteraction)
関数では、SlackからのHTTPリクエストからペイロードを取り出し、メッセージアクションというInteractionTypeであればチャネルに「メッセージアクションが実行されました!」というメッセージ投稿を行っています。
先ほど控えた Bot User OAuth Token
を環境変数 SLACK_BOT_OAUTH_TOKEN
に設定しています。Slack APIクライアント初期化の際に、この環境変数を使用しています。
package main import ( "encoding/json" "fmt" "log" "net/http" "os" "github.com/slack-go/slack" ) func main() { mux := http.NewServeMux() mux.HandleFunc("/verifing-requests", handleInteraction) middleware := NewSecretsVerifierMiddleware(mux) log.Fatal(http.ListenAndServe(":80", middleware)) } func handleInteraction(w http.ResponseWriter, r *http.Request) { fmt.Println("[START]handleInteraction") api := slack.New(os.Getenv("SLACK_BOT_OAUTH_TOKEN")) var payload *slack.InteractionCallback err := json.Unmarshal([]byte(r.FormValue("payload")), &payload) if err != nil { log.Println(err) w.WriteHeader(http.StatusInternalServerError) return } switch payload.Type { case slack.InteractionTypeMessageAction: api.PostMessage(payload.Channel.ID, slack.MsgOptionText("メッセージアクションが実行されました!", false)) } fmt.Println("[END]handleInteraction") }
middleware.go
HTTPリクエストのボディ・ヘッダーを読み込み、環境変数 SLACK_SIGNING_SECRET
を元に署名検証しています。署名検証の具体的な内容は次の<解説篇>で共有したいと思います。
環境変数 SLACK_SIGNING_SECRET
には先ほど控えたSigning Secret
を設定します。
package main import ( "bytes" "fmt" "io/ioutil" "net/http" "os" "github.com/slack-go/slack" ) type SecretsVerifierMiddleware struct { handler http.Handler } func NewSecretsVerifierMiddleware( h http.Handler) *SecretsVerifierMiddleware { return &SecretsVerifierMiddleware{h} } func (v *SecretsVerifierMiddleware) ServeHTTP( w http.ResponseWriter, r *http.Request) { fmt.Println("[Start]ServeHTTP") body, err := ioutil.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) return } r.Body.Close() r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) sv, err := slack.NewSecretsVerifier(r.Header, os.Getenv("SLACK_SIGNING_SECRET")) if err != nil { w.WriteHeader(http.StatusBadRequest) return } if _, err := sv.Write(body); err != nil { w.WriteHeader(http.StatusInternalServerError) return } if err := sv.Ensure(); err != nil { w.WriteHeader(http.StatusUnauthorized) return } fmt.Println("before hander.ServeHTTP") v.handler.ServeHTTP(w, r) fmt.Println("[END]ServeHTTP") }
再度Slack上からショートカットを実行してみる
Goコードのアプリをサーバー上にデプロイして、SlackからのHTTPリクエストを処理できるようになったところで、再度「Verify Request」ショートカットを実行してみます。
メッセージ投稿が表示され、期待した結果になっていることが分かります。
コンソール出力は以下のようになりました。
[Start]ServeHTTP before hander.ServeHTTP [START]handleInteraction [END]handleInteraction [END]ServeHTTP
SecretsVerifierMiddleware
の ServeHTTP
メソッドが実行され、SlackからのHTTPリクエストの署名検証が行われた後、ショートカット独自のhandlerが動いていることを確認できます。
次の記事では、署名検証の内容を読み解いてみます。
simple-minds-think-alike.moritamorie.com
参考コード
コードの方はGithubにも挙げてみたのでもしよろしければ参考にしてみてください。 github.com
関連記事
simple-minds-think-alike.moritamorie.com