Webhooks de l’Espace partenaires

S’applique à : Espace partenaires | Espace partenaires géré par 21Vianet | Espace partenaires de Microsoft Cloud for US Government

Rôles appropriés : Administrateur général | Administration de facturation | agent Administration | Agent commercial | Agent du support technique

Les API webhook de l’Espace partenaires permettent aux partenaires de s’inscrire aux événements de modification des ressources. Ces événements sont transmis sous forme de HTTP POST à l’URL enregistrée par le partenaire. Pour recevoir un événement de l’Espace partenaires, les partenaires hébergeront un rappel où l’Espace partenaires peut envoyer en mode POST l’événement de modification de ressource. L’événement sera signé numériquement afin que le partenaire puisse vérifier qu’il a été envoyé à partir de l’Espace partenaires. Les notifications webhook sont déclenchées uniquement dans l’environnement qui a la dernière configuration pour la co-vente.

Les partenaires peuvent sélectionner des événements Webhook, comme les exemples suivants, pris en charge par l’Espace partenaires.

  • Événement de fraude Azure détecté (« azure-fraud-event-détecté »)

    Cet événement est déclenché lorsque l’événement de fraude Azure est détecté.

  • Événement délégué Administration approuvé par la relation (« dap-admin-relationship-approved »)

    Cet événement est déclenché lorsque les privilèges de Administration délégués ont été approuvés par le client client.

  • Relation de revendeur acceptée par l’événement client (« reseller-relationship-accepted-by-customer »)

    Cet événement est déclenché lorsque la relation de revendeur est approuvée par le client client.

  • Événement délégué Administration relation terminée (« dap-admin-relationship-terminated »)

    Cet événement est déclenché lorsque les privilèges de Administration délégués ont été arrêtés par le client.

  • Dap Administration Relation terminée par l’événement Microsoft (« dap-admin-relationship-terminated-by-microsoft »)

    Cet événement est déclenché lorsque Microsoft met fin à DAP entre le partenaire et le client lorsque DAP est inactif pendant plus de 90 jours.

  • Événement granulaire Administration activation de l’affectation d’accès (« granular-admin-access-assignment-activated »)

    Cet événement est déclenché lorsque l’attribution d’accès Administration délégué granulaire est activée par le partenaire une fois que les rôles Microsoft Entra sont attribués à des groupes de sécurité spécifiques.

  • Événement granulaire Administration attribution d’accès créé (« granular-admin-access-assignment-created »)

    Cet événement est déclenché lorsque l’attribution d’accès aux privilèges délégués granulaires Administration est créée par le partenaire. Les partenaires peuvent attribuer des rôles Microsoft Entra approuvés par le client à des groupes de sécurité spécifiques.

  • Événement de suppression d’attribution d’accès granulaire Administration (« granular-admin-access-assignment-deleted »)

    Cet événement est déclenché lorsque l’attribution d’accès aux privilèges délégués granulaires Administration est supprimée par le partenaire.

  • Événement granulaire Administration attribution d’accès mis à jour (« granular-admin-access-assignment-updated »)

    Cet événement est déclenché lorsque l’attribution d’accès aux privilèges délégués granulaires Administration est mise à jour par le partenaire.

  • Événement granulaire Administration relation activé (« granular-admin-relationship-activated »)

    Cet événement est déclenché lorsque les privilèges de Administration délégué granulaires sont créés et actifs pour que le client approuve.

  • Événement granulaire Administration relation approuvée (« granular-admin-relationship-approved »)

    Cet événement est déclenché lorsque les privilèges de Administration délégué granulaire ont été approuvés par le client client.

  • Événement granulaire Administration relation expiré (« granular-admin-relationship-expire »)

    Cet événement est déclenché lorsque les privilèges de Administration délégué granulaires ont expiré.

  • Événement granulaire Administration relation mise à jour (« granular-admin-relationship-updated »)

    Cet événement est déclenché lorsque les privilèges de Administration délégué granulaires sont mis à jour par le locataire partenaire/client.

  • Événement étendu automatique de relation granulaire Administration (« granular-admin-relationship-auto-extended »)

    Cet événement est déclenché lorsque les privilèges délégués granulaires Administration sont automatiquement étendus par le système.

  • Événement granulaire Administration relation terminée (« granular-admin-relationship-terminated »)

    Cet événement est déclenché lorsque les privilèges de Administration délégué granulaires sont arrêtés par le locataire partenaire/client.

  • Nouvelle migration commerciale terminée (« new-commerce-migration-completed »)

    Cet événement est déclenché lorsque la nouvelle migration commerciale est terminée.

  • Nouvelle migration de commerce créée (« new-commerce-migration-created »)

    Cet événement est déclenché lorsque la nouvelle migration commerciale est créée.

  • Échec de la migration du nouveau commerce (« new-commerce-migration-failed »)

    Cet événement est déclenché lorsque la nouvelle migration commerciale a échoué.

  • Échec de la planification de la migration du nouveau commerce (« new-commerce-migration-schedule-failed »)

    Cet événement est déclenché lorsque la nouvelle planification de migration commerciale a échoué.

  • Événement de création de référence (« référence créée »)

    Cet événement est déclenché lors de la création de la référence.

  • Événement de mise à jour de référence (« référence mise à jour »)

    Cet événement est déclenché lorsque la référence est mise à jour.

  • Événement de création de référence connexe (« related-referral-created »)

    Cet événement est déclenché lorsque la référence associée est créée.

  • Événement de mise à jour de référence connexe (« related-referral-updated »)

    Cet événement est déclenché lorsque la référence associée est mise à jour.

  • Événement mis à jour de l’abonnement (« abonnement mis à jour »)

    Cet événement est déclenché lorsque l’abonnement change. Ces événements sont générés lorsqu’il existe une modification interne en plus du moment où des modifications sont apportées via l’API espace partenaires.

    Remarque

    Il existe un délai allant jusqu’à 48 heures entre le moment où un abonnement change et lorsque l’événement Subscription Updated est déclenché.

  • Événement de test (« test créé »)

    Cet événement vous permet d’intégrer et de tester automatiquement votre inscription en demandant un événement de test, puis en suivant sa progression. Vous pouvez voir les messages d’échec reçus par Microsoft lors de la tentative de remise de l’événement. Cette restriction s’applique uniquement aux événements « créés par test ». Les données antérieures à sept jours seront vidées.

  • Threshold Exceeded Event (« usagerecords-thresholdExceeded »)

    Cet événement est déclenché lorsque la quantité d’utilisation de Microsoft Azure pour tous les clients dépasse leur budget de dépense d’utilisation (leur seuil). Pour plus d’informations, consultez (Définir un budget de dépense Azure pour vos clients/partner-center/set-an-azure-spending-budget-for-your-customers).

