Webhook del Centro per i partner

Si applica a: Centro per i partner | Centro per i partner gestito da 21Vianet | Centro per i partner per Microsoft Cloud per il governo degli Stati Uniti

Ruoli appropriati: amministratore globale | Amministrazione di fatturazione | agente Amministrazione | Agente di vendita | Agente helpdesk

Le API webhook del Centro per i partner consentono ai partner di registrarsi per gli eventi di modifica delle risorse. Questi eventi vengono recapitati sotto forma di POST HTTP all'URL registrato del partner. Per ricevere un evento dal Centro per i partner, i partner ospiteranno un callback in cui il Centro per i partner può PUBBLICARE l'evento di modifica delle risorse. L'evento verrà firmato digitalmente in modo che il partner possa verificare che sia stato inviato dal Centro per i partner. Le notifiche webhook vengono attivate solo nell'ambiente con la configurazione più recente per la co-selling.

I partner possono selezionare eventi webhook, come gli esempi seguenti, supportati dal Centro per i partner.

  • Evento di frode di Azure rilevato ("azure-fraud-event-detected")

    Questo evento viene generato quando viene rilevato un evento di frode di Azure.

  • Evento approvato Amministrazione relazione delegato ("dap-admin-relationship-approved")

    Questo evento viene generato quando i privilegi di Amministrazione delegati sono stati approvati dal tenant del cliente.

  • Relazione rivenditore accettata dall'evento del cliente ("reseller-relationship-accepted-by-customer")

    Questo evento viene generato quando la relazione rivenditore viene approvata dal tenant del cliente.

  • Evento con terminazione relazione Amministrazione delegato ("dap-admin-relationship-terminate")

    Questo evento viene generato quando il Amministrazione delegate è stato terminato dal cliente.

  • Dap Amministrazione relazione terminata dall'evento Microsoft ("dap-admin-relationship-terminate-by-microsoft")

    Questo evento viene generato quando Microsoft termina DAP tra il tenant partner e il tenant del cliente quando DAP è inattivo per più di 90 giorni.

  • Evento attivato per l'assegnazione di accesso granulare Amministrazione ("granular-admin-access-assignment-activated")

    Questo evento viene generato quando l'assegnazione di accesso Granular Delegated Amministrazione Privilegi viene attivata dal partner dopo che i ruoli di Microsoft Entra vengono assegnati a gruppi di sicurezza specifici.

  • Evento creato granulare Amministrazione'assegnazione di accesso ("granular-admin-access-assignment-created")

    Questo evento viene generato quando l'assegnazione di accesso Granular Delegated Amministrazione Privilegi viene creata dal partner. I partner possono assegnare ruoli Microsoft Entra approvati dai clienti a specifici gruppi di sicurezza.

  • Evento eliminato granulare Amministrazione'assegnazione di accesso ("granular-admin-access-assignment-deleted")

    Questo evento viene generato quando l'assegnazione di accesso Granular Delegated Amministrazione Privilegi viene eliminata dal partner.

  • Evento aggiornato granulare dell'assegnazione di accesso Amministrazione ("granular-admin-access-assignment-updated")

    Questo evento viene generato quando l'assegnazione di accesso Granular Delegated Amministrazione Privilegi viene aggiornata dal partner.

  • Evento attivato Amministrazione relazione granulare ("granular-admin-relationship-activated")

    Questo evento viene generato quando viene creato il Amministrazione privilegi delegato granulare e attivo per consentire al cliente di approvare.

  • Evento approvato per Amministrazione relazione granulare ("granular-admin-relationship-approved")

    Questo evento viene generato quando i privilegi di Amministrazione delegate granulari sono stati approvati dal tenant del cliente.

  • Evento granulare Amministrazione relazione scaduta ("granular-admin-relationship-expired")

    Questo evento viene generato quando i privilegi di Amministrazione delegate granulari sono scaduti.

  • Evento aggiornato granulare Amministrazione relazione ("granular-admin-relationship-updated")

    Questo evento viene generato quando l'Amministrazione delegate granulare viene aggiornato dal tenant del partner o del cliente.

  • Evento esteso automatico della relazione granulare Amministrazione ("granular-admin-relationship-auto-extended")

    Questo evento viene generato quando il granulare delegato Amministrazione Privilegi viene esteso automaticamente dal sistema.

  • Evento granulare Amministrazione relazione terminata ("granular-admin-relationship-terminate")

    Questo evento viene generato quando il tenant partner/cliente termina con privilegi di Amministrazione delegate granulari.

  • Nuova migrazione commerciale completata ("new-commerce-migration-completed")

    Questo evento viene generato al termine della nuova migrazione commerciale.

  • Nuova migrazione commerciale creata ("new-commerce-migration-created")

    Questo evento viene generato quando viene creata la nuova migrazione commerciale.

  • Nuova migrazione commerciale non riuscita ("new-commerce-migration-failed")

    Questo evento viene generato quando la nuova migrazione commerciale non è riuscita.

  • Nuova pianificazione della migrazione commerciale non riuscita ("new-commerce-migration-schedule-failed")

    Questo evento viene generato quando la nuova pianificazione della migrazione commerciale non è riuscita.

  • Referral Created Event ("referral-created")

    Questo evento viene generato quando viene creata la segnalazione.

  • Evento aggiornato delle segnalazioni ("aggiornamento delle segnalazioni")

    Questo evento viene generato quando viene aggiornata la segnalazione.

  • Evento di segnalazione correlato creato ("related-referral-created")

    Questo evento viene generato quando viene creata la segnalazione correlata.

  • Evento aggiornato delle segnalazioni correlate ("related-referral-updated")

    Questo evento viene generato quando viene aggiornata la segnalazione correlata.

  • Evento aggiornato della sottoscrizione ("subscription-updated")

    Questo evento viene generato quando cambia la sottoscrizione. Questi eventi verranno generati quando si verifica una modifica interna oltre a quando vengono apportate modifiche tramite l'API del Centro per i partner.

    Nota

    Si verifica un ritardo di fino a 48 ore tra il momento in cui viene modificata una sottoscrizione e quando viene attivato l'evento Subscription Updated.

  • Evento test ("test-created")

    Questo evento consente di eseguire l'onboarding automatico e testare la registrazione richiedendo un evento di test e quindi monitorandone lo stato. È possibile visualizzare i messaggi di errore ricevuti da Microsoft durante il tentativo di recapitare l'evento. Questa restrizione si applica solo agli eventi "creati dal test". I dati precedenti a sette giorni verranno eliminati.

  • Evento soglia superata ("usagerecords-thresholdExceeded")

    Questo evento viene generato quando la quantità di utilizzo di Microsoft Azure per qualsiasi cliente supera il budget di spesa di utilizzo (soglia). Per altre informazioni, vedere (Impostare un budget di spesa di Azure per i clienti/partner-center/set-an-azure-spending-budget-for-your-customers).

