1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Microsoft GraphAPIでカレンダーイベントの通知(webhook)を受け取る

Last updated at Posted at 2022-07-05

概要

  • MicrosoftのOutlookからイベント受け取ったりしたい
  • カレンダーで何か予定が追加されたり削除されたり変更されたりしたときに、その情報をイベントドリブンに受け取って、他システムに連携させたい

この記事のゴール

  • GraphAPIにサブスクリプション登録し、そこで指定した通知先エンドポイントに対して、カレンダーイベントの作成や削除のデータが連携される
  • 連携されたデータが復号化され、別システムに連携するための準備が整った状態になっている

範囲外

  • GraphAPIそのものの話
  • Firebaseそのもの話
  • 実装の細かい話

手順

前提

  • 面倒なのでアクセストークンとったりする部分は未実装にし、Graph Explorer に任せる
  • サブスクリプションを受け止めるのは、Firebase Functions Gen1で実装する
  • しかしただの検証でいちいちデプロイするのは面倒なので、ngrokを使ってローカルで完結させる

Azureへの新規アプリケーション登録

image.png

  • API Permissionsを設定する。今回はカレンダー関連だけなので、Calenders.ReadだけでOK
    • この画像では、SharedとかUserも入れてあるけど

e29b1822-c662-bde7-cc68-6cda14a217a1.png

RSAキーペアの作成

  • イベント作成に関するペイロード、例えば何時から何時まで?とか参加者の情報は暗号化されて飛んでくる

    • その暗号化されたデータを復号しなければ実態を確認できない。
  • 何で暗号して何で復号するか?のキーペアは予め利用者側で作成して、サブスクリプション登録時にGraphAPI側に伝える必要がある。

  • まずはその元ネタとなるキーペアを作成する

    • プロダクション実装するならここは定期的にローテーションさせたりする実装が必要になるだろう
$ openssl genrsa -out private.key 1024
$ openssl req -new -x509 -key private.key -out publickey.cer -days 365
$ openssl pkcs12 -export -out public_privatekey.pfx -inkey private.key -in publickey.cer

Firebase Functionsを実装

  • 満たしたい要件は2つ
    • サブスクリプション登録を行った際に、検証トークンが指定した通知先エンドポイントに飛んでくるので、それを捕まえてクエリからトークンをとってGraphAPI側に投げ返す機能実装
    • サブスクリプション登録後、イベント作成や削除に応じて飛んでくる通知のペイロードを復号する機能
      • encryptedContent に暗号化されたデータが込められているので、それをキーペアの秘密鍵で復号する
  • 事前に作成したキーペアの公開鍵を環境変数(PRIV_KEY_PEM)に事前にセットしておく
    • 上述のコマンドそのまま打ったならファイル名はprivate.keyのはず
import * as functions from "firebase-functions";
import { privateDecrypt, createDecipheriv } from "crypto";

export const outlookEventsSubscription = functions.https.onRequest(
  (request, response) => {
    // GraphAPIへのSubscription登録の検証のために渡されたトークンをそのまま返す
    if ("validationToken" in request.query) {
      response.send(request.query.validationToken);
      return;

    // Subscription検証以外はこっち
    } else if (
      "value" in request.body &&
      request.body.value[0].encryptedContent
    ) {
      const base64encodedKey = request.body.value[0].encryptedContent.dataKey;
      const asymetricPrivateKey = proccess.env.PRIV_KEY_PEM;
      const decodedKey = Buffer.from(base64encodedKey, "base64");
      const decryptedSymetricKey = privateDecrypt(
        asymetricPrivateKey,
        decodedKey
      );

      const base64encodedPayload = request.body.value[0].encryptedContent.data;
      const iv = Buffer.alloc(16, 0);
      decryptedSymetricKey.copy(iv, 0, 0, 16);
      const decipher = createDecipheriv(
        "aes-256-cbc",
        decryptedSymetricKey,
        iv
      );
      let decryptedPayload = decipher.update(
        base64encodedPayload,
        "base64",
        "utf8"
      );
      decryptedPayload += decipher.final("utf8");
	  //復号したオブジェクトを表示
      console.log(`decrypted event payload: ${decryptedPayload}`);
    }

    response.send({});
    return;
  }
)

ngrokでパブリッシュ

  • ngrokを未インストールの場合はhomebrew経由で入れるのが簡単 (macの場合)
  • ngrokはアカウント作らないと外に公開できないので、適当にアカウント作ってauthtokenを取得してコンフィグに入れ込む
$ brew install --cask ngrok
$ ngrok authtoken [token]
  • firebase functionsはデフォでTCP:5001を使うので、そのポートを指定してngrokで外部公開
  • 生成されたhttpsなURLをコピっておく
$ ngrok http 5001

Graph Explorer経由でサブスクリプションを登録

  • 対象URLは https://graph.microsoft.com/beta/subscriptions
    • 詳細なリソースデータをもらうためにはbetaじゃないとダメ
    • v1だと未サポートだよーってエラー出る
  • ペイロード(要求本文)として以下を投げる
    • changeType: 何のイベント契機で通知を投げてもらうか?
    • resource: 今回はカレンダーイベントを狙い撃ち。かつ、selectとして参加者と開始時刻、終了時刻を指定
    • expirationDateTime: 参加者など具体的なリソースデータを取得する場合は現在時刻から有効期間は最大1日
    • encryptionCertificate: キーペアの公開鍵(publickey.cer)を指定するが、事前にbase64エンコードしておくこと
{
    "changeType": "created,deleted",
    "notificationUrl": "ngrokで公開したエンドポイントを指定(httpsが必須)",
    "resource": "me/events?$select=attendees,start,end",
    "expirationDateTime": "現在時刻から24時間以内を指定",
    "clientState": "適当に何か入れる",
    "includeResourceData": "true",
    "encryptionCertificate": "事前に生成したキーペアの公開鍵をbase64エンコードして入れる",
    "encryptionCertificateId": "生成した鍵を示す任意IDなのでこれも適当で"
}

こんな感じのレスポンスが返って来れば成功。
ダメならダメで比較的わかりやすいエラーメッセージも添えて返ってくるので、その通りにペイロードを調整する。

{
    "@odata.context": "https://graph.microsoft.com/beta/$metadata#subscriptions/$entity",
    "id": "適当なuuid",
    "resource": "me/events?$select=attendees,start,end",
    "applicationId": "予めazureで作ったappId",
    "changeType": "created,deleted",
    "clientState": "hogehogefoobar",
    "notificationUrl": "指定した通知先エンドポイント",
    "notificationQueryOptions": null,
    "notificationContentType": null,
    "lifecycleNotificationUrl": null,
    "expirationDateTime": "指定した有効期限",
    "creatorId": "AzureのuserId",
    "includeResourceData": true,
    "latestSupportedTlsVersion": "v1_2",
    "encryptionCertificate": "指定した鍵データ",
    "encryptionCertificateId": "指定した鍵データのID",
    "notificationUrlAppId": null
}

動作確認

  • あとはoutlook側で適当にカレンダーイベントを作成したり削除すれば、およそ3秒程度で通知が飛んでくるはず
    • キーペアまわりで問題なければ復号されて表示されるはず
    • 逆に言えば、何かしらエラーが起きているならサブスクリプション登録時などで間違った鍵データを登録してしまっているかもしれない
    • もちろん実装コード側での秘密鍵の読み込み方が違う、とかもあるかもだけど

参考

めちゃくちゃ参考にしました。ありがとうございました。

復号処理まわりは以下のstack overflowの話が非常に参考になりました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?