Les événements webhook futurs seront ajoutés pour les ressources qui changent dans le système dont le partenaire n’est pas le contrôle, et d’autres mises à jour seront apportées pour obtenir ces événements aussi près de « temps réel » que possible. Les commentaires des partenaires sur lesquels les événements ajoutent de la valeur à leur entreprise seront utiles pour déterminer les nouveaux événements à ajouter.

Pour obtenir la liste complète des événements webhook pris en charge par l’Espace partenaires, consultez les événements webhook de l’Espace partenaires.

Prérequis

  • Informations d’identification, comme décrit dans Authentification auprès de l’Espace partenaires. Ce scénario prend en charge l’authentification avec les informations d’identification d’application et d’application+utilisateur autonomes.

Réception d’événements à partir de l’Espace partenaires

Pour recevoir des événements à partir de l’Espace partenaires, vous devez exposer un point de terminaison accessible publiquement. Étant donné que ce point de terminaison est exposé, vous devez vérifier que la communication provient de l’Espace partenaires. Tous les événements Webhook que vous recevez sont signés numériquement avec un certificat qui se chaîne à la racine Microsoft. Un lien vers le certificat utilisé pour signer l’événement est également fourni. Cela permet au certificat d’être renouvelé sans avoir à redéployer ou reconfigurer votre service. L’Espace partenaires effectue 10 tentatives de remise de l’événement. Si l’événement n’est toujours pas remis après 10 tentatives, il est déplacé dans une file d’attente hors connexion et aucune autre tentative n’est effectuée à la remise.