Verranno aggiunti eventi webhook futuri per le risorse che cambiano nel sistema di cui il partner non è in controllo e verranno apportati ulteriori aggiornamenti per ottenere tali eventi il più vicino possibile al "tempo reale". Il feedback dei partner su cui gli eventi aggiungono valore all'azienda sarà utile per determinare quali nuovi eventi aggiungere.

Per un elenco completo degli eventi webhook supportati dal Centro per i partner, vedere Eventi webhook del Centro per i partner.

Prerequisiti

Ricezione di eventi dal Centro per i partner

Per ricevere eventi dal Centro per i partner, è necessario esporre un endpoint accessibile pubblicamente. Poiché questo endpoint è esposto, è necessario verificare che la comunicazione provena dal Centro per i partner. Tutti gli eventi webhook ricevuti vengono firmati digitalmente con un certificato concatenato alla radice Microsoft. Verrà fornito anche un collegamento al certificato usato per firmare l'evento. In questo modo il certificato verrà rinnovato senza dover ridistribuire o riconfigurare il servizio. Il Centro per i partner effettuerà 10 tentativi di consegnare l'evento. Se l'evento non viene ancora recapitato dopo 10 tentativi, verrà spostato in una coda offline e non verranno eseguiti ulteriori tentativi al recapito.

