Веб-перехватчики Центра партнеров

Область применения: Центр партнеров | Центр партнеров, управляемый 21Vianet | Центр партнеров для Microsoft Cloud for US Government

Соответствующие роли: глобальный администратор | Администратор выставления счетов | Агент администрирования | Агент продаж | Агент helpdesk

API-интерфейсы веб-перехватчика Центра партнеров позволяют партнерам регистрировать события изменения ресурсов. Эти события доставляются в виде HTTP POS-адресов в зарегистрированный URL-адрес партнера. Чтобы получить событие из Центра партнеров, партнеры размещают обратный вызов, в котором Центр партнеров может опубликовать событие изменения ресурса. Событие подписывается цифровой подписью, чтобы партнер смог убедиться, что он был отправлен из Центра партнеров. Уведомления веб-перехватчика активируются только в среде с последней конфигурацией для совместной продажи.

Центр партнеров поддерживает следующие события веб-перехватчика.

  • Обнаружено событие мошенничества Azure ("azure-fraud-event-detected")

    Это событие возникает при обнаружении события мошенничества Azure.

  • Делегированное утверждение связи администратора ("dap-admin-relationship-approved")

    Это событие возникает, когда делегированные права администратора утверждены клиентом клиента.

  • Отношение торгового посредника, принятое событием клиента ("торговых посредников, принятых клиентом")

    Это событие возникает, когда клиент клиента утверждает связь торгового посредника.

  • Непрямые отношения торгового посредника, принятые событием клиента ("косвенный торговый посредник,принятый клиентом")

    Это событие возникает, когда клиент клиента утверждает связь косвенного торгового посредника.

  • Делегированное событие связи администратора ("dap-admin-relationship-terminated")

    Это событие возникает, когда клиент завершает делегированные права администратора.

  • Связь администратора Dap прекращена событием Майкрософт ("dap-admin-relationship-terminated-by-microsoft")

    Это событие возникает, когда корпорация Майкрософт завершает DAP между партнером и клиентом, когда DAP неактивна более 90 дней.

  • Детализированное событие назначения доступа администратора активировано ("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-activated")

    Это событие возникает при создании и активном делегировании прав администратора для утверждаемого клиента.

  • Детализированное утверждение отношения администратора ("granular-admin-relationship-approved")

    Это событие возникает, когда клиент утверждает права делегированного администратора.

  • Детализированное событие отношения администратора с истекшим сроком действия ("гранулярно-администратор-отношение-истекло")

    Это событие возникает при истечении срока действия детализации делегированных прав администратора.

  • Детализированное событие создания связи администратора ("granular-admin-relationship-created")

    Это событие возникает при создании подробных делегированных прав администратора.

  • Детализированное событие обновления связи администратора ("granular-admin-relationship-updated")

    Это событие возникает, когда клиент или партнер обновляют права делегированного администратора.

  • Детализированное расширенное событие связи администратора ("granular-admin-relationship-auto-extended")

    Это событие возникает, когда система автоматически расширяет права делегированного администратора.

  • Детализированное событие связи администратора ("granular-admin-relationship-terminated")

    Это событие возникает, когда партнер или клиент завершает детализированные делегированные права администратора.

  • Событие "Готово к счету" ("счет-готово")

    Это событие возникает при готовности нового счета.

  • Завершена миграция новой коммерции ("new-commerce-migration-completed")

    Это событие возникает при завершении миграции новой коммерции.

  • Создание новой коммерческой миграции ("new-commerce-migration-created")

    Это событие возникает при создании новой коммерческой миграции.

  • Сбой миграции новой коммерции ("new-commerce-migration-failed")

    Это событие возникает при сбое новой коммерческой миграции.

  • Создание передачи ("create-transfer")

    Это событие возникает при создании передачи.

  • Обновление передачи ("обновление-передача")

    Это событие возникает при обновлении передачи.

  • Полная передача ("полная передача")

    Это событие возникает при завершении передачи.

  • Сбой передачи ("отработка отказа")

    Это событие возникает при сбое передачи.

  • Сбой расписания миграции новой коммерции ("new-commerce-migration-schedule-failed")

    Это событие возникает при сбое нового расписания миграции коммерции.

  • Событие создания рефералов ("реферал-создано")

    Это событие возникает при создании ссылки.

  • Обновленное событие рефералов (обновлено рефералов)

    Это событие возникает при обновлении ссылки.

  • Связанное событие создания рефералов (созданное со связанными рефералами)

    Это событие возникает при создании связанной ссылки.

  • Связанное событие обновления рефералов (связанное с реферальным обновлением)

    Это событие возникает при обновлении связанной ссылки.

  • Активное событие подписки ("subscription-active")

    Это событие возникает при активации подписки.

    Примечание.

    В настоящее время для клиентов песочницы доступны только активные веб-перехватчики подписки и соответствующее событие журнала действий.

  • Ожидающие подписки события ("подписка-ожидание")

    Это событие возникает, когда соответствующий заказ был успешно получен, и создание подписки ожидается.

    Примечание.

    Ожидается веб-перехватчик подписки и соответствующее событие журнала действий доступно только для клиентов песочницы в настоящее время.

  • Продленное событие подписки ("продленное подпиской")

    Это событие возникает, когда подписка завершит продление.

    Примечание.

    Обновление веб-перехватчика подписки и соответствующее событие журнала действий доступно только для клиентов песочницы.

  • Обновленное событие подписки (обновление подписки)

    Это событие возникает при изменении подписки. Эти события создаются при наличии внутреннего изменения в дополнение к тому, когда изменения вносятся через API Центра партнеров.

    Примечание.

    Существует задержка до 48 часов между временем изменения подписки и активацией события обновления подписки.

  • Тестовое событие ("тестовое создание")

    Это событие позволяет самостоятельно подключиться и проверить регистрацию, запросить тестовое событие и отслеживать его ход выполнения. При попытке доставки события вы увидите сообщения об ошибках, полученные от Корпорации Майкрософт. Это ограничение применяется только к событиям, созданным тестом. Данные старше семи дней очищаются.

  • Превышение порогового значения ("usagerecords-thresholdExceed")

    Это событие возникает, когда объем использования Microsoft Azure для любого клиента превышает бюджет расходов на использование (пороговое значение). Дополнительные сведения см. в разделе (Настройка бюджета расходов Azure для клиентов/ партнеров/set-an-azure-spending-budget-for-your-customers).

