CloudFunctionsでFitbitのアクセストークンを自動更新する

## 概要
- CloudFunctionsを使って、Fitbitのアクティビティデータを取得し続けたい。
- しかし、データ取得に必要なアクセストークンの有効期限が28,800秒(＝8時間)なので、8時間ごとにアクセストークンを更新する仕組みが必要。
- そのやり方をまとめました。

## 環境
Windows10
Visual Studio Code(1.73.0)
firebase-tools 11.16.0

## 前提条件
- [Node.js](https://nodejs.org/ja/)のダウンロード・インストール済み

## ライブラリインストール
- CloudFunctionsフォルダ直下へ以下のライブラリをインストールします。
  
- Buffer
    - Base64変換用
    - 公式ドキュメント https://www.npmjs.com/package/buffer
```terminal:my-project/functions
npm install buffer
```

- Axios
    - GET, POST用
    - 公式ドキュメント https://www.npmjs.com/package/axios
```terminal:my-project/functions
npm install axios
```

- Node Fetch
    - GET, POST用
    - 公式ドキュメント https://www.npmjs.com/package/node-fetch
    - Node Fetch v3から`require('node-fetch')`でimportできなくなったので、CloudFunctionsで使う場合はv2をnpm installする必要があります。
```terminal:my-project/functions
npm install node-fetch@2
```

## 実装例(サンプルコード)
- [Refresh Tokenの公式ドキュメント](https://dev.fitbit.com/build/reference/web-api/authorization/refresh-token/) にアクセストークンを更新するCURLコマンドが載っています。
- [Curl Converter](https://curlconverter.com/) のサイトを使うと、CURLコマンドをnode-fetchやaxiosの書き方へコンバートできるので便利です。


```javascript:my-project/functions/index.js
const functions = require("firebase-functions");
const Buffer = require('buffer').Buffer;
const fetch = require('node-fetch')

// アクセストークンの更新
async function updateAccessToken(refreshToken, id, secret) {
    // 現状のrefresh_token
    const currentRefreshToken = refreshToken

    // clientIdとclientSecretをbase64にエンコード
    const clientId = id
    const clientSecret = secret
    const base64Before = clientId+":"+clientSecret
    const base64After = Buffer.from(base64Before).toString('base64')

    // 新トークン
    let newAccessToken = ""
    let newRefreshToken = ""

    // アクセストークンの更新
    await fetch('https://api.fitbit.com/oauth2/token', {
        method: 'POST',
            headers: {
                'accept': 'application/json',
                'authorization': 'Basic '+base64After,
                'content-type': 'application/x-www-form-urlencoded'
            },
            body: 'grant_type=refresh_token&refresh_token='+currentRefreshToken
        })
        .then(response => {
            return response.json()
        })
        .then(data => {
            // 成功時
            if(data.access_token!==undefined && data.refresh_token!==undefined) {
                console.log("access_tokenの更新 -> 成功")
                newAccessToken = data.access_token
                newRefreshToken = data.refresh_token
                console.log("newAccessToken -> "+newAccessToken)
                console.log("newRefreshToken -> "+newRefreshToken)
            }
    
            // 失敗時
            if(data.success===false) {
                console.log("access_tokenの更新 -> 失敗")
            }
    
        })
        .catch(error => {
            console.log("error: ", error)
        })

        // 取得した新トークン
        const tokenObj = {
            newAccessToken: newAccessToken,
            newRefreshToken: newRefreshToken
        }
        return tokenObj
}

```


## より具体的な実装例
- Fitbitサーバへアクティビティデータを5分ごとに取得しにいき、アクセストークンが期限切れの場合、アクセストークンを更新し、新規取得したアクセストークンとリフレッシュトークンをFirestoreへ保存するサンプルコードです。

```javascript:my-project/functions/index.js
const functions = require("firebase-functions");
if (admin.apps.length === 0) {
    admin.initializeApp();
} else {
    console.log(admin.apps)
}
const db = admin.firestore()
const { region } = require("firebase-functions");
const axios = require('axios')
const Buffer = require('buffer').Buffer;
const fetch =  require('node-fetch')

// Fitbitからアクティビティデータを定期的に取得する
exports.getFitbitActivityData = functions.region('asia-northeast1')
    .runWith({ secrets: ["SECRET_CLIENT_ID", "SECRET_CLIENT_SECRET"] })
    .pubsub.schedule('every 5 minutes').onRun(async(context) => {

    // clientIdとclientSecret
    const clientId = process.env.SECRET_CLIENT_ID
    const clientSecret = process.env.SECRET_CLIENT_SECRET
    
    // 現在のアクセストークンをDBから取得
    const docRefAccessToken = db.doc("token/token")
    const tokenDoc = await docRefToken.get()
    const currentAccessToken = tokenDoc.data().access_token
    const currentRefreshToken = tokenDoc.data().refresh_token

    let accessTokenIsValid = true
    // Fitbitアクティビティデータを取得する
    // ここは割愛します
    // ...


    // ...
    // アクセストークンの有効期限が切れていた場合
    accessTokenIsValid = false

    // アクセストークンの更新
    let tokenObj = ""
    if(!accessTokenIsValid) {
        tokenObj = await updateAccessToken(currentRefreshToken, clientId, clientSecret)
    }

    // 取得したアクセストークンをDBへ保存
    const newAccessToken = tokenObj.newAccessToken
    const newRefreshToken = tokenObj.newRefreshToken
    if(newAccessToken !== "" && newRefreshToken !== "") {
        try {
            db.runTransaction(async (transaction) => {
                ////////////////////////////////////////////////////
                // 読み取り
                ////////////////////////////////////////////////////
        
                const docRefToken = db.doc("token/token")
                const tokenDoc = await transaction.get(docRefToken);
        
                ////////////////////////////////////////////////////
                // 書き込み
                ////////////////////////////////////////////////////
        
                transaction.update(docRefToken, {
                    access_token: newAccessToken,
                    refresh_token: newRefreshToken,
                })
        
            });
            console.log("アクセストークンの更新成功")
        } catch (e) {
            console.log("アクセストークンの更新失敗", e);
        }
    }
});


// アクセストークンの更新
async function updateAccessToken(refreshToken, id, secret) {
    // 現状のrefresh_token
    const currentRefreshToken = refreshToken

    // clientIdとclientSecretをbase64にエンコード
    const clientId = id
    const clientSecret = secret
    const base64Before = clientId+":"+clientSecret
    const base64After = Buffer.from(base64Before).toString('base64')

    // 新トークン
    let newAccessToken = ""
    let newRefreshToken = ""

    // アクセストークンの更新
    await fetch('https://api.fitbit.com/oauth2/token', {
        method: 'POST',
            headers: {
                'accept': 'application/json',
                'authorization': 'Basic '+base64After,
                'content-type': 'application/x-www-form-urlencoded'
            },
            body: 'grant_type=refresh_token&refresh_token='+currentRefreshToken
        })
        .then(response => {
            return response.json()
        })
        .then(data => {
            // 成功時
            if(data.access_token!==undefined && data.refresh_token!==undefined) {
                console.log("access_tokenの更新 -> 成功")
                newAccessToken = data.access_token
                newRefreshToken = data.refresh_token
                console.log("newAccessToken -> "+newAccessToken)
                console.log("newRefreshToken -> "+newRefreshToken)
            }
    
            // 失敗時
            if(data.success===false) {
                console.log("access_tokenの更新 -> 失敗")
            }
    
        })
        .catch(error => {
            console.log("error: ", error)
        })

        // 取得した新トークン
        const tokenObj = {
            newAccessToken: newAccessToken,
            newRefreshToken: newRefreshToken
        }
        return tokenObj
}
```