合作夥伴中心 Webhook
適用於:合作夥伴中心 |由 21Vianet 營運的合作夥伴中心 |美國政府Microsoft雲端合作夥伴中心
適當的角色:全域管理員 |計費管理員 |系統管理代理程式 |銷售專員 |技術服務人員代理程式
合作夥伴中心 Webhook API 可讓合作夥伴註冊資源變更事件。 這些事件會以 HTTP POST 的形式傳遞至合作夥伴的已註冊 URL。 若要從合作夥伴中心接收事件,合作夥伴會裝載回呼,合作夥伴中心可以在其中張貼資源變更事件。 活動會以數位方式簽署,讓合作夥伴可以驗證它是否已從合作夥伴中心傳送。 Webhook 通知只會觸發至具有共同銷售最新設定的環境。
合作夥伴中心支援下列 Webhook 事件。
偵測到 Azure 詐騙事件 (“azure-fraud-event-detected”)
偵測到 Azure 詐騙事件時,就會引發此事件。
委派的管理員關聯性核准事件 (“dap-admin-relationship-approved”)
當客戶租使用者核准委派系統管理員許可權時,就會引發此事件。
客戶事件接受的轉銷商關係 (“reseller-relationship-accepted-by-customer”)
當客戶租使用者核准 Reseller Relationship 時,就會引發此事件。
客戶事件接受的間接轉銷商關係 (“indirect-reseller-relationship-accepted-by-customer”)
當客戶租使用者核准間接轉銷商關係時,就會引發此事件。
委派的管理員關聯性終止事件 (“dap-admin-relationship-terminated”)
當客戶終止委派管理員許可權時,就會引發此事件。
dap Admin Relationship terminated by Microsoft Event (“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”)
當合作夥伴或客戶租使用者終止細微委派系統管理員許可權時,就會引發此事件。
發票就緒事件 (“invoice-ready”)
當新的發票就緒時,就會引發此事件。
新商務移轉已完成 (“new-commerce-migration-completed”)
當新的商務移轉完成時,就會引發此事件。
新商務移轉已建立 (“new-commerce-migration-created”)
建立新的商務移轉時,就會引發此事件。
新商務移轉失敗 (“new-commerce-migration-failed”)
當新的商務移轉失敗時,就會引發此事件。
建立傳輸 (“create-transfer”)
建立傳輸時,就會引發此事件。
更新傳輸 (“update-transfer”)
更新傳輸時,就會引發此事件。
完整傳輸 (“complete-transfer”)
完成傳輸時,就會引發此事件。
失敗傳輸 (“fail-transfer”)
當傳輸失敗時,就會引發此事件。
新商務移轉排程失敗 (“new-commerce-migration-schedule-failed”)
當新的商務移轉排程失敗時,就會引發此事件。
推薦已建立事件 (“轉介建立”)
建立轉介時,就會引發此事件。
轉介更新事件 (“轉介更新”)
此事件會在轉介更新時引發。
相關推薦已建立事件 (“related-referral-created”)
建立相關的轉介時,就會引發此事件。
相關推薦更新事件 (“related-referral-updated”)
當相關的轉介更新時,就會引發此事件。
訂用帳戶使用中事件 (“subscription-active”)
啟動訂閱時,就會引發此事件。
注意
訂用帳戶作用中 Webhook 和對應的活動記錄事件目前僅適用於沙箱租使用者。
訂閱擱置事件 (“subscription-pending”)
當已順利收到對應的訂單,且訂用帳戶建立擱置時,就會引發此事件。
注意
訂用帳戶擱置的 Webhook 和對應的活動記錄事件目前僅適用於沙箱租使用者。
訂閱續約事件 (“subscription-renewed”)
當訂閱完成續約時,就會引發此事件。
注意
訂閱更新的 Webhook 和對應的活動記錄事件目前僅適用於沙箱租使用者。
訂閱更新事件 (“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編碼簽章。
如何驗證回呼
若要驗證從合作夥伴中心收到的回呼事件,請遵循下列步驟:
- 確認必要的標頭存在(Authorization、x-ms-certificate-url、x-ms-signature-algorithm)。
- 下載用來簽署內容的憑證 (x-ms-certificate-url)。
- 確認憑證鏈結。
- 確認憑證的「組織」。
- 使用UTF8編碼將內容讀入緩衝區。
- 建立 RSA 密碼編譯提供者。
- 確認數據符合已使用指定哈希演算法簽署的專案(例如 SHA256)。
- 如果驗證成功,請處理訊息。
注意
根據預設,簽章令牌會在授權標頭中傳送。 如果您在註冊中將 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: 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 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}."));
}
}
}
}