次の方法で共有


パートナー センター Webhook

適用対象: パートナー センター | 21Vianet が運営するパートナー センター | Microsoft Cloud for US Government のパートナー センター

適切なロール: グローバル管理者 |課金管理者 |管理エージェント |販売エージェント |ヘルプデスク エージェント

パートナー センター Webhook API を使用すると、パートナーはリソース変更イベントに登録できます。 これらのイベントは、パートナーが登録した URL に HTTP POST の形式で配信されます。 パートナー センターからイベントを受信するには、パートナー センターがリソース変更イベントを POST できるコールバックをホストします。 イベントはデジタル署名されているため、パートナーはパートナー センターから送信されたことを確認できます。 Webhook 通知は、共同販売の最新の構成を持つ環境にのみトリガーされます。

パートナー センターでは、次の Webhook イベントがサポートされています。

  • Azure Fraud イベントが検出されました ("azure-fraud-event-detected")

    このイベントは、Azure の不正アクセス イベントが検出されたときに発生します。

  • 代理管理者リレーションシップ承認済みイベント ("dap-admin-relationship-approved")

    このイベントは、委任された管理者特権が顧客テナントによって承認されると発生します。

  • 顧客イベントによって承認されたリセラー関係 ("reseller-relationship-accepted-by-customer")

    このイベントは、顧客テナントがリセラー関係を承認すると発生します。

  • 顧客イベントによって受け入れられる間接リセラー関係 ("間接リセラー関係-顧客別受け入れ")

    このイベントは、顧客テナントが間接リセラー関係を承認したときに発生します。

  • 委任された管理者リレーションシップ終了イベント ("dap-admin-relationship-terminated")

    このイベントは、顧客が委任された管理者特権を終了したときに発生します。

  • Dap Admin Relationship Terminated By Microsoft Event ("dap-admin-relationship-terminated-by-microsoft")

    このイベントは、DAP が 90 日を超える間非アクティブになったときに、Microsoft がパートナーテナントと顧客テナントの間で DAP を終了したときに発生します。

  • 詳細な管理者アクセス割り当てアクティブ化イベント ("granular-admin-access-assignment-activated")

    このイベントは、Microsoft Entra ロールが特定のセキュリティ グループに割り当てられると、パートナーが詳細な代理管理者特権アクセス割り当てをアクティブ化したときに発生します。

  • 詳細な管理者アクセス割り当ての作成イベント ("granular-admin-access-assignment-created")

    このイベントは、パートナーが詳細な委任された管理者特権アクセス割り当てを作成するときに発生します。 パートナーは、顧客が承認した Microsoft Entra ロールを特定のセキュリティ グループに割り当てることができます。

  • 詳細な管理者アクセス割り当て削除済みイベント ("granular-admin-access-assignment-deleted")

    このイベントは、パートナーが詳細な委任された管理者特権のアクセス割り当てを削除したときに発生します。

  • 詳細な管理者アクセス割り当ての更新イベント ("granular-admin-access-assignment-updated")

    このイベントは、パートナーが詳細な委任された管理者特権のアクセス割り当てを更新したときに発生します。

  • 詳細な管理者リレーションシップのアクティブ化イベント ("きめ細かい管理者関係のアクティブ化")

    このイベントは、細かい委任された管理者特権が作成され、顧客が承認できるようにアクティブになると発生します。

  • きめ細かい管理者関係承認済みイベント ("詳細な管理者関係-承認済み")

    このイベントは、顧客テナントが詳細な委任された管理者特権を承認すると発生します。

  • 詳細な管理者リレーションシップの期限切れイベント ("granular-admin-relationship-expired")

    このイベントは、詳細な委任された管理者特権の有効期限が切れると発生します。

  • 詳細な管理者リレーションシップの作成イベント ("granular-admin-relationship-created")

    このイベントは、詳細な委任された管理者特権が作成されるときに発生します。

  • 詳細な管理者リレーションシップの更新イベント ("granular-admin-relationship-updated")

    このイベントは、顧客またはパートナーが詳細な委任された管理者特権を更新したときに発生します。

  • 詳細な管理者リレーションシップの自動拡張イベント ("granular-admin-relationship-auto-extended")

    このイベントは、システムが自動的に詳細な委任された管理者特権を拡張するときに発生します。

  • 詳細な管理者リレーションシップ終了イベント ("granular-admin-relationship-terminated")

    このイベントは、パートナーまたは顧客テナントが詳細な委任された管理者特権を終了したときに発生します。

  • Invoice Ready イベント ("invoice-ready")

    このイベントは、新しい請求書の準備ができたときに発生します。

  • New Commerce Migration Completed ("new-commerce-migration-completed")

    このイベントは、新しいコマース移行が完了したときに発生します。

  • 新しいコマース移行の作成 ("new-commerce-migration-created")

    このイベントは、新しいコマース移行が作成されるときに発生します。

  • 新しいコマース移行に失敗しました ("new-commerce-migration-failed")

    このイベントは、新しいコマースの移行が失敗したときに発生します。

  • 転送の作成 ("create-transfer")

    このイベントは、転送の作成時に発生します。

  • Update Transfer ("update-transfer")

    このイベントは、転送が更新されたときに発生します。

  • 転送の完了 ("完全転送")

    このイベントは、転送が完了したときに発生します。

  • 転送失敗 ("fail-transfer")

    このイベントは、転送が失敗したときに発生します。

  • 新しいコマース移行スケジュールが失敗しました ("new-commerce-migration-schedule-failed")

    このイベントは、新しいコマース移行スケジュールが失敗したときに発生します。

  • Referral Created イベント ("referral-created")

    このイベントは、紹介の作成時に発生します。

  • 紹介更新イベント ("紹介更新")

    このイベントは、紹介が更新されたときに発生します。

  • 関連紹介作成イベント ("related-referral-created")

    このイベントは、関連する紹介が作成されるときに発生します。

  • 関連紹介更新イベント ("related-referral-updated")

    このイベントは、関連する紹介が更新されたときに発生します。

  • サブスクリプションアクティブ イベント ("subscription-active")

    このイベントは、サブスクリプションがアクティブになると発生します。

    Note

    サブスクリプションのアクティブな Webhook と対応するアクティビティ ログ イベントは、現時点ではサンドボックス テナントでのみ使用できます。

  • Subscription Pending イベント ("subscription-pending")

    このイベントは、対応する注文が正常に受信され、サブスクリプションの作成が保留中の場合に発生します。

    Note

    サブスクリプション保留中の Webhook と対応するアクティビティ ログ イベントは、現時点ではサンドボックス テナントでのみ使用できます。

  • サブスクリプションの更新イベント ("サブスクリプションの更新")

    このイベントは、サブスクリプションの更新が完了したときに発生します。

    Note

    サブスクリプションが更新された Webhook と対応するアクティビティ ログ イベントは、現時点ではサンドボックス テナントでのみ使用できます。

  • Subscription Updated イベント ("subscription-updated")

    このイベントは、サブスクリプションが変更されたときに発生します。 これらのイベントは、パートナー センター API を介して変更が加えられた場合に加えて、内部の変更がある場合に生成されます。

    Note

    サブスクリプションが変更されてからサブスクリプションの更新イベントがトリガーされるまでに最大 48 時間の遅延があります。

  • Test イベント ("test-created")

    このイベントを使用すると、テスト イベントを要求し、その進行状況を追跡することで、登録を自己オンボードしてテストできます。 イベントの配信中に Microsoft から受信されているエラー メッセージを確認できます。 この制限は、"テストで作成された" イベントにのみ適用されます。 7 日より前のデータは消去されます。

  • Threshold Exceeded イベント ("usagerecords-thresholdExceeded")

    このイベントは、顧客の Microsoft Azure の使用量が使用量の支出予算 (しきい値) を超えると発生します。 詳細については、(顧客/パートナー センター/set-an-azure-spending-budget-for-your-customers の Azure 支出予算の設定) を参照してください。

