概要
LINE WORKSのAPIがVer2.0になりましたね。
そろそろ対応しておかないとな、と思って、Postmanで2.0のアクセストークンを取ったりして触っていたのですが、アクセストークンはどうやら24時間で切れる模様。
- Access Token
- 有効期限: 24時間
- Refresh Token
- 有効期限: 90日
LINE WORKSに限らず、アクセストークンなどはAzure KeyVaultに格納しているので、毎日KeyVaultのシークレットを更新する必要があるということになります。
LINE WORKSのBot運用はPowerAutomateで行っているので、トークンの更新もPowerAutomateのフローで行ってみました。
問題は...
リフレッシュトークンが90日で切れるということ。
リフレッシュトークンが有効期限切れとなった場合は、どうするんだろう...
ググってみると、
Refresh Tokenの有効期限は90日となっておりますので、期限内にTokenを再発行するような運用をご検討ください。
どうやら延長はできなさそう。
やっぱり、取り直さないといけないのか。
手っ取り早く対応するには
JWTの期限を十分長く取っておけば、作成したJWTは何度でも使いまわせるので、ダイスケ的にもオールオッケー(誰だお前は)
ただ、セキュリティ的にはどうなんでしょう?よく分からんとです。
JWTは使いまわさず、その都度作る
リフレッシュトークンが有効期限切れの時は、その都度JWTを作ってトークンを発行する方向で、フローを組んでみたいと思います。
(JWTの有効期限を長くすればよいと思いついたのが、なんやかんやでPowerAutomateでJWT作れた後だったので、今更後にも引けず)
事前準備
KeyVaultに以下のシークレットを作っておきます。
- ClientId
- ClientSecret
- AccessToken(有効期限付き)
- RefreshToken(有効期限付き)
それと、JWTの署名に使うプライベートキーファイルをKeyVaultにアップロードします。
ファイルの拡張子は.keyから.pemに変更しておく必要があります。
フロー
全体
1日2回起動するため、「繰り返し」トリガーで起動します。
前半はKeyVaultのアクセストークンの取得です。
その後、LINE WORKSのIDやシークレットをKeyVaultから取得。
条件ブロックでリフレッシュトークンが有効かどうか判別して、リフレッシュトークンによる更新か、JWTを作成して新規に発行するか、の場合分けを行っています。
KeyVaultにアクセスするためのトークンを取得
シークレットの更新はアクションが用意されていないので、直接APIを叩く必要があります。
その際に、アクセストークンが必要になるので、以下のような感じで、KeyVault用にアクセストークンを取得します。
こちらもアクセストークンの有効期限は1時間(3599秒)なので、都度取る必要があります。
詳しくはMSの公式を参照してください。
あと、LINE WORKSのシークレット等をKeyVaultから取得しておきます。
(AccessTokenは使わないので取らなくても構いません)
条件分岐
KeyVaultに設定された、RefreshTokenの有効期限までの残り日数で場合分けします。
残り日数は以下の式で求めています。
div(sub(ticks(outputs('LW2-RefreshToken')?['body/validityEndTime']),ticks(utcNow())),864000000000)
はいの場合(リフレッシュトークンで更新)
有効期限まで3日以上あれば、リフレッシュトークンでアクセストークンを更新します。
リフレッシュトークンによる再発行
はまりポイントは、本文を改行で区切るとエラーではじかれました。
ぜんぶURLエンコードして渡しているのは、そのときの名残で、必要ないものまで全部URLエンコードしてます。(たぶん、必要ない)
シークレットの更新
KeyVaultのAPIを叩くときは、日時はUNIX時間を指定する必要があります。
UNIX時間への変換は以下のページを参考にさせてもらいました。ありがとうございます。
updatedとexpに、更新日時と有効期限を渡してやります。
こちらは、ここで終了。
いいえの場合(JWTを作成して再発行)
JWTを作成して、アクセストークンを発行し、KeyVaultのシークレットを更新する流れになります。
JWTに必要なHeader、Payload、Signatureをそれぞれこさえていきます。
JWTのHeader部(①)
LINE WORKSのAPIリファレンス通りにJSONを書いて、Base64エンコードするだけ。
簡単ですね。
JWTのPayload部(②)
これもリファレンス通り。
生成日時をutcNow()、期限日時をutcNow()の1時間後としています。
JWTのSignature部(③)
最大の難関だったところです。
そもそも、署名のアルゴリズムとか全然わかっていないので、なにをどうしたらいいやら状態でした。
カスタムコネクタの前に、KeyVaultに格納されたキーファイルで署名を作成するHTTPアクションですが、これもKeyVaultのコネクタには署名アクションが用意されていないため、直接APIを叩く必要があります。
algでアルゴリズムを指定して、valueで値を渡すだけです。
結論としては、concat(①,'.',②)をSHA256でハッシュ化した値を渡せばよいということになります。
PowerAutomateではハッシュ化する関数は用意されていないので、以下のページを参考にさせていただきました。
上記ページは、MD5でのハッシュかですが、コードの中のMD5をSHA256に変えるだけで、ほぼそのまま流用させていただきました。感謝!
「SHA256」アクションに、以下の式を渡して、その結果をKeyVaultに渡すと、署名が出来上がります。
concat(outputs('作成-Header(encoded)'),'.',outputs('作成-ClaimSet(encoded)'))
JWT完成!
できあがった①②③を、'.'で連結するとJWTが完成します。
concat(outputs('作成-Header(encoded)'),'.',outputs('作成-ClaimSet(encoded)'),'.',body('JSON-PrivateKeyを使用して署名')?['value'])
(署名はBase64エンコード不要です)
アクセストークンの発行
作成したJWTをassertionに渡して、アクセストークンを発行します。
アクセストークン・リフレッシュトークンをKeyVaultに格納!
リフレッシュトークンで更新した場合と同様、KeyVaultに格納していきます。
アクセストークンだけでなく、リフレッシュトークンも更新する必要があります。
リフレッシュトークンの有効期限を90日後として、expに渡すことで、条件ブロックが正しく判別できるようになります。
2022-08-24_23h13_57.png
最後にお知らせ
更新したアクセストークンで、LINE WORKSに通知してやります。
この辺はお好みで。
あと、エラーとなった場合、LINE WORKSのアクセストークンが正しくないと思われるので、LINE WORKSではなく、Teamsに通知するようにしました。
まとめ
だいぶ苦労しましたが、署名の方法やカスタムコードなど、初めて知ったことが多いフローとなりました。
Azure Functionsでやるしかないか、と思っていたのが、Power Platform内で完結できるのは非常に便利だなと思います。
ただ、C#が...(VBぐらいしか使えない人間なので)
PrivateKeyを再発行した場合でも、KeyVaultのキーを更新すれば手動でJWTを作り直して格納しておく必要もないので、運用はかなり楽になるのではないかと思います。(そんなに頻繁に再発行するものではないと思いますが)
最後に、長くなりすぎてごめんなさい。(スクリーンショットが...)