LINE WORKSのトークンをPowerAutomateで更新する（RefreshToken期限切れ問題）

# 概要
LINE WORKSのAPIがVer2.0になりましたね。

https://line.worksmobile.com/jp/pr/20220404/

そろそろ対応しておかないとな、と思って、Postmanで2.0のアクセストークンを取ったりして触っていたのですが、アクセストークンはどうやら24時間で切れる模様。
> - Access Token
>   - 有効期限： 24時間
> - Refresh Token
>   - 有効期限： 90日

LINE WORKSに限らず、アクセストークンなどはAzure KeyVaultに格納しているので、毎日KeyVaultのシークレットを更新する必要があるということになります。
LINE WORKSのBot運用はPowerAutomateで行っているので、トークンの更新もPowerAutomateのフローで行ってみました。
## 問題は...
リフレッシュトークンが90日で切れるということ。
リフレッシュトークンが有効期限切れとなった場合は、どうするんだろう...
ググってみると、

https://forum.worksmobile.com/jp/posts/100776

> Refresh Tokenの有効期限は90日となっておりますので、期限内にTokenを再発行するような運用をご検討ください。

どうやら延長はできなさそう。
やっぱり、取り直さないといけないのか。

## 手っ取り早く対応するには
JWTの期限を十分長く取っておけば、作成したJWTは何度でも使いまわせるので、ダイスケ的にもオールオッケー（誰だお前は）
ただ、セキュリティ的にはどうなんでしょう？よく分からんとです。
## JWTは使いまわさず、その都度作る
リフレッシュトークンが有効期限切れの時は、その都度JWTを作ってトークンを発行する方向で、フローを組んでみたいと思います。
（JWTの有効期限を長くすればよいと思いついたのが、なんやかんやでPowerAutomateでJWT作れた後だったので、今更後にも引けず）

# 事前準備
KeyVaultに以下のシークレットを作っておきます。
1. ClientId
1. ClientSecret
1. AccessToken（有効期限付き）
1. RefreshToken（有効期限付き）

それと、JWTの署名に使うプライベートキーファイルをKeyVaultにアップロードします。
![2022-08-24_21h56_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/9a1c9e87-dde8-3afc-974e-03b84dc1d1ee.png)
ファイルの拡張子は.keyから.pemに変更しておく必要があります。
# フロー
## 全体
１日２回起動するため、「繰り返し」トリガーで起動します。
前半はKeyVaultのアクセストークンの取得です。
その後、LINE WORKSのIDやシークレットをKeyVaultから取得。
条件ブロックでリフレッシュトークンが有効かどうか判別して、リフレッシュトークンによる更新か、JWTを作成して新規に発行するか、の場合分けを行っています。
![2022-08-24_21h58_55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/12985f51-c95d-a1c9-c9aa-398b907690c8.png)

## KeyVaultにアクセスするためのトークンを取得
シークレットの更新はアクションが用意されていないので、直接APIを叩く必要があります。
その際に、アクセストークンが必要になるので、以下のような感じで、KeyVault用にアクセストークンを取得します。
こちらもアクセストークンの有効期限は１時間（3599秒）なので、都度取る必要があります。
![2022-08-24_22h11_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/2d5d5d07-93ef-4e76-6ae7-f32f70890168.png)
詳しくはMSの公式を参照してください。

https://docs.microsoft.com/ja-jp/graph/auth-v2-service

取得したトークンは変数にぶち込んでおきます。
![2022-08-24_22h16_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/a74994db-25e4-03fc-c505-794246d7b3ca.png)

あと、LINE WORKSのシークレット等をKeyVaultから取得しておきます。
（AccessTokenは使わないので取らなくても構いません）

## 条件分岐
KeyVaultに設定された、RefreshTokenの有効期限までの残り日数で場合分けします。
![2022-08-24_22h18_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/1397dbe0-835b-c68f-f373-92bd03cf86fd.png)
残り日数は以下の式で求めています。
~~~
div(sub(ticks(outputs('LW2-RefreshToken')?['body/validityEndTime']),ticks(utcNow())),864000000000)
~~~
## はいの場合（リフレッシュトークンで更新）
有効期限まで３日以上あれば、リフレッシュトークンでアクセストークンを更新します。
![2022-08-24_22h23_24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/d5f4c2b6-6fd1-5a72-bfa5-c5b1bcded693.png)
### リフレッシュトークンによる再発行
はまりポイントは、本文を改行で区切るとエラーではじかれました。
ぜんぶURLエンコードして渡しているのは、そのときの名残で、必要ないものまで全部URLエンコードしてます。（たぶん、必要ない）
![2022-08-24_22h24_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/07fd77dd-c598-37aa-64ce-612f7d441b40.png)

### シークレットの更新
KeyVaultのAPIを叩くときは、日時はUNIX時間を指定する必要があります。
UNIX時間への変換は以下のページを参考にさせてもらいました。ありがとうございます。

