ChatworkのOAuthフローをDockerを使ってPoCする

## 経緯
とあるプロジェクトでChatworkとの連携機能を設計することになり、PoC（概念実証）を行いました。検証の過程でローカル環境を汚したくなかったのと、HTTPSサーバ環境をサクッと構築したかったのでDockerを使うことにしました。

## 準備
### Chatwork OAuthクライアントの作成
Chatworkのサービス連携 > OAuth からOAuthクライントを作成します。
**クライアント名**はお好みで設定して構いません。
**リダイレクト先URL**にはこれから作成するlocalhostサーバのURL（https://localhost:3443/oauth ）を入力します。
![スクリーンショット_2021-07-27_14.51.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/8cbd811f-80ad-beef-8e49-bf5778a6f16a.png)
クライアントの作成が完了すると、クライアントIDとクライアントシークレットが発行されます。これらは後ほど使いますので控えておきます。
![スクリーンショット_2021-07-27_14_53_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/f24fd87d-3a1a-32f8-c25d-78a1fe270953.png)
### docker-compose.ymlの作成
手軽にHTTPSサーバ環境を構築するために [https-portal](https://github.com/SteveLTN/https-portal) というコンテナを使用します。https-portal は自己署名証明書（オレオレ証明書）の発行までやってくれるのでとても便利です。
アプリケーションサーバには Express.js を使用するのでNode.jsのコンテナを使用します。プロジェクトのサーバ環境がGoogle Cloud PlatformなのでNode.jsのバージョンは12を選びました。
#### docker-compose.yml
```YAML
version: '3'
services:
  https-portal:
    image: steveltn/https-portal:1
    ports:
      - '3443:443'
    environment:
      STAGE: local
      DOMAINS: 'localhost -> http://app:3000'
    volumes:
      - ./https-portal-data:/var/lib/https-portal

  app:
    image: node:12
    env_file: ./app.env
    environment:
      - TZ=Asia/Tokyo
      - DEBUG=app:*
    tty: true
    volumes:
      - ./src:/app
    working_dir: /app
    command: npm start
```
## Express.jsの環境構築
### express-generatorで雛形を生成
コンテナのコンソールからExpress.js の雛形を生成します。
今回はviewにEJSを使用するので、オプションに```--view=ejs```を加えます。
また、ChatworkのAPIを実行するためにaxiosを使用するのでパッケージをインストールしておきます。

```
$ docker-compose run --rm app bash
$ npx express-generator --view=ejs
$ npm i axios --save
$ npm install
$ exit
```
### 環境変数の定義
#### app.env
Express.jsで使用する環境変数を定義します。ルームIDはブラウザのURLから取得できます。
![スクリーンショット_2021-07-27_15_06_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/a06df589-f17d-a57f-5a28-20117dd0db87.png)

```ENV
# クライアントID
CLIENT_ID=[OAuthクライアント作成で生成したクライアントID]

# クライアントシークレット
CLIENT_SECRET=[OAuthクライアント作成で生成したクライアントシークレット]

# メッセージのテスト送信に使用するRoom ID
TEST_ROOM_ID=999999999
```

### OAuthリダイレクト用のRouterを追加
今回は /oauth というパスを用意することにしました。
#### src/app.js

```JS
...
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var oauthRouter = require('./routes/oauth'); // <- 追加
...
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/oauth', oauthRouter); // <- 追加
```
/oauth ではアクセストークンの取得とテストメッセージの送信を直列で行うようにします。実際は取得したトークンをDBへ保存したりする処理が必要になりますが、PoCなのでそこまでは実装しません。
#### src/routes/oauth.js

```JS
const express = require('express');
const router = express.Router();

// axios
const axiosBase = require('axios');
const axios = axiosBase.create({
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'X-Requested-With': 'XMLHttpRequest',
  },
  responseType: 'json',
});

router.get('/', async function (req, res, next) {
  let isSuccess = false;

  try {
    /**
     * Access Tokenを取得する
     */
    var tokenParams = new URLSearchParams();
    tokenParams.append('grant_type', 'authorization_code');
    tokenParams.append('code', req.query.code);

    const authKey = Buffer.from(
      process.env.CLIENT_ID + ':' + process.env.CLIENT_SECRET,
      'utf-8'
    ).toString('base64');

    const tokenResult = await axios.post(
      'https://oauth.chatwork.com/token',
      tokenParams,
      {
        headers: {
          Authorization: `Basic ${authKey}`,
        },
      }
    );
    const token = tokenResult.data.access_token;

    /**
     * テストメッセージを送信する
     */
    var messageParams = new URLSearchParams();
    messageParams.append('body', 'Hello!');

    const messageResult = await axios.post(
      `https://api.chatwork.com/v2/rooms/${process.env.TEST_ROOM_ID}/messages`,
      messageParams,
      {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      }
    );

    console.log(messageResult.data);
    isSuccess = true;
  } catch (error) {
    console.error(error);
    isSuccess = false;
  }

  res.render('oauth', {
    isSuccess,
  });
});

module.exports = router;
```
結果表示用のviewファイルも追加します。
#### src/views/oauth.ejs

```HTML
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="/stylesheets/style.css" />
  </head>
  <body>
    <% if (isSuccess) { %> Success!! <% } else { %> failure... <% } %>
  </body>
</html>
```

## 動かしてみる
ターミナルからdocker-compose を実行します。ログが見えるようにフォアグラウンドで実行することにします。

```
$ docker-compose up
```

全てのコンテナが問題なく起動したら、下記のURLからOAuth認証フローを開始します。stateはPoCでは使用しないので適当な文字列を設定しておきます。

```URL
https://www.chatwork.com/packages/oauth2/login.php?response_type=code&client_id={client_id}&state={state}&scope=offline_access%20rooms.messages:write
```
「許可」ボタンをクリックします。
![スクリーンショット_2021-07-27_14.48.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/0b0926f1-db70-cd05-425a-e17ac5557a54.png)
ブラウザにSuccess!!と表示されていればメッセージの送信まで成功していますのでChatworkのチャンネルを確認してみましょう。
![スクリーンショット_2021-07-27_14.37.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/2c28fdcc-c3e3-f5e6-b219-d89d4de199bc.png)
チャンネルにメッセージが送信されていることが確認できました。
![スクリーンショット_2021-07-27_14_40_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2221418/9cc09d31-e84a-99b4-bebb-bcbdc20c82fb.png)
## まとめ
Dockerとhttps-portalを使うことで面倒なHTTPSサーバの準備も極めて簡単に構築することができました。OAuth認証のリダイレクトURLにはHTTPSプロトコルを要求されるケースが多いので、他のサービス連携の調査にも活用できると思います。