Webhooky Partnerského centra

Platí pro: Partnerské centrum | Partnerské centrum provozované společností 21Vianet | Partnerské centrum pro Microsoft Cloud pro státní správu USA

Příslušné role: Globální správce | Fakturační Správa | Správa agent | Obchodní agent | Agent helpdesku

Rozhraní API Webhooku Partnerského centra umožňují partnerům registrovat události změn prostředků. Tyto události se doručují ve formě HTTP POST na zaregistrovanou adresu URL partnera. Pokud chcete přijmout událost z Partnerského centra, partneři budou hostovat zpětné volání, kde Partnerské centrum může událost změny prostředku POST. Událost bude digitálně podepsaná, aby partner mohl ověřit, že byla odeslána z Partnerského centra.

Partneři můžou vybírat z událostí Webhooku, jako jsou následující příklady, které Partnerské centrum podporuje.

  • Zjištěná událost podvodu Azure ("azure-fraud-event-detected")

    Tato událost se vyvolá při zjištění události podvodu Azure.

  • Delegovaná Správa událost schválená relací (dap-admin-relationship-approved)

    Tato událost se vyvolá, když tenant zákazníka schválil delegovaná oprávnění Správa.

  • Vztah prodejce akceptovaný událostí zákazníka (reseller-relationship-accepted-by-customer)

    Tato událost se vyvolá, když je vztah prodejce schválen tenantem zákazníka.

  • Delegovaná událost ukončení relace Správa (dap-admin-relationship-terminated)

    Tato událost se vyvolá, když zákazník ukončil delegovaná oprávnění Správa.

  • Dap Správa Relace ukončená událostí Microsoftu (dap-admin-relationship-terminated-by-microsoft)

    Tato událost se vyvolá, když Microsoft ukončí DAP mezi klientem partnera a zákazníka, když je DAP neaktivní po dobu delší než 90 dnů.

  • Podrobná Správa aktivovaná událost přiřazení přístupu (granular-admin-access-assignment-activated)

    Tato událost se vyvolá, když partner aktivuje přiřazení přístupu k oprávněním podrobného delegovaného Správa po přiřazení rolí Microsoft Entra ke konkrétním skupinám zabezpečení.

  • Podrobná událost vytvoření přiřazení přístupu Správa ("granular-admin-access-assignment-created")

    Tato událost se vyvolá, když partner vytvoří podrobné přiřazení přístupu k oprávněním delegovaného Správa. Partneři můžou přiřadit role Microsoft Entra schválené zákazníkem ke konkrétním skupinám zabezpečení.

  • Podrobná Správa Odstraněná událost přiřazení přístupu (granular-admin-access-assignment-deleted)

    Tato událost se vyvolá, když partner odstraní podrobné delegované Správa přiřazení přístupu k oprávněním.

  • Podrobná událost aktualizace přiřazení přístupu Správa (granular-admin-access-assignment-updated)

    Tato událost se vyvolá, když partner aktualizuje podrobné přiřazení přístupu k delegovaným Správa oprávněním.

  • Podrobná Správa aktivovaná událost relace (granular-admin-relationship-activated)

    Tato událost se vyvolá, když se vytvoří podrobná delegovaná Správa oprávnění a aktivuje se, aby zákazník schvaloval.

  • Podrobná Správa událost schválená relací (granular-admin-relationship-approved)

    Tato událost se vyvolá, když byl tenantem zákazníka schválena podrobná delegovaná Správa oprávnění.

  • Podrobná událost Správa vypršení platnosti relace (granular-admin-relationship-expired)

    Tato událost se vyvolá, když vypršela platnost podrobných delegovaných Správa oprávnění.

  • Podrobná Správa aktualizovaná událost relace ("granular-admin-relationship-updated")

    Tato událost se vyvolá, když je členitá delegovaná Správa oprávnění buď aktualizována partnerem nebo tenantem zákazníka.

  • Podrobná Správa událost automatického rozšířeného vztahu (granular-admin-relationship-auto-extended)

    Tato událost se vyvolá, když systém automaticky rozšíří členitá delegovaná Správa oprávnění.

  • Podrobná Správa Událost ukončení relace (granular-admin-relationship-terminated)

    Tato událost se vyvolá, když je členitá delegovaná Správa oprávnění buď ukončena partnerem nebo tenantem zákazníka.

  • Migrace nového obchodu dokončena (new-commerce-migration-completed)

    Tato událost se vyvolá po dokončení nové migrace komerčního obchodu.

  • Vytvořená nová migrace commerce (new-commerce-migration-created)

    Tato událost se vyvolá při vytvoření nové migrace komerčního obchodu.

  • Nová migrace commerce selhala (new-commerce-migration-failed)

    Tato událost se vyvolá, když dojde k selhání migrace nového komerčního obchodu.

  • Nový plán migrace obchodování selhal (new-commerce-migration-schedule-failed)

    Tato událost se vyvolá, když se nezdaří nový plán migrace komerčního obchodu.

  • Událost vytvoření referenčního seznamu (vytvoření referenčního seznamu)

    Tato událost se vyvolá při vytvoření referenčního seznamu.

  • Aktualizovaná událost referenčního seznamu ("referenční seznam aktualizován")

    Tato událost se vyvolá při aktualizaci referenčního seznamu.

  • Související událost vytvoření referenčního seznamu (vytvoření souvisejícího referenčního seznamu)

    Tato událost se vyvolá při vytvoření souvisejícího referenčního seznamu.

  • Související událost aktualizace referenčního seznamu ("související-referenční seznam-aktualizován")

    Tato událost se vyvolá při aktualizaci souvisejícího referenčního seznamu.

  • Událost aktualizace odběru (aktualizace předplatného)

    Tato událost se vyvolá při změně odběru. Tyto události se vygenerují, pokud dojde k interní změně kromě toho, kdy se změny provádějí prostřednictvím rozhraní API Partnerského centra.

    Poznámka:

    Mezi časem, kdy se předplatné změní, a aktivací události Aktualizace odběru je zpoždění až 48 hodin.

  • Testovací událost (vytvoření testu)

    Tato událost vám umožní připojit se k registraci a otestovat registraci tak, že požádáte o testovací událost a pak budete sledovat její průběh. Při pokusu o doručení události se zobrazí zprávy o selhání, které microsoft přijímá. Toto omezení platí jenom pro události vytvořené testem. Data starší než sedm dnů se vymažou.

  • Prahová hodnota překročila událost ("usagerecords-thresholdExceeded")

    Tato událost se vyvolá, když množství využití Microsoft Azure pro každého zákazníka překročí rozpočet útraty využití (prahová hodnota). Další informace najdete v tématu (Nastavení rozpočtu útraty Azure pro zákazníky, partnerské centrum/set-an-azure-spending-budget-for-your-customers).

