こんにちは、サーバーサイド開発部の原田です。
前回はこちらの記事でIFTTTというサービスおよび使い方についてご紹介しました。
creators-note.chatwork.com
今回はIFTTT連携の開発において苦労、および工夫した点について書きたいと思います。
IFTTT開発と問題点
IFTTT連携の開発において、使用した言語およびライブラリは以下のようになっています。
- Scala
- Akka Http
- Kamon
- Circe
- etc...
基本的にはIFTTTが公開している公式ドキュメントに沿って開発を進めました。
platform.ifttt.com
システム構成図
大まかな構成図は以下になります。
また各システムの説明こちらです。
- チャットワークIFTTT
- IFTTTからのリクエストを受けたり、チャットワークにメッセージ投稿するための(proxy的な役割の)システム
- 今回開発したのはこの部分になります
- チャットワークOAuth
- チャットワークのOAuthシステム
- チャットワークAPI
- チャットワークのAPIシステム
例えば、他サービスをTriggerとしてチャットワークへメッセージ投稿がされるまでの大まかな流れは以下のようになります。
- 他サービス(GmailやRSS)をTriggerとして「IFTTT」にリクエストが送信される
- 「IFTTT」から「チャットワークIFTTT」にアクセストークン付きでリクエストが送信される
- 渡ってきたアクセストークンをもとに、「チャットワークAPI」に対してメッセージ投稿をするリクエストを送信する
(このときアクセストークンの有効期限が切れていたら、「チャットワークOAuth」にトークンをリフレッシュするリクエストを送信して、新しいアクセストークンを取得する) - 「チャットワーク API」からメッセージ投稿がされる
問題点
苦労した点の1つは、リクエストの判別です。
IFTTTからはリクエストヘッダーとしてX-Requeset-IDが送られてくるのですが、運用する中で詳細な調査をしようと思ったときにチャットワークのどのユーザーかを判別する手段がありませんでした。
IFTTTからリクエストを送ってきたユーザーはチャットワークユーザーであるため、X-Requeset-IDとチャットワークのアカウントIDを紐づける必要があったということです。
またアクセストークンを使っていたためIFTTTから見たときに「チャットワークIFTTT」もリソースサーバーとして振る舞う必要がありました。 (公式ドキュメントが公開されている以上エンドポイントが容易に分かってしまい、不正なリクエストがきやすいという点もあります)
解決方法
そこで使用したのが、JWTの検証です。
(ChatWorkのOAuth2で払い出されるアクセストークンはJWT形式を採用しています。)
大まかな流れは以下になります。
- 秘密鍵に対応する公開鍵を取得
- アクセストークンを検証
- チャットワークユーザーのアカウントIDを取得
JWTの検証
IFTTTの開発ドキュメントに記載してあるように、IFTTTからのリクエストにはAuthorization: Bearer {{user_access_token}}が渡ってきますので、こちらの検証を行います。
このとき使用したライブラリは以下です。
val algorithm = Algorithm.RSA256(publicKey) val verification: JWTVerifier.BaseVerification = JWT .require(algorithm) // algorithmのチェック .acceptExpiresAt(acceptExpiresAt) // 有効期限のチェック .withIssuer(url) // JWTの発行者のチェック .withAudience(audience) // JWTの利用者のチェック
jwtの形式は(header).(payload).(signature)になっています。
このときalgをnone
に書き換え、署名部分を取り除くことでpayload部分を改竄可能になってしまうため、alg部分も正しいアルゴリズムであるかどうかを検証する必要があります。
またalgorithmの他にもissuer(JWTの発行者)やaudience(JWTの利用者)の正当性検証もあわせて行なっています。
アカウントIDの抽出
正当性が確認できたら、次にチャットワークユーザーのアカウントIDをデコードしたJWTから抽出します。
val decodedJwt: DecodedJWT = verifier.verify(jwt) decodedJwt.getSubjectAsScala match { case Some(userId) => userId case None => throw new Exception("could not get user id") }
また、auth0/java-jwtのScala用Wrapperとして以下のライブラリを使用しました。
このようにして抽出したアカウントIDとX-Requeset-IDを一緒にログに吐き出すことで、どのチャットワークユーザーからのリクエストかを判別できるようになったため、調査がしやすくなりました。
まとめ
IFTTT連携の開発に関しては参考になるのが公式のドキュメントしかなく、実際に検証をしつつ開発を進めることになりましたが、なんとか無事にリリースすることができました。
今後もIFTTT連携に関する機能追加を考えていますので、ご利用いただければ嬉しいです。