今後の Webhook イベントは、パートナーが制御していないシステムで変更されるリソースに対して追加され、可能な限り "リアルタイム" に近いイベントを取得するためにさらに更新が行われます。 ビジネスに価値を追加するイベントに関するパートナーからのフィードバックは、追加する新しいイベントを決定する際に役立ちます。

パートナー センターでサポートされている Webhook イベントの完全な一覧については、「 Partner Center webhook イベントを参照してください。

前提条件

  • パートナー センターの認証に関するページで説明している資格情報。 このシナリオでは、スタンドアロン アプリとアプリ + ユーザーの両方の資格情報を使った認証がサポートされています。

パートナー センターからのイベントの受信

パートナー センターからイベントを受信するには、パブリックにアクセスできるエンドポイントを公開する必要があります。 このエンドポイントは公開されているため、通信がパートナー センターからの通信であることを検証する必要があります。 受信したすべての Webhook イベントは、Microsoft Root にチェーンされている証明書でデジタル署名されます。 イベントの署名に使用される証明書へのリンクも提供されます。 これにより、サービスを再デプロイまたは再構成しなくても、証明書を更新できます。 パートナー センターは、イベントの配信を 10 回試行します。 10 回の試行後もイベントが配信されない場合、イベントはオフライン キューに移動され、配信時にそれ以上試行されません。

