共用方式為


合作夥伴中心 Webhook

適用於:合作夥伴中心 |由 21Vianet 營運的合作夥伴中心 |美國政府適用的 Microsoft Cloud 合作夥伴中心

適當的角色:全域管理員 |計費 管理員 |管理員 代理程式 |銷售專員 |技術服務人員代理程式

合作夥伴中心 Webhook API 可讓合作夥伴註冊資源變更事件。 這些事件會以 HTTP POST 的形式傳遞至合作夥伴的已註冊 URL。 若要從合作夥伴中心接收事件,合作夥伴會裝載回呼,合作夥伴中心可以在其中張貼資源變更事件。 活動將會以數位方式簽署,讓合作夥伴能夠驗證它是否已從合作夥伴中心傳送。 Webhook 通知只會觸發至具有共同銷售最新設定的環境。

合作夥伴可以從合作夥伴中心支援的 Webhook 事件中選取,例如下列範例。

  • 偵測到 Azure 詐騙事件 (“azure-fraud-event-detected”)

    偵測到 Azure 詐騙事件時,就會引發此事件。

  • 委派 管理員 關聯性核准事件 (“dap-admin-relationship-approved”)

    當客戶租使用者核准委派 管理員 許可權時,就會引發此事件。

  • 客戶事件接受的轉銷商關係 (“reseller-relationship-accepted-by-customer”)

    當客戶租使用者核准 Reseller Relationship 時,就會引發此事件。

  • 委派 管理員 關聯性終止事件 (“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-updated”)

    當合作夥伴/客戶租使用者更新數據粒度委派 管理員 許可權時,就會引發此事件。

  • 細微 管理員 關聯性自動擴充事件 (“granular-admin-relationship-auto-extended”)

    當系統自動擴充數據粒度委派 管理員 許可權時,就會引發此事件。

  • 細微 管理員 關聯性終止事件 (“granular-admin-relationship-terminated”)

    當合作夥伴/客戶租使用者終止數據粒度委派 管理員 許可權時,就會引發此事件。

  • 新商務移轉已完成 (“new-commerce-migration-completed”)

    當新的商務移轉完成時,就會引發此事件。

  • 新商務移轉已建立 (“new-commerce-migration-created”)

    建立新的商務移轉時,就會引發此事件。

  • 新商務移轉失敗 (“new-commerce-migration-failed”)

    當新的商務移轉失敗時,就會引發此事件。

  • 新商務移轉排程失敗 (“new-commerce-migration-schedule-failed”)

    當新的商務移轉排程失敗時,就會引發此事件。

  • 推薦已建立事件 (“轉介建立”)

    建立轉介時,就會引發此事件。

  • 轉介更新事件 (“轉介更新”)

    此事件會在轉介更新時引發。

  • 相關推薦已建立事件 (“related-referral-created”)

    建立相關的轉介時,就會引發此事件。

  • 相關推薦更新事件 (“related-referral-updated”)

    當相關的轉介更新時,就會引發此事件。

  • 訂閱更新事件 (“subscription-updated”)

    當訂用帳戶變更時,就會引發此事件。 除了透過合作夥伴中心 API 進行變更之外,還會在內部變更時產生這些事件。

    注意

    訂閱變更與觸發訂閱更新事件之間的延遲時間最多為 48 小時。

  • 測試事件 (“test-created”)

    此事件可讓您透過要求測試事件,然後追蹤其進度,自我上線並測試註冊。 您可以在嘗試傳遞事件時看到從 Microsoft 收到的失敗訊息。 此限制僅適用於「測試建立」事件。 超過七天的數據將會清除。

  • 臨界值超過事件 (“usagerecords-thresholdExceeded”)

    當任何客戶的 Microsoft Azure 使用量超過其使用量預算(其閾值)時,就會引發此事件。 如需詳細資訊,請參閱(為您的客戶設定 Azure 消費預算/partner-center/set-an-azure-spending-budget-for-your-customers)。

未來的 Webhook 事件將會新增給合作夥伴無法控制之系統中變更的資源,而且會進行進一步更新,讓這些事件盡可能接近「即時」。 合作夥伴對於哪些事件為其業務增加價值的意見反應,有助於判斷要新增的事件。

如需合作夥伴中心所支援 Webhook 事件的完整清單,請參閱 合作夥伴中心 Webhook 事件

必要條件

  • 認證,如合作夥伴中心驗證所述。 此案例支援使用獨立應用程式和 App+使用者認證進行驗證。

從合作夥伴中心接收事件

