通过


合作伙伴中心的Webhooks

应用到:合作伙伴中心 |由世纪互联运营的合作伙伴中心 |美国政府Microsoft Cloud合作伙伴中心

相应的角色:计费管理员 | 管理员代理 | 销售代理 | 支持人员代理

合作伙伴可以设置 Webhook,以便在更改内容(如客户的订阅或市场产品/服务)时立即收到通知,而不是不断检查合作伙伴中心 API 以获取更新。 每当发生特定事件时,系统都会向合作伙伴的注册 URL 发送 HTTP POST,以便他们可以立即响应。 例如,合作伙伴可以快速处理订阅更改,或跟踪迁移的发生情况。

主要优点:

  • 自动化工作流: Webhook 负责例行检查,因此合作伙伴无需手动查找更新。 这简化了业务流程并节省时间。
  • 即时警报: 合作伙伴在发生重要事件(例如客户订阅被更改、暂停或取消)后立即收到通知。 这意味着他们可以更快地行动并提供更好的服务。
  • SaaS 产品/服务管理: 发布者可以使用 Webhook 使 SaaS 订阅的状态与合作伙伴中心保持同步,确保交易双方保持最新状态。
  • 迁移跟踪: Webhook 可帮助合作伙伴自动执行客户订阅迁移到新的商业平台并监视其订阅迁移。 提高效率:通过依靠事件通知而不是轮询更新,合作伙伴可减少其系统和合作伙伴中心 API 上的工作负荷。

合作伙伴中心 Webhook 的工作原理

合作伙伴中心 Webhook API 允许合作伙伴注册资源更改事件。 这些事件以 HTTP POST 的形式传递到合作伙伴的注册 URL。 若要从合作伙伴中心接收到事件,合作伙伴需要托管一个回调函数,以便合作伙伴中心可以在其中发送资源更改事件。 事件经过数字签名,以便合作伙伴可以验证它是否已从合作伙伴中心发送。 Webhook 通知仅会触发到具有最新 Co-sell 配置的环境。

合作伙伴中心支持以下 Webhook 事件。

  • Azure检测到欺诈事件(“azure-fraud-event-detected”)

    检测到Azure欺诈事件时,将引发此事件。

  • 委托管理员关系批准事件("dap-admin-relationship-approved")

    当委派的管理员权限由客户租户批准时,将引发此事件。

  • 客户接受经销商关系事件(“reseller-relationship-accepted-by-customer”)

    当客户租户批准了分销商关系后,将触发此事件。

  • 由客户接受的间接经销商关系事件(“indirect-reseller-relationship-accepted-by-customer”)

    当客户租户批准间接销售商关系时,将引发此事件。

  • 委托的管理员关系终止事件(“dap-admin-relationship-terminated”)

    当客户终止委派的管理员权限时,将引发此事件。

  • Dap 管理员关系由 Microsoft 终止事件(“dap-admin-relationship-terminated-by-microsoft”)

    当 DAP 处于非活动状态超过 90 天时,Microsoft终止合作伙伴与客户租户之间的 DAP 时,将引发此事件。

  • 细粒度管理员访问分配已激活事件(“granular-admin-access-assignment-activated”)

    当合作伙伴在将Microsoft Entra角色分配给特定安全组后激活粒度委派管理员特权访问分配时,将引发此事件。

  • 细粒度管理员访问分配已创建事件(“granular-admin-access-assignment-created”)

    当合作伙伴创建精细的委派管理员权限访问分配时,将引发此事件。 合作伙伴可以将客户批准的Microsoft Entra角色分配给特定的安全组。

  • 粒度管理员访问分配已删除事件(“granular-admin-access-assignment-deleted”)

    当合作伙伴删除细粒度委派的管理员权限访问分配时,将引发此事件。

  • 粒度管理员访问分配更新事件 (“granular-admin-access-assignment-updated”)

    当合作伙伴更新粒度委派管理员权限的访问分配时,会触发该事件。

  • 细粒度管理员关系激活事件 (“granular-admin-relationship-activated”)

    当粒度委派管理员权限被创建并激活以供客户批准时,将引发此事件。

  • 粒度管理关系已批准事件(“granular-admin-relationship-approved”)

    当客户租户批准细粒度委派管理员权限时,此事件将被引发。

  • 粒度管理员关系已过期事件(“granular-admin-relationship-expired”)

    当粒度委派管理员权限过期时,将引发此事件。

  • 粒度管理关系创建事件 (“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”)

    当创建传输时,将触发此事件。

  • 更新传输 (“update-transfer”)

    更新传输时,将引发此事件。

  • 完全传输(“complete-transfer”)

    此事件将在传输完成后触发。

  • 传输到期(“expire-transfer”)

    传输到期时会触发此事件。

  • 失败传输 (“fail-transfer”)

    传输失败时会引发此事件。

  • 新商务迁移计划失败(“new-commerce-migration-schedule-failed”)

    当新的商务迁移计划失败时,将引发此事件。

  • 引荐创建事件 (“referral-created”)

    创建推荐时将触发此事件。

  • 引荐更新事件 (“referral-updated”)

    当推荐更新时会触发此事件。

  • 相关引荐已创建事件(“related-referral-created”)

    当创建相关引荐时,会触发此事件。

  • 相关推荐更新事件(“related-referral-updated”)

    当相关引荐被更新时,此事件将被触发。

  • 订阅激活事件("subscription-active")

    激活订阅时将引发此事件。

    注意

    订阅活动 Webhook 和相应的活动日志事件目前仅对沙盒租户可用。

  • 订阅挂起事件(“subscription-pending”)

    当成功收到相应的订单且订阅创建处于挂起状态时,将引发此事件。

    注意

    订阅待处理 Webhook 和相应的活动日志事件目前仅适用于沙盒租户。

  • 订阅续订事件 (“subscription-renewed”)

    续订完成时,将触发此事件。

    注意

    当前,订阅续订的 Webhook 和相应的活动日志事件仅适用于沙盒租户。

  • 订阅更新事件(“subscription-updated”)

    订阅更改时会引发此事件。 这些事件的生成,既包括内部更改,也包括通过合作伙伴中心 API 的更改。

    注意

    订阅更改的时间和触发订阅更新事件的时间之间,延迟最长为 48 小时。

  • 测试事件(“test-created”)

    此活动允许你可以请求测试活动,然后跟踪其进度,以便进行自助注册和测试注册。 在尝试传递事件时,可以看到从Microsoft接收的失败消息。 此限制仅适用于“测试创建”事件。 清除超过 7 天的数据。

  • 阈值超出事件(“usagerecords-thresholdExceeded”)

    当任何客户的Microsoft Azure使用量超出其使用情况支出预算(其阈值)时,将引发此事件。 有关详细信息,请参阅(为客户设置Azure支出预算/合作伙伴中心/set-an-azure-spending-budget-for-your-customers)。