次の例は、パートナー センターから投稿されたイベントを示しています。

POST /webhooks/callback
Content-Type: application/json
Authorization: Signature VOhcjRqA4f7u/4R29ohEzwRZibZdzfgG5/w4fHUnu8FHauBEVch8m2+5OgjLZRL33CIQpmqr2t0FsGF0UdmCR2OdY7rrAh/6QUW+u+jRUCV1s62M76jbVpTTGShmrANxnl8gz4LsbY260LAsDHufd6ab4oejerx1Ey9sFC+xwVTa+J4qGgeyIepeu4YCM0oB2RFS9rRB2F1s1OeAAPEhG7olp8B00Jss3PQrpLGOoAr5+fnQp8GOK8IdKF1/abUIyyvHxEjL76l7DVQN58pIJg4YC+pLs8pi6sTKvOdSVyCnjf+uYQWwmmWujSHfyU37j2Fzz16PJyWH41K8ZXJJkw==
X-MS-Certificate-Url: https://3psostorageacct.blob.core.windows.net/cert/pcnotifications-dispatch.microsoft.com.cer
X-MS-Signature-Algorithm: rsa-sha256
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length: 195

{
    "EventName": "test-created",
    "ResourceUri": "http://localhost:16722/v1/webhooks/registration/test",
    "ResourceName": "test",
    "AuditUri": null,
    "ResourceChangeUtcDate": "2017-11-16T16:19:06.3520276+00:00"
}

Note

Authorization ヘッダーには、"Signature" のスキームがあります。 これは、コンテンツの base64 でエンコードされた署名です。

コールバックを認証する方法

パートナー センターから受信したコールバック イベントを認証するには、次の手順に従います。

  1. 必要なヘッダーが存在することを確認します (Authorization、x-ms-certificate-url、x-ms-signature-algorithm)。
  2. コンテンツに署名するために使用する証明書をダウンロードします (x-ms-certificate-url)。
  3. 証明書チェーンを確認します。
  4. 証明書の "組織" を確認します。
  5. UTF8 エンコードを使用してコンテンツをバッファーに読み取ります。
  6. RSA 暗号化プロバイダーを作成します。
  7. 指定したハッシュ アルゴリズム (SHA256 など) で署名されたものとデータが一致するかどうかを確認します。
  8. 検証が成功した場合は、メッセージを処理します。

Note

既定では、署名トークンは Authorization ヘッダーで送信されます。 登録で SignatureTokenToMsSignatureHeader を true に設定した場合、署名トークンは代わりに x-ms-signature ヘッダーで送信されます。

イベント モデル

次の表では、パートナー センター イベントのプロパティについて説明します。