Budoucí události webhooku budou přidány pro prostředky, které se mění v systému, který partner nemá pod kontrolou, a další aktualizace budou provedeny tak, aby se tyto události co nejvíce blížily "reálnému čase". Zpětná vazba od partnerů o tom, které události přidávají hodnotu do své firmy, budou užitečné při určování nových událostí, které se mají přidat.

Úplný seznam událostí webhooku podporovaných Partnerským centrem najdete v tématu Události webhooku Partnerského centra.

Požadavky

  • Přihlašovací údaje popsané v ověřování v Partnerském centru Tento scénář podporuje ověřování pomocí samostatných přihlašovacích údajů aplikace i aplikace a uživatele.

Příjem událostí z Partnerského centra

Pokud chcete přijímat události z Partnerského centra, musíte zveřejnit veřejně přístupný koncový bod. Vzhledem k tomu, že je tento koncový bod vystavený, musíte ověřit, že komunikace pochází z Partnerského centra. Všechny události webhooku, které obdržíte, jsou digitálně podepsané certifikátem, který je zřetěděný s kořenem Microsoftu. Zobrazí se také odkaz na certifikát použitý k podepsání události. To umožní obnovení certifikátu, aniž byste museli znovu nasadit nebo znovu nakonfigurovat službu. Partnerské centrum provede 10 pokusů o doručení události. Pokud se událost stále nedoručí po 10 pokusech, přesune se do offline fronty a při doručení se neprovedou žádné další pokusy.

Následující ukázka ukazuje událost publikované z Partnerského centra.

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

Poznámka:

Autorizační hlavička má schéma "Podpis". Toto je podpis obsahu kódovaný v base64.

Ověření zpětného volání

Pokud chcete ověřit událost zpětného volání přijatou z Partnerského centra, postupujte takto:

  1. Ověřte, že jsou k dispozici požadovaná záhlaví (autorizace, x-ms-certificate-url, x-ms-signature-algorithm).

  2. Stáhněte si certifikát použitý k podepsání obsahu (x-ms-certificate-url).

  3. Ověřte řetěz certifikátů.

  4. Ověřte "organizaci" certifikátu.

  5. Přečtěte si obsah s kódováním UTF8 do vyrovnávací paměti.

  6. Vytvořte poskytovatele kryptografických služeb RSA.

  7. Ověřte, že data odpovídají tomu, co bylo podepsáno zadaným algoritmem hash (například SHA256).

  8. Pokud ověření proběhne úspěšně, zpracujte zprávu.

Poznámka:

Ve výchozím nastavení se token podpisu odešle v autorizační hlavičce. Pokud v registraci nastavíte SignatureTokenToMsSignatureHeader hodnotu true, token podpisu se místo toho odešle do hlavičky x-ms-signature.

Model událostí

Následující tabulka popisuje vlastnosti události Partnerského centra.

Vlastnosti

Název Popis
Eventname Název události. Ve formuláři {resource}-{action}. Například "test-created".
Identifikátor ResourceUri Identifikátor URI prostředku, který se změnil.
ResourceName Název prostředku, který se změnil.
AuditUrl Nepovinné. Identifikátor URI záznamu auditování.
ResourceChangeUtcDate Datum a čas ve formátu UTC, kdy došlo ke změně prostředku.