L’exemple suivant montre un événement publié à partir de l’Espace partenaires.

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

Remarque

L’en-tête d’autorisation a un schéma de « Signature ». Il s’agit d’une signature encodée en base64 du contenu.

Comment authentifier le rappel

Pour authentifier l’événement de rappel reçu à partir de l’Espace partenaires, procédez comme suit :

  1. Vérifiez que les en-têtes requis sont présents (Authorization, x-ms-certificate-url, x-ms-signature-algorithm).

  2. Téléchargez le certificat utilisé pour signer le contenu (x-ms-certificate-url).

  3. Vérifiez la chaîne de certificats.

  4. Vérifiez l'« organisation » du certificat.

  5. Lisez le contenu avec l’encodage UTF8 dans une mémoire tampon.

  6. Créez un fournisseur de chiffrement RSA.

  7. Vérifiez que les données correspondent à ce qui a été signé avec l’algorithme de hachage spécifié (par exemple SHA256).

  8. Si la vérification réussit, traitez le message.

Remarque

Par défaut, le jeton de signature est envoyé dans un en-tête d’autorisation. Si vous définissez SignatureTokenToMsSignatureHeader sur true dans votre inscription, le jeton de signature est envoyé dans l’en-tête x-ms-signature à la place.

Modèle d’événement

Le tableau suivant décrit les propriétés d’un événement espace partenaires.

Propriétés

Nom Description
EventName Nom de l’événement. Sous la forme {resource}-{action}. Par exemple, « test-created ».
ResourceUri URI de la ressource qui a changé.
ResourceName Nom de la ressource qui a changé.
AuditUrl facultatif. URI de l’enregistrement Audit.
ResourceChangeUtcDate Date et heure, au format UTC, lorsque la modification de la ressource s’est produite.

Exemple

L’exemple suivant montre la structure d’un événement espace partenaires.

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

Authentification

Tous les appels aux API Webhook sont authentifiés à l’aide du jeton du porteur dans l’en-tête d’autorisation. Acquérir un jeton d’accès pour accéder https://api.partnercenter.microsoft.com. Ce jeton est le même que celui utilisé pour accéder au reste des API de l’Espace partenaires.

Obtenir la liste des événements

Retourne une liste des événements actuellement pris en charge par les API Webhook.

URL de la ressource

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

Exemple de requête

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

Exemple de réponse

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

S’inscrire pour recevoir des événements

Inscrit un locataire pour recevoir les événements spécifiés.

URL de la ressource

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

Exemple de requête

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

Exemple de réponse

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

Afficher une inscription

Retourne l’inscription d’événements Webhooks pour un locataire.

URL de la ressource

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

Exemple de requête

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

Exemple de réponse

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

Mettre à jour une inscription d’événement

Mises à jour une inscription d’événement existante.

URL de la ressource

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

Exemple de requête

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

Exemple de réponse

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

Envoyer un événement de test pour valider votre inscription

Génère un événement de test pour valider l’inscription des Webhooks. Ce test est destiné à valider que vous pouvez recevoir des événements de l’Espace partenaires. Les données de ces événements seront supprimées sept jours après la création de l’événement initial. Vous devez être inscrit pour l’événement « test-created », à l’aide de l’API d’inscription, avant d’envoyer un événement de validation.

Remarque

Il existe une limite de 2 requêtes par minute lors de la publication d’un événement de validation.

URL de la ressource

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

Exemple de requête

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:

Exemple de réponse

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

Vérifier que l’événement a été remis

Retourne l’état actuel de l’événement de validation. Cette vérification peut être utile pour résoudre les problèmes de remise d’événements. La réponse contient un résultat pour chaque tentative effectuée pour remettre l’événement.

URL de la ressource

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

Exemple de requête

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

Exemple de réponse

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

Exemple de validation de signature

Exemple de signature du contrôleur de rappel (ASP.NET)

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

Signature Validation

L’exemple suivant montre comment ajouter un attribut d’autorisation au contrôleur qui reçoit des rappels à partir d’événements 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}."));
            }
        }
    }
}