概要
- MicrosoftのOutlookからイベント受け取ったりしたい
- カレンダーで何か予定が追加されたり削除されたり変更されたりしたときに、その情報をイベントドリブンに受け取って、他システムに連携させたい
この記事のゴール
- GraphAPIにサブスクリプション登録し、そこで指定した通知先エンドポイントに対して、カレンダーイベントの作成や削除のデータが連携される
- 連携されたデータが復号化され、別システムに連携するための準備が整った状態になっている
範囲外
- GraphAPIそのものの話
- Firebaseそのもの話
- 実装の細かい話
手順
前提
- 面倒なのでアクセストークンとったりする部分は未実装にし、Graph Explorer に任せる
- サブスクリプションを受け止めるのは、Firebase Functions Gen1で実装する
- しかしただの検証でいちいちデプロイするのは面倒なので、ngrokを使ってローカルで完結させる
Azureへの新規アプリケーション登録
- ただの検証なので、Azure ADアプリを登録するを参考に適当に作る
- API Permissionsを設定する。今回はカレンダー関連だけなので、
Calenders.Read
だけでOK- この画像では、SharedとかUserも入れてあるけど
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秒程度で通知が飛んでくるはず
- キーペアまわりで問題なければ復号されて表示されるはず
- 逆に言えば、何かしらエラーが起きているならサブスクリプション登録時などで間違った鍵データを登録してしまっているかもしれない
- もちろん実装コード側での秘密鍵の読み込み方が違う、とかもあるかもだけど
参考
めちゃくちゃ参考にしました。ありがとうございました。
- https://qiita.com/kenakamu/items/e7dfd7a93ea6a3c631bf
- https://dev.classmethod.jp/articles/msgraph-changenotifications-trackchanges/
- https://docs.microsoft.com/ja-jp/graph/api/subscription-post-subscriptions?view=graph-rest-1.0&tabs=http
- https://docs.microsoft.com/ja-jp/graph/outlook-change-notifications-overview?view=graph-rest-1.0
復号処理まわりは以下のstack overflowの話が非常に参考になりました。