kintone上に簡易OAuth2クライアントを作ってみる

myThings Developers を kintone から使うために簡易的な OAuth2 クライアントを kintone 上に作ってみました。 

OAuth2 の認証フローではコールバックの設定と取得したトークンの保存が必要となりますが、これを kintone 上のアプリで実装しています。実用性はあまりなくて、Node.js などを使ったサーバーを構築することなく kintone だけで完結できるかという実験です。

# 前提
- 今回 OAuth2 の連携先として使用するのは、 [myThings Developers](mythings-developers.yahoo.co.jp) です。ハッカソンなどでもお馴染みですね。
- myThings側の設定方法はチュートリアルを参考にしてください。

# 方針
- kintone 上には OAuth2 認証情報取得用のアプリ（= 認証アプリ)と、実際にリクエストを送信するためのアプリ（= 実行アプリ)の2つを作成します。
- OAuth2 認証フローにおけるコールバックURLとして、認証アプリのURLを擬似的なコールバックURLとして使います。
- レコード編集開始のイベント発火時に kintone 上のjavascript からトークンを取得して、認証アプリに保存します。  
- 擬似コールバック(= 認証アプリ)のURLは、情報の特定のためにレコード番号を指定した形となります。
- 実行アプリでは認証アプリに保存されたリフレッシュトークンを使って myThings の API をコールします。

# kintone に認証アプリを作る
## アプリのデザイン
下記のような構成でフィールドを設定します。

|フィールド名|フィールドコード|タイプ|用途|
|---|---|---|---|
|実行エンドポイント|endPointUrl|文字列|myThingsのエンドポイントを入力する|
|クライアントID|clientID|文字列|OAuth2 のクライアントIDを入力する|
|シークレット|secret|文字列|OAuth2 のクライアントシークレットを入力する|
|リフレッシュトークン|refreshToken|文字列|取得したリフレッシュトークンを保存する|
    
## javascriptの設定
共通部分

```js:getToken.js
    const yahooAuthUrl = 'https://auth.login.yahoo.co.jp/yconnect/v1/authorization';
    const yahooTokenUrl = 'https://auth.login.yahoo.co.jp/yconnect/v1/token';

    const getRedirectUrl = (base) => {
        const pathname = location.pathname.replace(base, 'show#mode=edit&record=');
        return encodeURI('https://' + location.host + pathname + kintone.app.record.getId());
    };
```

認証コード取得部分

```js:getToken.js
            location.href = yahooAuthUrl 
                + '?response_type=code'
                + '&client_id=' + dataSet.record.clientID.value
                + '&redirect_uri=' + getRedirectUrl(funcName)
                + '&state=xxxxxxxxxxxxx';
```

コールバック部分

```js:getToken.js
    const getArgs = () => {
        // 下記の2パターンを想定
        // https://xxxxx.cybozu.com/k/xxx/edit?...
        // https://xxxxx.cybozu.com/k/xxx/show#...
        const pair = (location.href.substring(location.host.length + location.pathname.length + 9)).split('&');
        let args = {};
        for(let i = 0; pair[i]; i++) {
            const kv = pair[i].split('=');
            args[kv[0]] = kv[1];
        }
        return args;
    };

    kintone.events.on([
        'app.record.create.show'
    ], (event) => {
        const args = getArgs();
        if ('code' in args) {
            location.href = 'https://' + location.host
                + location.pathname.replace('edit', 'show')
                + '#record=' + args.record
                + '&mode=edit'
                + '&code=' + args.code
                + '&state=' + args.state;
        }
    });

    kintone.events.on([
        'app.record.edit.show'
    ], (event) => {
        const args = getArgs();
        if ('code' in args) {
            //トークン認証
            const header = {
                'Authorization': 'Basic ' + btoa(event.record.clientID.value + ':' + event.record.secret.value),
                'Content-Type': 'application/x-www-form-urlencoded'
            };
            const body = 'grant_type=authorization_code'
                + '&redirect_uri=' + 'https://' + location.host + location.pathname
                + '&code=' + args.code;
            kintone.proxy(yahooTokenUrl, 'POST', header, body).then((response) => {
                const responseCode = response[1];
                const responseBody = JSON.parse(response[0]);
                if (responseCode === 200) {
                    let dataSet = kintone.app.record.get();
                    dataSet.record.refreshToken.value = responseBody.refresh_token;
                    kintone.app.record.set(dataSet);
                }
            }, (error) => {
                alert('トークンの取得でエラーが発生しました。\n' + error.message);
            });
        }
    });
```
コールバックは、レコード編集画面のURLを指定したいところですが