若要從合作夥伴中心接收事件,您必須公開可公開的可公開端點。 因為此端點已公開,因此您必須驗證通訊是否來自合作夥伴中心。 您收到的所有 Webhook 事件都會使用鏈結至 Microsoft Root 的憑證進行數字簽署。 也會提供用來簽署事件的憑證連結。 這可讓您更新憑證,而不需要重新部署或重新設定服務。 合作夥伴中心會嘗試 10 次傳遞活動。 如果事件在 10 次嘗試之後仍未傳遞,則會移至離線佇列,而且不會再嘗試傳遞。

下列範例顯示從合作夥伴中心張貼的事件。

POST /webhooks/callback
Content-Type: application/json
Authorization: Signature VOhcjRqA4f7u/4R29ohEzwRZibZdzfgG5/w4fHUnu8FHauBEVch8m2+5OgjLZRL33CIQpmqr2t0FsGF0UdmCR2OdY7rrAh/6QUW+u+jRUCV1s62M76jbVpTTGShmrANxnl8gz4LsbY260LAsDHufd6ab4oejerx1Ey9sFC+xwVTa+J4qGgeyIepeu4YCM0oB2RFS9rRB2F1s1OeAAPEhG7olp8B00Jss3PQrpLGOoAr5+fnQp8GOK8IdKF1/abUIyyvHxEjL76l7DVQN58pIJg4YC+pLs8pi6sTKvOdSVyCnjf+uYQWwmmWujSHfyU37j2Fzz16PJyWH41K8ZXJJkw==
X-MS-Certificate-Url: https://3psostorageacct.blob.core.windows.net/cert/pcnotifications-dispatch.microsoft.com.cer
X-MS-Signature-Algorithm: rsa-sha256
Host: api.partnercenter.microsoft.com
Accept-Encoding: gzip, deflate
Content-Length: 195

{
    "EventName": "test-created",
    "ResourceUri": "http://localhost:16722/v1/webhooks/registration/test",
    "ResourceName": "test",
    "AuditUri": null,
    "ResourceChangeUtcDate": "2017-11-16T16:19:06.3520276+00:00"
}

注意

Authorization 標頭具有「簽章」的配置。 這是內容的base64編碼簽章。

如何驗證回呼

若要驗證從合作夥伴中心收到的回呼事件,請遵循下列步驟:

  1. 確認必要的標頭存在(Authorization、x-ms-certificate-url、x-ms-signature-algorithm)。

  2. 下載用來簽署內容的憑證 (x-ms-certificate-url)。

  3. 確認憑證鏈結。

  4. 確認憑證的「組織」。

  5. 使用UTF8編碼將內容讀入緩衝區。

  6. 建立 RSA 密碼編譯提供者。

  7. 確認數據符合已使用指定哈希演算法簽署的專案(例如 SHA256)。

  8. 如果驗證成功,請處理訊息。

注意

根據預設,簽章令牌會在授權標頭中傳送。 如果您在註冊中將 SignatureTokenToMsSignatureHeader 設定為 true,簽章令牌將會改為在 x-ms-signature 標頭中傳送。

事件模型

下表描述合作夥伴中心事件的屬性。

屬性

名稱 描述
EventName 事件的名稱。 在 {resource}-{action} 窗體中。 例如,“test-created”。
ResourceUri 已變更之資源的 URI。
ResourceName 已變更之資源的名稱。
AuditUrl 選擇性。 稽核記錄的 URI。
ResourceChangeUtcDate 發生資源變更時的日期和時間,以UTC格式顯示。

範例

下列範例顯示合作夥伴中心事件的結構。

{
    "EventName": "test-created",
    "ResourceUri": "http://api.partnercenter.microsoft.com/webhooks/v1/registration/validationEvents/c0bfd694-3075-4ec5-9a3c-733d3a890a1f",
    "ResourceName": "test",
    "AuditUri": null,
    "ResourceChangeUtcDate": "2017-11-16T16:19:06.3520276+00:00"
}

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: c0bcf3a3-46e9-48fd-8e05-f674b8fd5d66
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: 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" ]
}

檢視註冊

傳回租使用者的 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: c3b88ab0-b7bc-48d6-8c55-4ae6200f490a
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: 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" ]
}

傳送測試事件以驗證您的註冊

產生測試事件來驗證 Webhook 註冊。 此測試旨在驗證您可以從合作夥伴中心接收事件。 建立初始事件之後,將會刪除這些事件的數據七天。 您必須使用註冊 API 註冊「測試建立」事件,才能傳送驗證事件。

注意

張貼驗證事件時,每分鐘有2個要求的節流限制。

資源 URL

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

要求範例

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:

回應範例

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

確認事件已傳遞

傳回驗證事件的目前狀態。 這項驗證有助於針對事件傳遞問題進行疑難解答。 回應包含每次嘗試傳遞事件的結果。

資源 URL

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

要求範例

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

回應範例

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

簽章驗證的範例

範例回呼控制器簽章 (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 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}."));
            }
        }
    }
}