[Slack API]OAuthの認可サーバをCloud Functionsで実装

## はじめに
SlackのOAuthを使ったユーザートークンの取得方法はいろいろ紹介されていますが、私が今回行ったGCPのCloud Functionsを使ったユーザートークンの取得方法について備忘録の意味であげています。

## 背景
そもそもSlackのユーザートークンが必要になったのは、IoT機器からSlackにアクセスする必要があったためです。IoT機器と言っていますが、実はおもちゃの「へぇボタン」を改造してIoT機器にしたものです。詳しくは以下の記事を御覧ください。

https://qiita.com/aramss/items/7e129d29c82e1334a91e

このおもちゃの「へぇボタン」からSlack APIを叩くことでSlackの投稿に対してリアクションできるようになりますが、操作した人のアカウントでリアクションさせたかったので、操作する人自身のユーザートークンにする必要がありました。

ちなみに、この「へぇボタン」はSlackの指定チャンネルの新着投稿を監視してLEDで通知できるようになっています。投稿の監視は操作する人のユーザートークンである必要はありませんが、せっかく取得するので同じユーザートークンを使って監視するようにしています。

## Slackアプリの作成
最終的にOAuthを使ってユーザートークンを取得することになりますが、その土台となるSlackアプリを作成していきます。

### 「Create App」するまでの設定
[api.slack.com](api.slack.com)に入ったあと、「Your Apps」で「Create an App」をクリックします。

![slackapi1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/687523e4-6270-0097-98c2-75752c93ab3f.png)

つづいて、「From scrach」を選び、

![slackapi2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/7239d1ab-361f-b517-ad15-1bb4c17baad1.png)

「Name app & choose workspace」の設定のところで次のように設定します。

![slakcapi3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/4be58aae-a765-c6e8-27fe-817d5da9bfa7.png)

<dl>
  <dt>App Name</dt>
  <dd>HeiButton (一応「へぇボタン」用なので)</dd>
  <dt>Pick a workspace to develop your app in:</dt>
  <dd>mafws (アプリをインストールするワークスペース名を指定)</dd>
</dl>

設定後、「Create App」をクリックすることで「HeiButton」というアプリが作られます。(この時点では中身はカラです)

### OAuth & Permissionsの設定
左のメニューから「OAuth & Permissions」を選びます。

![slackapi4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/8a771f9c-e3b7-f121-b822-5a1fdb70a7e9.png)

「OAuth & Permissions」の設定が開くので「User Token Scopes」に移動して次のようにScopeを設定します。

![slackapi5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/86df4b47-700f-596a-ee77-93aaa0324b70.png)

ここで`channels:history`と`reactions:write`を「OAuth Scope」に追加しているのは、APIを介してSlackの投稿を監視するのと投稿に対してリアクションできるようにするためです。

![slackapi6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/8283b9be-6f51-5882-b4d1-4269fb5057ed.png)

本来ならば、「OAuth & Permissions」設定内の「Redirect URLs」を設定することになりますが、このタイミングではリダイレクト先のURLはまだ決まっていませんので、後述のCloud Functions側設定が終わってURLが確定したあとに再度設定します。

## Cloud FunctionsでOAuthの認可サーバの作成
Slackから認可コードを受けるための認可サーバをCloud Functions上に作成します。

![cloudfunc1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/4ddcc3e3-e81a-fd0b-27f4-8876980eb539.png)

「ファンクションを作成」に入り次のように設定して行きます。

![cloudfunc2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/88d41c7e-c471-2cb3-1d3b-faee7b53a16b.png)

<dl>
  <dt>環境</dt>
  <dd>第1世代　※第2世代も選べますが設定が容易な第1世代で今回は済ませます</dd>
  <dt>関数名</dt>
  <dd>authfunc ※設定保存用の名前なので任意でOK</dd>
  <dt>リージョン</dt>
  <dd>asia-northeast1(東京) ※任意のリージョンでOK</dd>
</dl>

つづいて、次のように「トリガー」を設定します。

![cloudfunc3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/b5b7ac44-8e98-c06d-6c2b-3f962b5b2001.png)

「認証が必要」に設定しますが、後述の「権限」の設定のところで誰でもアクセスできるように権限を変更します。

つづいて、認可サーバーのコードを実装します。