```
/show#mode=edit&record=x
```

みたいなURLとなっているため、myThings側のコールバックURLに設定してもうまく解釈してくれずQuery文字列が欠落した状態で返ってきます（多分 # のところが問題）。そのため、myThingsからのコールバックURLとしてレコードの再利用画面を設定し、そこからリダイレクトする形になっています（うーむ）。

## myThings側の事前準備
### 設定箇所その1
```text:コールバックURL
https://mythings-developers.yahoo.co.jp/sample/confirm
https://<ドメイン名>.cybozu.com/k/<認証アプリのID>/edit?record=<認証アプリのレコード番号>
```
myThings 側に kintone 上の擬似コールバックのURLを追加します。

### 設定箇所その2
```text:IPアドレス登録
103.79.0.0/16
```
CybozuのIPアドレスを設定します。上記の設定は、雑なのでちゃんと調べてくださいね。

# kintone の実行アプリから呼び出してみる
## アプリのデザイン
下記のような構成でフィールドを設定します。

|フィールド名|フィールドコード|タイプ|用途|
|---|---|---|---|
|通知項目|通知項目|文字列|myThingsのエンドポイントに送信するメッセージを登録する|
    
## javascriptの設定
レコード追加時に呼び出すようにしてみます。

```js:setPush.js
    const configAppId = <認証アプリのアプリ番号>;
    const apiRecNo = <認証情報を取得したレコード番号>;

    const yahooTokenUrl = 'https://auth.login.yahoo.co.jp/yconnect/v1/token';

    kintone.events.on([
        'app.record.create.submit.success'
    ], (event) => {
        return kintoneUtility.rest.getRecord({
            'app': configAppId,
            'id': apiRecNo
        }).then((response) => {
            return new kintone.Promise((resolve, reject) => {
                let rec = response.record;
    
                //アクセストークンの再取得
                const header = {
                    'Authorization': 'Basic ' + btoa(rec.clientID.value + ':' + rec.secret.value),
                    'Content-Type': 'application/x-www-form-urlencoded'
                };
                const body = 'grant_type=refresh_token'
                    + '&refresh_token=' + rec.refreshToken.value;
                kintone.proxy(yahooTokenUrl, 'POST', header, body).then((response) => {
                    const responseCode = response[1];
                    const responseBody = JSON.parse(response[0]);
                    if (responseCode === 200) {
                        //トークン取得に成功
                        const header = {
                            'Content-Type' : 'application/x-www-form-urlencoded',
                            'Authorization' : 'Bearer ' + responseBody.access_token
        
                        };
                        const body = 'entry=' + encodeURIComponent(JSON.stringify({
                            'message': event.record.通知項目.value
                        }));
                        resolve({
                            'success': true,
                            'url': rec.endPointUrl.value,
                            'header': header,
                            'body': body
                        });
                    } else {
                        alert('トークンの取得でエラーが発生しました。\n' + error);
                        reject();
                    }
                }, (error) => {
                    alert('トークンの取得でエラーが発生しました。\n' + error);
                    reject();
                });
            }).then((result) => {
                if (result.success) {
                    return new kintone.Promise((resolve, reject) => {
                        // myThings にカスタムトリガーを送信
                        kintone.proxy(result.url, 'POST', result.header, result.body).then((response) => {
                            resolve(response);
                        }, (error) => {
                            alert('トリガーの送信でエラーが発生しました。\n' + error);
                            reject();
                        });
                    }).then((result) => {
                        console.log(response[1]);
                        return event; 
                    });
                }
            });
        });
    });
```

[kintone Utility](https://github.com/kintone/kintoneUtility) を使用しているので、アプリの設定画面で読込む設定をしてください。

```text:kintoneアプリの設定
https://kintone.github.io/kintoneUtility/kintoneUtility.min.js
```

# ソース一式
https://github.com/m-ando-japan/kintone-oauth2-mythings

# 注意事項
- 上記の処理では<>などの一部の文字が送信できません。
- kintone や myThings Developers の仕様が変わった場合に動作しなくなることがあるでしょう。
- 実際に運用する場合はやはり OAuth2 を処理する外部 API を作った方が便利でしょう。
- myThings Developers のリフレッシュトークンは4週間の有効期限つきです。上記にはリフレッシュトークンを再取得する処理は含まれていません。期限が切れた場合は、認証アプリを操作して再取得する必要があります。