https://qiita.com/h-nagao/items/60a8a72db6706e68b5d7

![2022-08-24_22h35_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/86772753-8d84-c8e9-4c59-15a6d4e63621.png)
updatedとexpに、更新日時と有効期限を渡してやります。

こちらは、ここで終了。

## いいえの場合（JWTを作成して再発行）
JWTを作成して、アクセストークンを発行し、KeyVaultのシークレットを更新する流れになります。
![2022-08-24_22h38_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/ba3db2a4-ce4a-78c6-1a99-04a8d24af78e.png)
JWTに必要なHeader、Payload、Signatureをそれぞれこさえていきます。
### JWTのHeader部(①)
LINE WORKSのAPIリファレンス通りにJSONを書いて、Base64エンコードするだけ。
簡単ですね。
![2022-08-24_22h45_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/fae5b7c0-bef9-99fd-fa28-48b35e2ad94a.png)
### JWTのPayload部(②)
これもリファレンス通り。
生成日時をutcNow()、期限日時をutcNow()の１時間後としています。
![2022-08-24_22h47_38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/d4d4c4da-c561-3827-2794-7bb75c77c92c.png)
### JWTのSignature部(③)
最大の難関だったところです。
そもそも、署名のアルゴリズムとか全然わかっていないので、なにをどうしたらいいやら状態でした。
![2022-08-24_22h54_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/6afbb84e-7530-1a91-3285-ef14cf0640db.png)
カスタムコネクタの前に、KeyVaultに格納されたキーファイルで署名を作成するHTTPアクションですが、これもKeyVaultのコネクタには署名アクションが用意されていないため、直接APIを叩く必要があります。

https://docs.microsoft.com/ja-jp/rest/api/keyvault/keys/sign/sign?tabs=HTTP

algでアルゴリズムを指定して、valueで値を渡すだけです。
結論としては、concat(①,'.',②)をSHA256でハッシュ化した値を渡せばよいということになります。
PowerAutomateではハッシュ化する関数は用意されていないので、以下のページを参考にさせていただきました。

https://qiita.com/karuakun/items/a37af4bb98792ab57c6e

上記ページは、MD5でのハッシュかですが、コードの中のMD5をSHA256に変えるだけで、ほぼそのまま流用させていただきました。感謝！
「SHA256」アクションに、以下の式を渡して、その結果をKeyVaultに渡すと、署名が出来上がります。
~~~
concat(outputs('作成-Header(encoded)'),'.',outputs('作成-ClaimSet(encoded)'))
~~~
### JWT完成！
できあがった①②③を、'.'で連結するとJWTが完成します。
![2022-08-24_23h09_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/b3e31f55-6d32-9c75-62a1-018a4d5ddbc7.png)
~~~
concat(outputs('作成-Header(encoded)'),'.',outputs('作成-ClaimSet(encoded)'),'.',body('JSON-PrivateKeyを使用して署名')?['value'])
~~~
（署名はBase64エンコード不要です）

### アクセストークンの発行
作成したJWTをassertionに渡して、アクセストークンを発行します。
![2022-08-24_23h11_50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/bdf9f1d0-3cba-a846-32ca-0f383b6ce8d0.png)
### アクセストークン・リフレッシュトークンをKeyVaultに格納!
リフレッシュトークンで更新した場合と同様、KeyVaultに格納していきます。
アクセストークンだけでなく、リフレッシュトークンも更新する必要があります。
リフレッシュトークンの有効期限を90日後として、expに渡すことで、条件ブロックが正しく判別できるようになります。
[2022-08-24_23h13_57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/b7c71979-58f3-262c-0185-464a3ef7ab1a.png)

### 最後にお知らせ
更新したアクセストークンで、LINE WORKSに通知してやります。
この辺はお好みで。
![2022-08-24_23h16_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/c9a57506-100a-03e1-1665-72cf9565ddf5.png)

あと、エラーとなった場合、LINE WORKSのアクセストークンが正しくないと思われるので、LINE WORKSではなく、Teamsに通知するようにしました。
![2022-08-24_23h18_37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244231/9e37393a-bff0-cad1-a1f1-aaa89655d5cd.png)

# まとめ
だいぶ苦労しましたが、署名の方法やカスタムコードなど、初めて知ったことが多いフローとなりました。
Azure Functionsでやるしかないか、と思っていたのが、Power Platform内で完結できるのは非常に便利だなと思います。
ただ、C#が...（VBぐらいしか使えない人間なので）

PrivateKeyを再発行した場合でも、KeyVaultのキーを更新すれば手動でJWTを作り直して格納しておく必要もないので、運用はかなり楽になるのではないかと思います。（そんなに頻繁に再発行するものではないと思いますが）

最後に、長くなりすぎてごめんなさい。（スクリーンショットが...）