L'esempio seguente mostra un evento pubblicato dal Centro per i partner.

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

Nota

L'intestazione Authorization ha uno schema "Signature". Si tratta di una firma con codifica Base64 del contenuto.

Come autenticare il callback

Per autenticare l'evento di callback ricevuto dal Centro per i partner, seguire questa procedura:

  1. Verificare che le intestazioni necessarie siano presenti (Autorizzazione, x-ms-certificate-url, x-ms-signature-algorithm).

  2. Scarica il certificato usato per firmare il contenuto (x-ms-certificate-url).

  3. Verificare la catena di certificati.

  4. Verificare l'"organizzazione" del certificato.

  5. Leggere il contenuto con codifica UTF8 in un buffer.

  6. Crea un provider di crittografia RSA.

  7. Verificare che i dati corrispondano a quanto firmato con l'algoritmo hash specificato ,ad esempio SHA256.

  8. Se la verifica ha esito positivo, elaborare il messaggio.

Nota

Per impostazione predefinita, il token di firma verrà inviato in un'intestazione di autorizzazione. Se si imposta SignatureTokenToMsSignatureHeader su true nella registrazione, il token di firma verrà invece inviato nell'intestazione x-ms-signature.

Modello di evento

Nella tabella seguente vengono descritte le proprietà di un evento del Centro per i partner.

Proprietà

Nome Descrizione
EventName Nome dell'evento. Nel formato {resource}-{action}. Ad esempio, "test-created".
ResourceUri URI della risorsa modificata.
Resourcename Nome della risorsa modificata.
AuditUrl Facoltativo. URI del record Audit.
ResourceChangeUtcDate Data e ora, in formato UTC, quando si è verificata la modifica della risorsa.

Esempio

L'esempio seguente illustra la struttura di un evento del Centro per i partner.

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

Autenticazione

Tutte le chiamate alle API webhook vengono autenticate usando il token bearer nell'intestazione dell'autorizzazione. Acquisire un token di accesso per accedere https://api.partnercenter.microsoft.coma . Questo token è lo stesso token usato per accedere al resto delle API del Centro per i partner.

Ottenere un elenco di eventi

Restituisce un elenco degli eventi attualmente supportati dalle API webhook.

URL risorsa

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

Esempio di richiesta

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

Risposta di esempio

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

Registrarsi per ricevere gli eventi

Registra un tenant per ricevere gli eventi specificati.

URL risorsa

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

Esempio di richiesta

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

Risposta di esempio

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

Visualizzare una registrazione

Restituisce la registrazione dell'evento Webhooks per un tenant.

URL risorsa

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

Esempio di richiesta

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

Risposta di esempio

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

Aggiornare una registrazione di eventi

Aggiornamenti una registrazione eventi esistente.

URL risorsa

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

Esempio di richiesta

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

Risposta di esempio

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

Inviare un evento di test per convalidare la registrazione

Genera un evento di test per convalidare la registrazione dei webhook. Questo test è progettato per verificare che sia possibile ricevere eventi dal Centro per i partner. I dati per questi eventi verranno eliminati sette giorni dopo la creazione dell'evento iniziale. È necessario essere registrati per l'evento "test-created", usando l'API di registrazione, prima di inviare un evento di convalida.

Nota

Quando si pubblica un evento di convalida, è previsto un limite di 2 richieste al minuto.

URL risorsa

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

Esempio di richiesta

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:

Risposta di esempio

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

Verificare che l'evento sia stato recapitato

Restituisce lo stato corrente dell'evento di convalida. Questa verifica può essere utile per la risoluzione dei problemi di recapito degli eventi. La risposta contiene un risultato per ogni tentativo effettuato per recapitare l'evento.

URL risorsa

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

Esempio di richiesta

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

Risposta di esempio

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

Esempio di convalida della firma

Firma del controller di callback di esempio (ASP.NET)

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

Convalida della firma

Nell'esempio seguente viene illustrato come aggiungere un attributo di autorizzazione al controller che riceve i callback dagli eventi webhook.

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