Vzorek

Následující ukázka ukazuje strukturu události Partnerského centra.

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

Rozhraní API webhooku

Ověřování

Všechna volání rozhraní API webhooku se ověřují pomocí nosné tokeny v autorizační hlavičce. Získejte přístupový token pro přístup https://api.partnercenter.microsoft.com. Tento token je stejný token, který se používá pro přístup ke zbytku rozhraní API Partnerského centra.

Získání seznamu událostí

Vrátí seznam událostí, které jsou aktuálně podporovány rozhraními API webhooku.

Adresa URL prostředku

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

Příklad požadavku

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

Příklad odpovědi

HTTP/1.1 200
Status: 200
Content-Length: 183
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: c0bcf3a3-46e9-48fd-8e05-f674b8fd5d66
MS-RequestId: 79419bbb-06ee-48da-8221-e09480537dfc
X-Locale: en-US

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

Registrace k příjmu událostí

Zaregistruje tenanta pro příjem zadaných událostí.

Adresa URL prostředku

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

Příklad požadavku

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

Příklad odpovědi

HTTP/1.1 200
Status: 200
Content-Length: 346
Content-Type: application/json; charset=utf-8
content-encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: 718f2336-8b56-4f42-93ac-54896047c59a
MS-RequestId: f04b1b5e-87b4-4d95-b087-d65fffec0bd2

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

Zobrazení registrace

Vrátí registraci události Webhooky pro tenanta.

Adresa URL prostředku

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

Příklad požadavku

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

Příklad odpovědi

HTTP/1.1 200
Status: 200
Content-Length: 341
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: c3b88ab0-b7bc-48d6-8c55-4ae6200f490a
MS-RequestId: ca30367d-4b24-4516-af08-74bba6dc6657
X-Locale: en-US

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

Aktualizace registrace události

Aktualizace existující registraci události.

Adresa URL prostředku

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

Příklad požadavku

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

Příklad odpovědi

HTTP/1.1 200
Status: 200
Content-Length: 346
Content-Type: application/json; charset=utf-8
content-encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: 718f2336-8b56-4f42-93ac-54896047c59a
MS-RequestId: f04b1b5e-87b4-4d95-b087-d65fffec0bd2

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

Odeslání testovací události pro ověření registrace

Vygeneruje testovací událost pro ověření registrace webhooků. Cílem tohoto testu je ověřit, že můžete přijímat události z Partnerského centra. Data pro tyto události budou odstraněna sedm dní po vytvoření počáteční události. Před odesláním ověřovací události musíte být zaregistrovaní pro událost "vytvoření testu" pomocí rozhraní API pro registraci.

Poznámka:

Při publikování události ověření platí limit 2 požadavků za minutu.

Adresa URL prostředku

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

Příklad požadavku

POST /webhooks/v1/registration/validationEvents
MS-CorrelationId: 3ef0202b-9d00-4f75-9cff-15420f7612b3
Authorization: Bearer ...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length:

Příklad odpovědi

HTTP/1.1 200
Status: 200
Content-Length: 181
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: 04af2aea-d413-42db-824e-f328001484d1
MS-RequestId: 2f498d5a-a6ab-468f-98d8-93c96da09051
X-Locale: en-US

{ "correlationId": "04af2aea-d413-42db-824e-f328001484d1" }

Ověření doručení události

Vrátí aktuální stav události ověření. Toto ověření může být užitečné při řešení potíží s doručováním událostí. Odpověď obsahuje výsledek pro každý pokus o doručení události.

Adresa URL prostředku

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

Příklad požadavku

GET /webhooks/v1/registration/validationEvents/04af2aea-d413-42db-824e-f328001484d1
MS-CorrelationId: 3ef0202b-9d00-4f75-9cff-15420f7612b3
Authorization: Bearer ...
Accept: */*
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate

Příklad odpovědi

HTTP/1.1 200
Status: 200
Content-Length: 469
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Vary: Accept-Encoding
MS-CorrelationId: 497e0a23-9498-4d6c-bd6a-bc4d6d0054e7
MS-RequestId: 0843bdb2-113a-4926-a51c-284aa01d722e
X-Locale: en-US

{
    "correlationId": "04af2aea-d413-42db-824e-f328001484d1",
    "partnerId": "00234d9d-8c2d-4ff5-8c18-39f8afc6f7f3",
    "status": "completed",
    "callbackUrl": "{{YourCallbackUrl}}",
    "results": [{
        "responseCode": "OK",
        "responseMessage": "",
        "systemError": false,
        "dateTimeUtc": "2017-12-08T21:39:48.2386997"
    }]
}

Příklad ověření podpisu

Ukázkový podpis kontroleru zpětného volání (ASP.NET)

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

Ověření podpisu

Následující příklad ukazuje, jak přidat autorizační atribut kontroleru, který přijímá zpětná volání z událostí Webhooku.

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