다음을 통해 공유


파트너 센터 웹후크

적용 대상: 파트너 센터 | 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로 인코딩된 서명입니다.

콜백을 인증하는 방법

파트너 센터에서 받은 콜백 이벤트를 인증하려면 다음 단계를 수행합니다.

  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입니다.
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}."));
            }
        }
    }
}