FirebaseアプリからSlackへ通知を行う。アクセストークンも自動連携する編。


[前回](https://qiita.com/masatomix/items/56fc2024e383875cca1a)より、あるWEBサービスのユーザへの通知の方法として、そのユーザのSlackに通知が飛ばせたらイイねという件に着手しています。

前回はHello World 的な Slack App を作成しました。つぎはそのSlack Appへのアクセストークンの受け渡しをどうするかについて書くといったので、今回はそれを整理していきます。

今回目指す処理シーケンスは下記の通りとなります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/110fd0ff-088a-9626-4c95-73fe0c288913.png)


- アクセストークン(Access Token)は Slackが用意した Slack Appの開発画面で(目視で)確認するのではなく、DBMSやFirebase Firestoreなどへ自分で永続化する。今回は Firestoreへ保存します。
- Firestoreへ保存する処理を動かすために、OAuth認可サーバ(上図のSlack認可サーバ)からのリダイレクト先として(Slack Appの開発画面ではなく)、自前の処理を動かせるところを指定する。上図では「Firebase Functionsのoauth関数」をリダイレクト先にしている。
- 前回 Curlで実行した箇所は 今回は Firebase Functionsなどでスケジュール実行させる。上図では「Firebase Functionsのchat関数」がその役割で、chat関数はFirestoreからアクセストークンを取り出し、API(/api/chat.postMessage)を呼び出すことで、Slackへ投稿を行う


## やってみる
さあやってみます。がその前に準備や設定などを。

### 前提の環境

```bash
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.3
BuildVersion:	18D42

$ node --version
v10.14.2    <-ホントはFunctionsとかのバージョンに合わせるべきなんだけどいったん気にしない :-)
```



### Firebaseのサインアップと準備