プロパティ

件名 説明
EventName イベントの名前です。 {resource}-{action} の形式。 たとえば、"test-created" などです。
ResourceUri 変更されたリソースの URI。
ResourceName 変更されたリソースの名前。
AuditUrl 省略可能。 監査レコードの URI。
ResourceChangeUtcDate リソースの変更が発生した日時 (UTC 形式)。

サンプル

次の例は、パートナー センター イベントの構造を示しています。

{
    "EventName": "test-created",
    "ResourceUri": "http://api.partnercenter.microsoft.com/webhooks/v1/registration/validationEvents/c0bfd694-3075-4ec5-9a3c-733d3a890a1f",
    "ResourceName": "test",
    "AuditUri": null,
    "ResourceChangeUtcDate": "2017-11-16T16:19:06.3520276+00:00"
}

Webhook API

認証

Webhook API へのすべての呼び出しは、承認ヘッダーのベアラー トークンを使用して認証されます。 https://api.partnercenter.microsoft.comにアクセスするためのアクセス トークンを取得します。 このトークンは、パートナー センター API の残りの部分にアクセスするために使用されるのと同じトークンです。

イベントの一覧の取得

Webhook API で現在サポートされているイベントの一覧を返します。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration/events

要求の例

GET /webhooks/v1/registration/events
content-type: application/json
authorization: Bearer eyJ0e.......
accept: */*
host: api.partnercenter.microsoft.com

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 183
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: aaaa0000-bb11-2222-33cc-444444dddddd
MS-RequestId: 79419bbb-06ee-48da-8221-e09480537dfc
X-Locale: en-US

[ "subscription-updated", "test-created", "usagerecords-thresholdExceeded" ]

イベントを受け取るために登録する

指定されたイベントを受け取るテナントを登録します。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration

要求の例

POST /webhooks/v1/registration
Content-Type: application/json
Authorization: Bearer eyJ0e.....
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length: 219

{
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": ["subscription-updated", "test-created"]
}

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 346
Content-Type: application/json; charset=utf-8
content-encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: bbbb1111-cc22-3333-44dd-555555eeeeee
MS-RequestId: f04b1b5e-87b4-4d95-b087-d65fffec0bd2

{
    "SubscriberId": "e82cac64-dc67-4cd3-849b-78b6127dd57d",
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": [ "subscription-updated", "test-created" ]
}

登録を表示する

テナントの Webhook イベント登録を返します。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration

要求の例

GET /webhooks/v1/registration
Content-Type: application/json
Authorization: Bearer ...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 341
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: cccc2222-dd33-4444-55ee-666666ffffff
MS-RequestId: ca30367d-4b24-4516-af08-74bba6dc6657
X-Locale: en-US

{
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": ["subscription-updated", "test-created"]
}

イベント登録を更新する

既存のイベント登録を更新します。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration

要求の例

PUT /webhooks/v1/registration
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOR...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length: 258

{
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": ["subscription-updated", "test-created"]
}

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 346
Content-Type: application/json; charset=utf-8
content-encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: bbbb1111-cc22-3333-44dd-555555eeeeee
MS-RequestId: f04b1b5e-87b4-4d95-b087-d65fffec0bd2

{
    "SubscriberId": "e82cac64-dc67-4cd3-849b-78b6127dd57d",
    "WebhookUrl": "{{YourCallbackUrl}}",
    "WebhookEvents": [ "subscription-updated", "test-created" ]
}

テスト イベントを送信して登録を検証する

Webhook の登録を検証するテスト イベントを生成します。 このテストは、パートナー センターからイベントを受信できることを検証するためのものです。 これらのイベントのデータは、最初のイベントが作成されてから 7 日後に削除されます。 検証イベントを送信する前に、登録 API を使用して"test-created" イベントに登録する必要があります。

Note

検証イベントを投稿する場合、スロットル制限は 1 分あたり 2 要求です。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration/validationEvents

要求の例

POST /webhooks/v1/registration/validationEvents
MS-CorrelationId: dddd3333-ee44-5555-66ff-777777aaaaaa
Authorization: Bearer ...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length:

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 181
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: eeee4444-ff55-6666-77aa-888888bbbbbb
MS-RequestId: 2f498d5a-a6ab-468f-98d8-93c96da09051
X-Locale: en-US

{ "correlationId": "eeee4444-ff55-6666-77aa-888888bbbbbb" }

イベントが配信されたことを確認する

検証イベントの現在の状態を返します。 この検証は、イベント配信の問題のトラブルシューティングに役立ちます。 応答には、イベントの配信を試行するたびに結果が含まれます。

リソースの URL

https://api.partnercenter.microsoft.com/webhooks/v1/registration/validationEvents/{correlationId}

要求の例

GET /webhooks/v1/registration/validationEvents/eeee4444-ff55-6666-77aa-888888bbbbbb
MS-CorrelationId: dddd3333-ee44-5555-66ff-777777aaaaaa
Authorization: Bearer ...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate

応答の例

HTTP/1.1 200
Status: 200
Content-Length: 469
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: ffff5555-aa66-7777-88bb-999999cccccc
MS-RequestId: 0843bdb2-113a-4926-a51c-284aa01d722e
X-Locale: en-US

{
    "correlationId": "eeee4444-ff55-6666-77aa-888888bbbbbb",
    "partnerId": "00234d9d-8c2d-4ff5-8c18-39f8afc6f7f3",
    "status": "completed",
    "callbackUrl": "{{YourCallbackUrl}}",
    "results": [{
        "responseCode": "OK",
        "responseMessage": "",
        "systemError": false,
        "dateTimeUtc": "2017-12-08T21:39:48.2386997"
    }]
}

署名の検証の例

コールバック コントローラーシグネチャのサンプル (ASP.NET)

[AuthorizeSignature]
[Route("webhooks/callback")]
public IHttpActionResult Post(PartnerResourceChangeCallBack callback)

署名の検証

次の例は、Webhook イベントからコールバックを受信しているコントローラーに Authorization 属性を追加する方法を示しています。

namespace Webhooks.Security
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Security.Cryptography;
    using System.Security.Cryptography.X509Certificates;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Web.Http;
    using System.Web.Http.Controllers;
    using Microsoft.Partner.Logging;

    /// <summary>
    /// Signature based Authorization
    /// </summary>
    public class AuthorizeSignatureAttribute : AuthorizeAttribute
    {
        private const string MsSignatureHeader = "x-ms-signature";
        private const string CertificateUrlHeader = "x-ms-certificate-url";
        private const string SignatureAlgorithmHeader = "x-ms-signature-algorithm";
        private const string MicrosoftCorporationIssuer = "O=Microsoft Corporation";
        private const string SignatureScheme = "Signature";

        /// <inheritdoc/>
        public override async Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            ValidateAuthorizationHeaders(actionContext.Request);

            await VerifySignature(actionContext.Request);
        }

        private static async Task<string> GetContentAsync(HttpRequestMessage request)
        {
            // By default the stream can only be read once and we need to read it here so that we can hash the body to validate the signature from microsoft.
            // Load into a buffer, so that the stream can be accessed here and in the api when it binds the content to the expected model type.
            await request.Content.LoadIntoBufferAsync();

            var s = await request.Content.ReadAsStreamAsync();
            var reader = new StreamReader(s);
            var body = await reader.ReadToEndAsync();

            // set the stream position back to the beginning
            if (s.CanSeek)
            {
                s.Seek(0, SeekOrigin.Begin);
            }

            return body;
        }

        private static void ValidateAuthorizationHeaders(HttpRequestMessage request)
        {
            var authHeader = request.Headers.Authorization;
            if (string.IsNullOrWhiteSpace(authHeader?.Parameter) && string.IsNullOrWhiteSpace(GetHeaderValue(request.Headers, MsSignatureHeader)))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Authorization header missing."));
            }

            var signatureHeaderValue = GetHeaderValue(request.Headers, MsSignatureHeader);
            if (authHeader != null
                && !string.Equals(authHeader.Scheme, SignatureScheme, StringComparison.OrdinalIgnoreCase)
                && !string.IsNullOrWhiteSpace(signatureHeaderValue)
                && !signatureHeaderValue.StartsWith(SignatureScheme, StringComparison.OrdinalIgnoreCase))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, $"Authorization scheme needs to be '{SignatureScheme}'."));
            }

            if (string.IsNullOrWhiteSpace(GetHeaderValue(request.Headers, CertificateUrlHeader)))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, $"Request header {CertificateUrlHeader} missing."));
            }

            if (string.IsNullOrWhiteSpace(GetHeaderValue(request.Headers, SignatureAlgorithmHeader)))
            {
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.BadRequest, $"Request header {SignatureAlgorithmHeader} missing."));
            }
        }

        private static string GetHeaderValue(HttpHeaders headers, string key)
        {
            headers.TryGetValues(key, out var headerValues);

            return headerValues?.FirstOrDefault();
        }

        private static async Task VerifySignature(HttpRequestMessage request)
        {
            // Get signature value from either authorization header or x-ms-signature header.
            var base64Signature = request.Headers.Authorization?.Parameter ?? GetHeaderValue(request.Headers, MsSignatureHeader).Split(' ')[1];
            var signatureAlgorithm = GetHeaderValue(request.Headers, SignatureAlgorithmHeader);
            var certificateUrl = GetHeaderValue(request.Headers, CertificateUrlHeader);
            var certificate = await GetCertificate(certificateUrl);
            var content = await GetContentAsync(request);
            var alg = signatureAlgorithm.Split('-'); // for example RSA-SHA1
            var isValid = false;

            var logger = GetLoggerIfAvailable(request);

            // Validate the certificate
            VerifyCertificate(certificate, request, logger);

            if (alg.Length == 2 && alg[0].Equals("RSA", StringComparison.OrdinalIgnoreCase))
            {
                var signature = Convert.FromBase64String(base64Signature);
                var csp = (RSACryptoServiceProvider)certificate.PublicKey.Key;

                var encoding = new UTF8Encoding();
                var data = encoding.GetBytes(content);

                var hashAlgorithm = alg[1].ToUpper();

                isValid = csp.VerifyData(data, CryptoConfig.MapNameToOID(hashAlgorithm), signature);
            }

            if (!isValid)
            {
                // log that we were not able to validate the signature
                logger?.TrackTrace(
                    "Failed to validate signature for webhook callback",
                    new Dictionary<string, string> { { "base64Signature", base64Signature }, { "certificateUrl", certificateUrl }, { "signatureAlgorithm", signatureAlgorithm }, { "content", content } });

                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Signature verification failed"));
            }
        }

        private static ILogger GetLoggerIfAvailable(HttpRequestMessage request)
        {
            return request.GetDependencyScope().GetService(typeof(ILogger)) as ILogger;
        }

        private static async Task<X509Certificate2> GetCertificate(string certificateUrl)
        {
            byte[] certBytes;
            using (var webClient = new WebClient())
            {
                certBytes = await webClient.DownloadDataTaskAsync(certificateUrl);
            }

            return new X509Certificate2(certBytes);
        }

        private static void VerifyCertificate(X509Certificate2 certificate, HttpRequestMessage request, ILogger logger)
        {
            if (!certificate.Verify())
            {
                logger?.TrackTrace("Failed to verify certificate for webhook callback.", new Dictionary<string, string> { { "Subject", certificate.Subject }, { "Issuer", certificate.Issuer } });

                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Certificate verification failed."));
            }

            if (!certificate.Issuer.Contains(MicrosoftCorporationIssuer))
            {
                logger?.TrackTrace($"Certificate not issued by {MicrosoftCorporationIssuer}.", new Dictionary<string, string> { { "Issuer", certificate.Issuer } });

                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.Unauthorized, $"Certificate not issued by {MicrosoftCorporationIssuer}."));
            }
        }
    }
}