파트너 센터 웹후크
적용 대상: 파트너 센터 | 21Vianet에서 운영되는 파트너 센터 | Microsoft Cloud for US Government 파트너 센터
적절한 역할: 전역 관리자 | 청구 관리자 | 관리 에이전트 | 판매 에이전트 | 기술 지원팀 에이전트
파트너 센터 웹후크 API를 사용하면 파트너가 리소스 변경 이벤트에 등록할 수 있습니다. 이러한 이벤트는 파트너의 등록된 URL에 HTTP POST 형식으로 전달됩니다. 파트너 센터에서 이벤트를 받으려면 파트너 센터에서 리소스 변경 이벤트를 게시할 수 있는 콜백을 호스트합니다. 이 이벤트는 파트너가 파트너 센터에서 전송되었는지 확인할 수 있도록 디지털 서명됩니다. 웹후크 알림은 공동 판매에 대한 최신 구성이 있는 환경으로만 트리거됩니다.
파트너 센터에서는 다음 웹후크 이벤트를 지원합니다.
Azure Fraud 이벤트 감지됨("azure-fraud-event-detected")
이 이벤트는 Azure 사기 이벤트가 감지될 때 발생합니다.
위임된 관리자 관계 승인 이벤트("dap-admin-relationship-approved")
이 이벤트는 위임된 관리자 권한이 고객 테넌트에 의해 승인될 때 발생합니다.
고객 이벤트에서 수락한 재판매인 관계("reseller-relationship-accepted-by-customer")
이 이벤트는 고객 테넌트가 재판매인 관계를 승인할 때 발생합니다.
고객 이벤트에서 수락한 간접 재판매인 관계("간접 재판매인-관계-수락 고객")
이 이벤트는 고객 테넌트가 간접 재판매인 관계를 승인할 때 발생합니다.
위임된 관리자 관계 종료 이벤트("dap-admin-relationship-terminated")
이 이벤트는 고객이 위임된 관리자 권한을 종료할 때 발생합니다.
Microsoft 이벤트에 의해 종료된 Dap 관리자 관계("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")
이 이벤트는 전송이 업데이트될 때 발생합니다.
전체 전송("전체 전송")
이 이벤트는 전송이 완료될 때 발생합니다.
전송 실패("장애 전송")
이 이벤트는 전송이 실패할 때 발생합니다.
새 상거래 마이그레이션 일정 실패("new-commerce-migration-schedule-failed")
이 이벤트는 새 상거래 마이그레이션 일정이 실패할 때 발생합니다.
조회 생성 이벤트("조회 생성")
이 이벤트는 조회를 만들 때 발생합니다.
조회 업데이트 이벤트("조회 업데이트")
이 이벤트는 조회가 업데이트될 때 발생합니다.
관련 조회 생성 이벤트("related-referral-created")
이 이벤트는 관련 조회를 만들 때 발생합니다.
관련 조회 업데이트 이벤트("related-referral-updated")
이 이벤트는 관련 조회가 업데이트될 때 발생합니다.
구독 활성 이벤트("subscription-active")
이 이벤트는 구독이 활성화될 때 발생합니다.
참고 항목
구독 활성 웹후크 및 해당 활동 로그 이벤트는 현재 샌드박스 테넌트에만 사용할 수 있습니다.
구독 보류 이벤트("구독 보류 중")
이 이벤트는 해당 주문이 성공적으로 수신되고 구독 만들기가 보류 중인 경우에 발생합니다.
참고 항목
구독 보류 중인 웹후크 및 해당 활동 로그 이벤트는 현재 샌드박스 테넌트에만 사용할 수 있습니다.
구독 갱신 이벤트("구독 갱신")
이 이벤트는 구독이 갱신을 완료할 때 발생합니다.
참고 항목
구독 갱신된 웹후크 및 해당 활동 로그 이벤트는 현재 샌드박스 테넌트에만 사용할 수 있습니다.
구독 업데이트 이벤트("subscription-updated")
이 이벤트는 구독이 변경될 때 발생합니다. 이러한 이벤트는 파트너 센터 API를 통해 변경된 경우 외에 내부 변경이 있을 때 생성됩니다.
참고 항목
구독이 변경되는 시간과 구독 업데이트 이벤트가 트리거되는 시간 사이에는 최대 48시간이 지연됩니다.
테스트 이벤트("test-created")
이 이벤트를 사용하면 테스트 이벤트를 요청한 다음 진행률을 추적하여 등록을 자체 온보딩하고 테스트할 수 있습니다. 이벤트를 배달하는 동안 Microsoft에서 수신되는 오류 메시지를 볼 수 있습니다. 이 제한은 "테스트 생성" 이벤트에만 적용됩니다. 7일보다 오래된 데이터가 제거됩니다.
임계값 초과 이벤트("usagerecords-thresholdExceeded")
이 이벤트는 고객의 Microsoft Azure 사용량이 사용량 지출 예산(임계값)을 초과할 때 발생합니다. 자세한 내용은 (고객/파트너 센터/set-an-azure-spending-budget-for-your-customers에 대한 Azure 지출 예산 설정)을 참조하세요.
향후 웹후크 이벤트는 파트너가 제어하지 않는 시스템에서 변경되는 리소스에 대해 추가되며, 해당 이벤트를 가능한 한 "실시간"에 가깝게 가져오기 위해 추가 업데이트가 수행됩니다. 비즈니스에 가치를 더하는 이벤트에 대한 파트너의 피드백은 추가할 새 이벤트를 결정하는 데 유용합니다.
파트너 센터에서 지원하는 웹후크 이벤트의 전체 목록은 파트너 센터 웹후크 이벤트를 참조 하세요.
필수 조건
- 자격 증명(파트너 센터 인증에서 설명). 이 시나리오는 독립 실행형 앱과 App+사용자 자격 증명을 모두 사용하여 인증을 지원합니다.
파트너 센터에서 이벤트 수신
파트너 센터에서 이벤트를 수신하려면 공개적으로 액세스할 수 있는 엔드포인트를 노출해야 합니다. 이 엔드포인트가 노출되므로 파트너 센터에서 통신이 수행되고 있는지 확인해야 합니다. 받는 모든 웹후크 이벤트는 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"
}
참고 항목
권한 부여 헤더에는 "서명" 체계가 있습니다. 이는 콘텐츠의 base64로 인코딩된 서명입니다.
콜백을 인증하는 방법
파트너 센터에서 받은 콜백 이벤트를 인증하려면 다음 단계를 수행합니다.
- 필요한 헤더가 있는지 확인합니다(권한 부여, 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 형식)입니다. |
Sample
다음 샘플에서는 파트너 센터 이벤트의 구조를 보여줍니다.
{
"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 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" ]
}
등록 보기
테넌트에 대한 Webhooks 이벤트 등록을 반환합니다.
리소스 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" ]
}
등록 유효성을 검사하는 테스트 이벤트 보내기
웹후크 등록의 유효성을 검사하는 테스트 이벤트를 생성합니다. 이 테스트는 파트너 센터에서 이벤트를 받을 수 있는지 유효성을 검사하기 위한 것입니다. 이러한 이벤트에 대한 데이터는 초기 이벤트를 만든 후 7일 후에 삭제됩니다. 유효성 검사 이벤트를 보내기 전에 등록 API를 사용하여 "test-created" 이벤트에 등록해야 합니다.
참고 항목
유효성 검사 이벤트를 게시할 때 분당 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)
서명 유효성 검사
다음 예제에서는 웹후크 이벤트에서 콜백을 수신하는 컨트롤러에 권한 부여 특성을 추가하는 방법을 보여 줍니다.
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}."));
}
}
}
}