まずはFirebaseのサインアップなどですが、別途記事にまとめました。
[Firebase や Google Cloud Platformの初回のサインアップその他の備忘メモ](https://qiita.com/masatomix/items/791699b3de3486618dd7)
実施すると、いわゆる

```javascript

  var config = {
    apiKey: "##FIREBASE API KEY##",
    authDomain: "##FIREBASE AUTH DOMAIN##",
    databaseURL: "https://##PROJECT ID##.firebaseio.com",
    projectId: "##PROJECT ID##",
    storageBucket: "##PROJECT ID##.appspot.com",
    messagingSenderId: "YOUR-SENDER-ID"
  };
```

を取得するところまでは行けるとおもいます。

あとは、**Firestoreを有効**にして、そして**Authentication機能を有効にし、ログインプロバイダとしてGoogle を有効**にしておいてください。これらは下記情報を参考にすればよいかもしれません。

- [FirestoreとVue.jsで連絡先管理アプリを作ってみる](https://qiita.com/takato0903/items/ecd18322a50e1728ad4b)
- [firebaseのチュートリアルで、Error: Missing or insufficient permissions.と言われた時の解決法メモ](https://qiita.com/hibohiboo/items/7af59b6d0d9df98d96c4)
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)](https://qiita.com/masatomix/items/5316acdb5980289e5cc2#firebase%E3%81%AE%E8%AA%8D%E8%A8%BC%E3%82%92%E3%82%84%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B)


あとは

- [FirebaseとGoogle Cloud Platform をさわれる環境を構築する](https://qiita.com/masatomix/items/0ddbfacb9bc1797044a1)

ココの**「firebase-tools のインストール」「Firebaseにログイン」**を実施しておいてください。


### Host名を設定する/Firebaseへ承認済みドメインの追加

今回動かそうとしているWEBアプリは、Cookieを用いているのですがその関係上、WEBアプリにはlocalhostではなくホスト名 client.example.com でアクセスしたいです。なので **/etc/hosts などで名前解決**しておきます。Macの例ですがこんな感じ。

```bash
$ cat /etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting.  Do not change this entry.
##
127.0.0.1	localhost
255.255.255.255	broadcasthost
::1             localhost
127.0.0.1 client.example.com
```

さらに、URLをlocalhostではなく client.example.com でアクセスすることになるので、**FirebaseのAuthenticationの設定にこのドメインを登録する**必要があります。[承認済みドメインを追加する](https://qiita.com/masatomix/items/5316acdb5980289e5cc2#%E6%89%BF%E8%AA%8D%E6%B8%88%E3%81%BF%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B) の手順に従って、承認済みドメインに「client.example.com」 という値を追加してください(コレを行わないと認証を行う ``firebase.auth().signInWithPopup``関数が``exception``となるみたいですね。)。


### Slack App のCredentials確認

[前回](https://qiita.com/masatomix/items/56fc2024e383875cca1a)の記事で紹介したSlack Appの管理画面 https://api.slack.com/apps より該当するSlack Appを探し、App Credentials にある**Client ID/Client Secretをひかえて**おきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/48e2bf97-80cb-a790-de95-27f9fa9f9ab9.png)


### Slack App のRedirect URLsの設定追加
Slack Appでもう一つ。今回**「Slack認可サーバからのリダイレクト先として、Slack Appの開発画面ではなく Firebase Functionsのoauth関数をリダイレクト先にする」** ことにしましたが、OAuthではそのRedirect先のURLを登録しておく必要があります。

その画面は Slack Appの開発画面にアクセスして、「OAuth & Permissions」を開きます。Redirect URLs を設定出来る箇所があるので下記のとおり ``http://client.example.com:5001/##PROJECT ID##/us-central1/oauth`` を追加し Save URLs をクリックして保存しましょう。「##PROJECT ID##」は、Firebaseの設定値 projectIdの値となります。


![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/6796c08c-9d08-61bf-b286-f6e9ada08a42.png)


準備が長くてスイマセン。。けっきょく、

| 項目       |                     値 |    
|:----:|:----------------|
|Firebaseプロジェクト名|``##PROJECT ID##``|
|WEBアプリのアクセスURL|``http://client.example.com:5000/``|
|Functions上の関数(oauth)|``http://client.example.com:5001/##PROJECT ID##/us-central1/oauth``|
|Functions上の関数(chat)|``http://client.example.com:5001/##PROJECT ID##/us-central1/chat``|
|投稿先のSlackワークスペース|Slack Appを開発しているワークスペース|
|Slack App のclient_id|``##SLACK CLIENT ID##``|
|Slack App のclient_secret|``##SLACK CLIENT SECRET##``|
|認可後の、Redirect URLs|``http://client.example.com:5001/##PROJECT ID##/us-central1/oauth``|

などを準備した感じです。。





### ビルドする

さて、説明のためのコードをつくって[GitHub](https://github.com/masatomix/todo-examples.git)においてあるので、下記の通り落としてビルドしていきます。

```bash
$ git clone --branch for_qiita_slack000  https://github.com/masatomix/todo-examples.git
```

まずはWEBアプリ。

```bash
$ cd todo-examples/
$ npm install

src/firebaseConfig.js を自分の設定に書き換え

$ cat src/firebaseConfig.js
export default {  ↓さきほどひかえておいた値を設定
  apiKey: '##FIREBASE API KEY##',
  authDomain: '##FIREBASE AUTH DOMAIN##',
  databaseURL: 'https://##PROJECT ID##.firebaseio.com',
  projectId: '##PROJECT ID##',
  storageBucket: '##PROJECT ID##.appspot.com',
  messagingSenderId: 'YOUR-SENDER-ID'
}

src/restConfig.js を自分の設定に書き換え

$ cat src/restConfig.js
export default {
  ##PROJECT ID## 書き換え(上記のprojectIdの値)
  apiUri: 'http://client.example.com:5001/##PROJECT ID##/us-central1/oauth'
}

$ npm run build
```

つづいて、Firebase Functionsのビルド。

```bash
$ cd functions/
$ npm install

functions/src/oauthConfig.ts  を自分の設定に書き換え

$ cat src/oauthConfig.ts 
export default {
  client_id: '##SLACK CLIENT ID##', ← さきほどひかえておいたSlackのClient IDの値を設定
  client_secret: '##SLACK CLIENT SECRET##',← さきほどひかえておいたSlackのClient Secretの値を設定
  authorization_endpoint: 'https://slack.com/oauth/authorize', ←ココはこのまま
  token_endpoint: 'https://slack.com/api/oauth.access', ←ココはこのまま
  redirect_uri: 'http://client.example.com:5001/##PROJECT ID##/us-central1/oauth', ##PROJECT ID## 書き換え(上記のprojectIdの値)
  scope: 'chat:write:user' ←ココはこのまま
}


functions/src/oidcConfig.ts  を自分の設定に書き換え

$ cat src/oidcConfig.ts 
export default {
  iss: 'https://securetoken.google.com/##PROJECT ID##',
  aud: '##PROJECT ID##'
}

$ npm run build
$ cd ../
```

つづいて下記コマンドで、このコード群がデフォルトで使用するFirebaseプロジェクト名を指定します。

```bash
$ firebase use --add
? Which project do you want to add? xxxxxxxxxxx   ← 複数選択肢が表示された場合は、上記の「 ##PROJECT ID## 」の値を選びます
? What alias do you want to use for this project? (e.g. staging) default

Created alias default for xxxxxxxxxxx.
Now using alias default (xxxxxxxxxxx)
$
```

参考: [FirebaseとGoogle Cloud Platform をさわれる環境を構築する](https://qiita.com/masatomix/items/0ddbfacb9bc1797044a1#%E3%81%A1%E3%81%AA%E3%81%BF%E3%81%AB-select-a-default-firebase-project-%E3%81%A3%E3%81%A6%E3%81%AA%E3%81%AB)

### 動かしてみる

さあ、ローカルでWEBアプリとFunctionsを起動してみます。

```bash
$ firebase serve --only hosting,functions
```

さて、ブラウザで http://client.example.com:5000/ にアクセスしてください。ログイン画面が表示されるとおもいます。Googleアカウントでログインできるようにしてあるのでログインしましょう。ログインできると「Add to Slack」ボタンが配置されている画面が表示されると思います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/93fc63cf-82a6-b8d9-d092-7cb923101dd6.png)

ボタンをクリックすると、ウィンドウが開き、Slackの認可サーバへリダイレクトされます。すでにWEBブラウザでSlackを使っていれば、下記の通り、前回記事と同様の認可画面が表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/aeff6657-2081-cb85-7c43-13f4516c65ff.png)
(WEBブラウザでSlackを使っていない場合は、ワークスペースを選択したりログインしたりする画面が表示されたのち、上記画面が表示されると思います。)

さて「許可する」をクリックすると、[前回の記事では「Slack Appの開発画面」にリダイレクトされました](https://qiita.com/masatomix/items/56fc2024e383875cca1a#%E3%81%9F%E3%82%81%E3%81%97%E3%81%AB%E8%87%AA%E5%88%86%E3%81%AE%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9%E3%81%AB%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B)が、**今回はRedirect URLsで設定追加した、 ``http://client.example.com:5001/##PROJECT ID##/us-central1/oauth`` へリダイレクトされる**はずです。 ``firebase serve --only hosting,functions``によって、ローカルで Firebase Functionsも起動しているので、ローカルで oauth 関数が動いた結果、Firestoreへアクセストークンが保存されるとおもいます。

後述しますが、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/d0164e6b-6326-e528-3ea6-ac3e789db9ae.png)
こんな感じにFirestoreに保存されるはずです。


### chat関数を呼び出す

さて、保存したSlackのアクセストークンを取り出して使用する処理を動かすために、**chat関数を呼び出し**ます。

```bash
$ curl http://client.example.com:5001/##PROJECT ID##/us-central1/chat
ok
$
```

**認可をおこなったSlackに通知が飛んだ**と思います！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/925cfa2a-9796-8330-7b65-c39bc55abe60.png)

サンプルアプリによる動作の紹介は以上です。

## 各ソースの説明

各ソースの主要なとこだけ紹介します。

### Add to Slack ボタンを配置してあるVue.jsのWEBアプリ

まずはリンクを配置するWEBアプリから。

WEBアプリはVue.jsで構築され、Firebase認証でログイン出来るようにしてあります。
ログインすると表示される、Add to Slack のボタンがある Slack.vue ファイルのソースは下記の通り。


```vue:src/components/Slack.vue
<template>
  <main v-if="$store.state.loginStatus" class="container">
    <h1>
      <img
        alt="Add to Slack"
        height="40"
        width="139"
        src="https://platform.slack-edge.com/img/add_to_slack.png"
        srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x"
        @click="popup()"
        style="cursor:pointer"
      >
    </h1>
  </main>
</template>

<script>
import restConfig from '@/restConfig'
import firebase from 'firebase'
export default {
  name: 'Slack',
  methods: {
    async popup () {
      const token = await firebase.auth().currentUser.getIdToken()
      console.log(token)
      // const user= JSON.parse(JSON.stringify(this.$store.state.user))
      // console.log(user.stsTokenManager.accessToken)

      const url = [
        restConfig.apiUri,
        '?idToken=',
        token
      ].join('')
      window.open(
        url,
        'pop',
        () =>
          `toolbar=0,status=0,top=100,left=200,width=700,height=600,modal=yes,alwaysRaised=yes`
      )
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}
</style>
```

コード中の ``firebase.auth().currentUser.getIdToken()`` という値は、FirebaseのユーザUIDがはいっているいわゆる``idToken``です。ちなみに「FirebaseのユーザUID」とは、コレのことですね。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/80ae4cbe-285b-a026-c244-0e77ad8705a5.png)

「``idToken``にどんなフォーマットでユーザUID(``user_id``)が格納されているか」また「その``idToken``が改ざん出来ない仕組みになっている」などについては下記の記事に整理しましたので、適宜こちらをご参照ください。

参考: [REST APIをFirebase認証で保護する。](https://qiita.com/masatomix/items/b446a07a641f97686a97#%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E5%80%A4%E3%81%AE%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB)


さて読み込んでいる設定ファイル ``restConfig.js``は以下。

```javascript:src/restConfig.js
export default {
  // ##PROJECT ID## 書き換え(上記のprojectIdの値)
  apiUri: 'http://client.example.com:5001/##PROJECT ID##/us-central1/oauth'
}
```

この値が、Add to Slackボタンを押したときに別ウィンドウで開かれるブラウザのURLに設定してあります。よって別ウィンドウで ``http://client.example.com:5001/##PROJECT ID##/us-central1/oauth?idToken=[FirebaseのユーザUIDが入ったidToken]`` が開かれます。

**クエリパラメタに(FirebaseのユーザUIDを含んだ)idTokenを渡している**のは、ずっとあとでSlack側で認可処理が完了してSlackのアクセストークンを取得できた後に「FirebaseのユーザUIDをキー」に「Slackのアクセストークン」をFirestoreに格納したいからです[^1]。

[^1]: もひとつOAuth認可のための処理シーケンスのセキュリティ対策でCSRF対策があり、セッションを繋いでおく必要があるのですが、そのためのセッションIDのような役割も持たせています。


### Firebase Functionsのoauth関数

つづいて上記で呼ばれたFunctions のoauth関数を見てみます。

```typescript:functions/src/index.ts(ほぼ全部)
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as request from 'request'
import * as cookie from 'cookie'
import session from './session'
import oauthConfig from './oauthConfig'
import oidcConfig from './oidcConfig'

admin.initializeApp()

export const oauth = functions.https.onRequest(async (req, res) => {

  // errorでリダイレクトされたとき
  // ユーザがキャンセルしたときはココなので、そこそこちゃんと実装しないと。。(今んとこ適当実装)
  if (req.query.error) {
    res.setHeader('Content-Type', 'text/plain;charset=UTF-8')
    const message = `
error: ${req.query.error}
error_uri: ${req.query.error_uri}
error_description: ${req.query.error_description}
`
    res.send(message)
    return
  }

  const code = req.query.code

  // codeがなかったとき、まずは認可画面へ遷移
  if (!code) {
    const reqIdToken = req.query.idToken
    if (!reqIdToken) {
      console.log('codeがないのに、req.query.idToken もない')
      res.status(400).send('req.query.idToken が取れませんでした')
      return
    }

    // idTokenをチェックする必要あり
    try {
      await verifyIdToken(reqIdToken)
    } catch (error) {
      console.log(error.message)
      res.status(400).send('req.query.idToken が正しくありません<br />' + error.message)
      return
    }
    addCookie(res, 'idToken', reqIdToken)

    const randomValue = getRandomString()
    console.log('randomValue: ' + randomValue)

    const authorization_endpoint_uri = [
      oauthConfig.authorization_endpoint,
      '?client_id=',
      oauthConfig.client_id,
      '&redirect_uri=',
      oauthConfig.redirect_uri,
      '&state=',
      randomValue,
      '&response_type=code',
      '&scope=',
      oauthConfig.scope
    ].join('')

    session.setAttributeById(reqIdToken, 'state', randomValue)
    res.redirect(authorization_endpoint_uri)
  } else {
    // そもそもidTokenがなかったら後続を続ける意味がないので、正当性チェック verifyIdToken もここで実施
    const cookies = cookie.parse(req.headers.cookie || '')
    const idToken = cookies.idToken

    let userId = ''
    // idTokenをチェックする必要あり
    try {
      userId = await verifyIdToken(idToken)
    } catch (error) {
      console.log(error.message)
      res.status(400).send('cookies.idToken が正しくありません。そもそも取得できなかったかも。<br />' + error.message)
      return
    }

    const csrf = await checkCSRF(req, res, idToken)
    if (!csrf) {
      res
        .status(400)
        .send('前回のリクエストと今回のstate値が一致しないため、エラー。')
      return
    }

    const formParams = {
      redirect_uri: oauthConfig.redirect_uri,
      client_id: oauthConfig.client_id,
      client_secret: oauthConfig.client_secret,
      grant_type: 'authorization_code',
      code: code
    }

    const options = {
      uri: oauthConfig.token_endpoint,
      method: 'POST',
      headers: {
        'content-type': 'application/x-www-form-urlencoded'
      },
      form: formParams,
      json: true
    }

    const body: any = await doRequest(options)

    console.log(userId)

    admin
      .firestore()
      .collection('slackToken')
      .doc(userId)
      .set(body)

    res.send('登録完了。ブラウザ閉じちゃってください。')
  }
})

function doRequest(option) {
  return new Promise((resolve, reject) => {
    request(option, (error, response, body) => {
      if (!error && response.statusCode == 200) {
        resolve(body)
      } else {
        reject(error)
      }
    })
  })
}

// https://qiita.com/fukasawah/items/db7f0405564bdc37820e 感謝！
function getRandomString() {
  var S = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  var N = 50
  const randomValue = Array.from(Array(N))
    .map(() => S[Math.floor(Math.random() * S.length)])
    .join('')
  return randomValue
}

async function verifyIdToken(idToken) {
  const decodedToken = await admin.auth().verifyIdToken(idToken)

  const iss_aud_check =
    decodedToken.iss == oidcConfig.iss && decodedToken.aud == oidcConfig.aud
  if (!iss_aud_check) {
    console.log(`iss(Expected): ${oidcConfig.iss}`)
    console.log(`iss(Actual  ): ${decodedToken.iss}`)
    console.log(`aud(Expected): ${oidcConfig.aud}`)
    console.log(`aud(Actual  ): ${decodedToken.aud}`)
    throw new Error('issもしくはaudが想定外でした')
  }
  return decodedToken.uid
}

async function checkCSRF(req, res, idToken) {
  const state = req.query.state

  const sessionState = await session.getAttributeById(idToken, 'state')

  session.invalidate(idToken)
  console.log('requestState: ' + state)
  console.log('sessionState: ' + sessionState)
  return state === sessionState
}

function addCookie(res, key, value) {
  res.setHeader('Cache-Control', 'private') // Hosting経由だと、これがないとset cookieが削除される
  const expiresIn = 60 * 60 * 24
  const options = { maxAge: expiresIn, httpOnly: true }
  // const options = { maxAge: expiresIn, httpOnly: true, secure: true }
  res.setHeader('Set-Cookie', cookie.serialize(key, value, options))
}
```

```typescript:session.ts
import * as admin from 'firebase-admin'

const me = {
  setAttributeById (sessionId: string, key: string, value: string) {
    const ref = admin
      .firestore()
      .collection('session')
      .doc(sessionId)

    ref.get().then(docref => {
      if (!docref.exists) {
        const target: any = {}
        target[key] = value
        // admin.firestore().collection('session').add(target)
        admin
          .firestore()
          .collection('session')
          .doc(sessionId)
          .set(target)
      } else {
        const target: any = docref.data()
        target[key] = value
        ref.set(target)
      }
    })
  },

  async getAttributeById (sessionId: string, key: string) {
    const docref = await admin
      .firestore()
      .collection('session')
      .doc(sessionId)
      .get()

    // const docref = await ref.get()
    let returnValue: any = {}
    if (!docref.exists) {
      return
    } else {
      returnValue = docref.data()
    }
    return returnValue[key]
  },

  async invalidate (sessionId: string) {
    await admin
      .firestore()
      .collection('session')
      .doc(sessionId).delete()
  }

}

export default me
```

超ザックリいうと、クエリパラメタに「``code``」が入っているかで場合分けしていて

- ``code``が入っていない
    - → (OAuthのトークン取得でいう)初回のリクエストと見なし、設定ファイル(``functions/src/oauthConfig.ts``)よりクエリパラメタを生成しながら、Slack認可サーバ「``https://slack.com/oauth/authorize``」へリダイレクト。

- ``code``が入っている 
    - → Slack認可サーバから認可コードが渡ってきたとみなし、Slack認可サーバ「``https://slack.com/api/oauth.access``」にアクセスしてアクセストークンを取得し、Firestoreへアクセストークンを保存する処理を実行


という動きをします。

もう少し丁寧に書くと、

- ``code``が入っていない

1. WEBアプリ側が発行した``idToken``(くどいですがFirebaseのユーザUIDが含まれている) をクエリパラメタから取得
1. ``idToken``が「有効かつ改ざんされていない」か、また「``idToken``に含まれている``PROJECT ID``をみて、想定通りのWEBアプリが発行した``idToken``である」かをチェック(``verifyIdToken``関数のところ)
1. OKなら、その``idToken``をCookieにセット(今後のリクエストで``idToken``を用いるため)
1. **randomな文字列(``state``値)を生成**し、Slack認可サーバにクエリパラメタ``state``で連係します。あわせてその値は**``idToken``をキーにFirestoreに保存**しておきます。
1. Slack認可サーバへリダイレクト

- ``code``が入っている 

1. Cookieから``idToken``を取り出します。
1. ``verifyIdToken``関数で``idToken``の正当性をチェックします。
1. **``idToken``をキーにFirestoreから``state``値を取得**します。
1. クライアントからは、「認可サーバからのレスポンス時に渡ってくる(返ってくる)さっきの``state``の値」が渡ってきているはずなので、**クエリからも``state``パラメタの値を取得**。
1. ``code``が入ってないリクエストと、``code``が入っているリクエストが**おなじセッションであれば、上記二つの``state``の値は一致するはず**なので、二つの値の一致を確認します(**CSRF対策**)。
1. OKだったら、認可コード(``code``)と、``client_id/client_secret``を使ってSlack認可サーバ「``https://slack.com/api/oauth.access``」へアクセストークンを要求。Slack認可サーバは、**``client_id``によって「自分が認可コードを渡したかったクライアントかな？」という判定**と **``client_secret``によって「(接続を許可したWEBアプリだという)正当なヤツからのアクセストークン要求かな？」という判定**をして、**認可コードに紐付くアクセストークンを発行**して返す
1. つづいてcookieから取得できる**``idToken``からFirebaseのユーザUID を取得**、それをキー値にして、さきほど取得した**SlackのアクセストークンをFirestoreの「slackToken」テーブルに格納**する。
1. 関数は、画面に「登録完了。ブラウザ閉じちゃってください。」と表示して、完了する。

となります。

上記によってFirestoreには下記のような形式でアクセストークンが格納されます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/d0164e6b-6326-e528-3ea6-ac3e789db9ae.png)



### Firebase Functionsのchat関数

さてWEBアプリから 「Add to Slack」ボタンを押したあとSlackでの認証・認可をおこなうことで、Firebase Firestoreにアクセストークンが格納されました。あとは Functionsから周期的に、この値を取り出してAPI経由でSlackへ投稿をおこなえば完成です。

今回は WEBから呼び出せるchat関数をつくってあり、それ経由でAPIを呼び出します。コードは、以下の通り。

```typescript:functions/src/index.ts(ほぼ全部といった、残り)
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as request from 'request'
import * as cookie from 'cookie'
import session from './session'
import oauthConfig from './oauthConfig'
import oidcConfig from './oidcConfig'

admin.initializeApp()

export const chat = functions.https.onRequest(async (req, res) => {
  await sendSlack()
  res.send('ok')
})

export const chat_pub = functions.pubsub
  .topic('slackChatTopic')
  .onPublish(async message => {
    await sendSlack()
  })

// $ gcloud pubsub topics publish slackChatTopic  --message '{"name":"Xenia"}'

async function sendSlack () {
  const querySnapshot = await admin.firestore()
    .collection('slackToken')
    .get()

  querySnapshot.forEach(doc => {
    const fbUserId = doc.id
    const jsonData = doc.data()

    const option = {
      url: 'https://slack.com/api/chat.postMessage',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
        Authorization: `Bearer ${jsonData.access_token}`
      },
      json: {
        channel: '#general',
        text: `${fbUserId} です、今日は！`
      }
    }
    request(option, (error, response, body) => {
      if (error) {
        console.log('error:', error)
        return
      }
      if (response && body) {
        console.log('status Code:', response && response.statusCode)
        console.log(body)
      }
    })
  })
}
```

``async function sendSlack ()`` では、FirestoreのslackTokenテーブルのデータを全件取得し、アクセストークンを取りだします。そのアクセストークンを Authorization ヘッダの Bearer トークンとしてセットし「``https://slack.com/api/chat.postMessage``」へPOSTすることで、該当のアクセストークンが紐付いたSlackへ、メッセージが投稿されます。


今回はchat関数をWEB経由で起動しましたが、本来はスケジューラから起動したいですよね。じつはすでに [Firebase の関数をスケジューラから定期的に呼び出す](https://qiita.com/masatomix/items/b40a2c604319e52df4bf) の記事で用いた形式の関数 chat_pub を作成済みなので、次回は

- WEBアプリとFunctionsの、本番へのデプロイ
- スケジューラから chat関数(chat_pub関数)を呼び出す事で、よりSlack上で動くアプリっぽくする
- そのための諸々の環境設定

をやっていきます。


**--2019/09/24追記--**
次回記事を書きました。
[FirebaseアプリからSlackへ通知を行う。Slack AppのWEBへのデプロイ編。](https://qiita.com/masatomix/items/993c27f6f7615c48ebf4)
**--2019/09/24追記以上--**


おつかれ様でしたー。。


## 関連リンク
- [FirebaseアプリからSlackへ通知を行う。アクセストークンは手動で設定する編。](https://qiita.com/masatomix/items/56fc2024e383875cca1a) 前回記事
- [Vue.js とCloud Functions for Firebase をFirebase Hostingへデプロイするための環境構築手順](https://qiita.com/masatomix/items/bd1c3a6b31e8d2cb7aef) Vue.jsやFirebase Functions をサーバに配置する際の基本的な知識を整理
- [Cloud Functions for Firebase の関数をスケジューラから定期的に呼び出す](https://qiita.com/masatomix/items/b40a2c604319e52df4bf) 定期実行の基礎知識
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)](https://qiita.com/masatomix/items/5316acdb5980289e5cc2) Firbaseの認証機能の基礎知識
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編)](https://qiita.com/masatomix/items/7be14b8eb433edcfbfc7)Firestoreの基礎知識