Simple minds think alike

より多くの可能性を

【Golang】slack-goを使ってSlackのリクエスト署名を検証する<実装編>

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にそれぞれ任意の値を設定します。

スコープ追加

画面左メニューの「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というライブラリを使用して実装しています。

github.com

また、今回作成したサンプルアプリは 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

SecretsVerifierMiddlewareServeHTTP メソッドが実行され、SlackからのHTTPリクエストの署名検証が行われた後、ショートカット独自のhandlerが動いていることを確認できます。

次の記事では、署名検証の内容を読み解いてみます。

simple-minds-think-alike.moritamorie.com

参考コード

コードの方はGithubにも挙げてみたのでもしよろしければ参考にしてみてください。 github.com

関連記事

simple-minds-think-alike.moritamorie.com

参考資料