将来,会为那些在系统中发生更改且合作伙伴无法控制的资源添加 Webhook 事件,同时会进行进一步更新,以使这些事件尽可能接近“实时”。 合作伙伴关于哪些事件为其业务增加值的反馈有助于确定要添加的新事件。

  • 下载请求创建事件(“download-request-created”)

当某些用户使用 AI Assist 创建下载时,将引发此事件。

  • 下载请求已完成事件(“download-request-completed”)

当下载请求已完成并且可从客户工作区下载页面下载时,将引发此事件。

先决条件

从合作伙伴中心接收事件

若要从合作伙伴中心接收事件,必须公开一个可公开访问的端点。 由于此终结点已公开,因此必须验证通信是否来自合作伙伴中心。 你接收的所有 Webhook 事件都使用链接到 Microsoft 根证书的证书进行数字签名。 同时还提供了用于对事件进行签名的证书的链接。 这允许续订证书,而无需重新部署或重新配置服务。 合作伙伴中心尝试 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. 验证所需的标头是否存在(授权、x-ms-certificate-url、x-ms-signature-algorithm)。
  2. 下载用于对内容进行签名的证书 (x-ms-certificate-url)。
  3. 验证证书链。
  4. 验证证书的“组织”。
  5. 使用 UTF8 编码将内容读入缓冲区。
  6. 创建 RSA 加密提供程序。
  7. 验证数据是否与使用指定哈希算法(例如 SHA256)签名的内容匹配。
  8. 如果验证成功,请处理该消息。

注意

默认情况下,签名令牌在认证标头中发送。 如果在注册中将 SignatureTokenToMsSignatureHeader 设置为 true,则签名令牌将改为在 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/aaaabbbb-0000-cccc-1111-dddd2222eeee",
    "ResourceName": "test",
    "AuditUri": null,
    "ResourceChangeUtcDate": "2017-11-16T16:19:06.3520276+00:00"
}

Webhook API

身份验证

对 Webhook API 的所有调用都使用授权标头中的持有者令牌进行身份验证。 获取访问令牌以访问 https://api.partnercenter.microsoft.com。 此令牌是用于访问合作伙伴中心 API 其余部分的相同令牌。

获取事件列表

返回 Webhook API 当前支持的事件列表。

资源 URL

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

请求示例

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

响应示例

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

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

注册以接收事件

注册租户以接收指定的事件。

资源 URL

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

请求示例

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

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

响应示例

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

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

查看注册信息

返回租户的 Webhook 事件注册信息。

资源链接(URL)

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

请求示例

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

响应示例

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

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

更新活动注册

更新现有事件登记。

资源 URL

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

请求示例

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

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

响应示例

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

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

发送测试事件以验证注册

生成用于验证 Webhook 注册的测试事件。 此测试旨在验证是否可以从合作伙伴中心接收事件。 创建初始事件七天后,将删除这些事件的数据。 在发送验证事件之前,必须使用注册 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)

签名验证

以下示例演示如何向从 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 StreamReaders;
            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}."));
            }
        }
    }
}