![cloudfunc4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/d56cc9ca-315c-1c06-967c-b2760143a26c.png)

ランタイムは`node.js 20`を選び、エントリポイントには`authfunc`を指定して、`authfunc`は次のように実装しました。

```js:index.js
const request = require('request')

exports.authfunc = (req, res) => {
    const code = req.query["code"];
    request({
        url: "https://slack.com/api/oauth.access",
        method: "POST",
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'charset': 'utf-8'
        },
        form: {
            client_id: 'xxxxxxxxxxxxx.xxxxxxxxxxxxx',
            client_secret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
            code: code
        }
    }, (error, response, body) => {
        const param = JSON.parse(body);
        const access_token = param["access_token"]
        const scope = param["scope"]
        const user_id = param["user_id"]
        const team_id = param["team_id"]
        const team_name = param["team_name"]
        console.log(param);
        return res.send(`
            access_token: ${access_token}<br>
            scope: ${scope}<br>
            user_id: ${user_id}<br>
            teams_id: ${team_id}<br>
            team_name: ${team_name}
        `);
    })
};
```

```json:package.json
{
    "dependencies": {
        "request": "^2.88.0"
    }
}
```


`client_id`と`client_secret`に指定している値は、前段で作成したSlackアプリの「Basic Information」の「App Credentials」から`Client ID`と`Client Secret`の値を拾ってそれぞれ設定します。

![slackapi7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/ba6a1c1b-d708-9cce-1980-5dd37d5d8f91.png)

コードの入力が完了したらディブロイします。

![cloudfunc5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/838157b6-db23-8141-77ac-b4c948bea19f.png)

ディプロイが完了したら、次は作成した「ファンクション」の権限を変更します。「権限」のタブから「アクセス権の付与」を選択します。

![cloudfunc6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/48171d0f-ce3f-7642-6513-068e0d08095a.png)

今回作成した「ファンクション」はSlackからの認可コードを受ける認可サーバとなるため、だれでも実行できるよう変更します。

![cloudfunc7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/990d094d-06c6-025f-6609-8951a273b127.png)

追加するプリンシパルには「allUser」を指定し、割り当てるロールは「Cloud Functions起動元」にします。これで、未認証でも実行できるようになります。

以上でCloud Functionsの設定は完了となりますが、最後に作成した「ファンクション」のURLをSlackアプリの「Redirect URLs」に反映させます。

![slackapi6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/8283b9be-6f51-5882-b4d1-4269fb5057ed.png)

## ユーザートークンの取得方法
自身のユーザートークンを取得したい人に次の形式で書かれたURLを踏んでもらうのが一番早いです。

```console
https://slack.com/oauth/authorize?scope=reactions:write&client_id=xxxxxxx.xxxxxxx
```
`xxxxxxx.xxxxxxx`の部分は作成したSlackアプリの「App Credentials」にある「Client ID」を指定します。

![slackapi8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/8f32c755-b3dd-e1fa-46b9-7250f1bc2f94.png)


scopeには`reaction:write`しか指定していませんが、今回のSlackアプリのScopeで設定した`reaction:write`か`channels:history`のどちらかが指定されていれば大丈夫なようです。

最終的にユーザーにURLを踏んでもらうことで、ユーザー側のブラウザには次のような画面が表示されます。

![slackuser1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/39e19c69-3011-bb36-f7a3-efb999a3330c.png)

これを許可することでSlackから認可コードが発行され、今回Cloud Functions上に作ったリダイレクト先の認可サーバーへ渡され、ユーザー側のブラウザ上には最終的なレスポンスとして次のように表示されます。

![slackuser2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763691/be838ca5-43e9-8c3f-7a20-a1d04ae00675.png)

この表示内の`access token`の部分の`xoxp-`で始まる文字列がユーザートークンとなります。このトークンを使ってSlack APIを叩くことで、ユーザー自身のアカウントでリアクションができるようになります。

## 最後に
今回Cloud Functionsで作った「ファンクション」は第1世代を指定しましたが、Cloud Runを使う第2世代にもトライしてみたいと思います。

## 参考記事

https://nado1999.me/articles/slack-oauth-token-20220407

https://qiita.com/dbgso/items/a95a3364d9a8c67f3387