Будущие события веб-перехватчика будут добавлены для ресурсов, которые изменяются в системе, над которым партнер не управляет, и дальнейшие обновления будут сделаны, чтобы получить эти события как можно ближе к "в режиме реального времени". Отзывы партнеров о том, какие события добавляют ценность для своего бизнеса, полезно определить, какие новые события следует добавить.

Полный список событий веб-перехватчика, поддерживаемых Центром партнеров, см. в разделе "События веб-перехватчика Центра партнеров".

Необходимые компоненты

  • Учетные данные, описанные в статье о проверке подлинности в Центре партнеров. Этот сценарий поддерживает проверку подлинности с помощью автономных учетных данных приложения и приложения и пользователя.

Получение событий из Центра партнеров

Чтобы получать события из Центра партнеров, необходимо предоставить общедоступную конечную точку. Так как эта конечная точка предоставляется, необходимо убедиться, что обмен данными осуществляется из Центра партнеров. Все события веб-перехватчика, которые вы получаете, имеют цифровую подпись с сертификатом, который цепочки с 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"
}

Примечание.

Заголовок авторизации имеет схему "Подпись". Это сигнатура в кодировке 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. Если проверка выполнена успешно, обработайте сообщение.

Примечание.

По умолчанию маркер подписи отправляется в заголовке авторизации. Если в регистрации задано значение SignatureTokenTokenToMsSignatureHeader , маркер подписи отправляется в заголовке подписи x-ms-signature.

Модель событий

В следующей таблице описаны свойства события Центра партнеров.

Свойства

Имя Описание
EventName Имя события. В форме {resource}-{action}. Например, "test-created".
ResourceUri URI ресурса, который изменился.
Имя ресурса Имя измененного ресурса.
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"
}

API веб-перехватчика

Проверка подлинности

Все вызовы API веб-перехватчика проходят проверку подлинности с помощью маркера носителя в заголовке авторизации. Получение маркера доступа для доступа https://api.partnercenter.microsoft.com. Этот маркер является тем же маркером, который используется для доступа к остальным API Центра партнеров.

Получение списка событий

Возвращает список событий, поддерживаемых 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" ]
}

Просмотр регистрации

Возвращает регистрацию событий веб-перехватчиков для клиента.

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" ]
}

Отправка тестового события для проверки регистрации

Создает тестовое событие для проверки регистрации веб-перехватчиков. Этот тест предназначен для проверки того, что вы можете получать события из Центра партнеров. Данные для этих событий удаляются семь дней после создания первоначального события. Перед отправкой события проверки необходимо зарегистрировать событие "test-create" с помощью API регистрации.

Примечание.

При публикации события проверки существует ограничение в 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)

Проверка подписи

В следующем примере показано, как добавить атрибут авторизации в контроллер, получающий обратные вызовы из событий веб-перехватчика.

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}."));
            }
        }
    }
}