@@ -375,12 +375,12 @@ private static long ParsePayPalAmount(string? value)
375375 #region Webhooks
376376
377377 /// <inheritdoc />
378- public async Task < WebhookResult > ProcessWebhookAsync ( string payload , string ? signature )
378+ public async Task < WebhookResult > ProcessWebhookAsync ( string payload , string ? signature , IDictionary < string , string > ? headers = null )
379379 {
380380 EnsureEnabled ( ) ;
381381
382- // Verify webhook signature
383- var isValid = await VerifyWebhookSignatureAsync ( payload , signature ) ;
382+ // Verify webhook signature using PayPal's verification API
383+ var isValid = await VerifyWebhookSignatureAsync ( payload , signature , headers ) ;
384384 if ( ! isValid )
385385 {
386386 _logger . LogWarning ( "PayPal webhook signature verification failed" ) ;
@@ -490,18 +490,82 @@ private WebhookResult HandlePaymentCompleted(PayPalWebhookEvent webhookEvent)
490490 } ;
491491 }
492492
493- private async Task < bool > VerifyWebhookSignatureAsync ( string payload , string ? signature )
493+ private async Task < bool > VerifyWebhookSignatureAsync ( string payload , string ? signature , IDictionary < string , string > ? headers )
494494 {
495495 if ( string . IsNullOrEmpty ( PayPalConfig ? . WebhookId ) )
496496 {
497497 _logger . LogWarning ( "PayPal webhook ID not configured, skipping signature verification" ) ;
498- return true ; // Allow in development
498+ return true ; // Allow in development only
499499 }
500500
501- // PayPal webhook verification requires multiple headers
502- // For simplicity, we'll trust the webhook if the webhook ID is configured
503- // In production, implement full signature verification
504- return true ;
501+ if ( headers == null || string . IsNullOrEmpty ( signature ) )
502+ {
503+ _logger . LogWarning ( "PayPal webhook missing required headers for verification" ) ;
504+ return false ;
505+ }
506+
507+ // Extract required headers for PayPal verification
508+ headers . TryGetValue ( "PAYPAL-AUTH-ALGO" , out var authAlgo ) ;
509+ headers . TryGetValue ( "PAYPAL-CERT-URL" , out var certUrl ) ;
510+ headers . TryGetValue ( "PAYPAL-TRANSMISSION-ID" , out var transmissionId ) ;
511+ headers . TryGetValue ( "PAYPAL-TRANSMISSION-TIME" , out var transmissionTime ) ;
512+
513+ if ( string . IsNullOrEmpty ( authAlgo ) || string . IsNullOrEmpty ( certUrl ) ||
514+ string . IsNullOrEmpty ( transmissionId ) || string . IsNullOrEmpty ( transmissionTime ) )
515+ {
516+ _logger . LogWarning ( "PayPal webhook missing required verification headers" ) ;
517+ return false ;
518+ }
519+
520+ try
521+ {
522+ var accessToken = await GetAccessTokenAsync ( ) ;
523+
524+ // Build the verification request per PayPal's API
525+ var verificationRequest = new PayPalWebhookVerificationRequest
526+ {
527+ AuthAlgo = authAlgo ,
528+ CertUrl = certUrl ,
529+ TransmissionId = transmissionId ,
530+ TransmissionSig = signature ,
531+ TransmissionTime = transmissionTime ,
532+ WebhookId = PayPalConfig . WebhookId ,
533+ WebhookEvent = JsonSerializer . Deserialize < JsonElement > ( payload )
534+ } ;
535+
536+ var request = new HttpRequestMessage ( HttpMethod . Post ,
537+ $ "{ PayPalConfig . ApiBaseUrl } /v1/notifications/verify-webhook-signature") ;
538+ request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , accessToken ) ;
539+ request . Content = new StringContent (
540+ JsonSerializer . Serialize ( verificationRequest , JsonOptions ) ,
541+ Encoding . UTF8 , "application/json" ) ;
542+
543+ var response = await _httpClient . SendAsync ( request ) ;
544+ var responseBody = await response . Content . ReadAsStringAsync ( ) ;
545+
546+ if ( ! response . IsSuccessStatusCode )
547+ {
548+ _logger . LogWarning ( "PayPal webhook verification API failed: {StatusCode} - {Body}" ,
549+ response . StatusCode , responseBody ) ;
550+ return false ;
551+ }
552+
553+ var result = JsonSerializer . Deserialize < PayPalVerificationResponse > ( responseBody , JsonOptions ) ;
554+ var isValid = result ? . VerificationStatus == "SUCCESS" ;
555+
556+ if ( ! isValid )
557+ {
558+ _logger . LogWarning ( "PayPal webhook verification returned status: {Status}" ,
559+ result ? . VerificationStatus ?? "null" ) ;
560+ }
561+
562+ return isValid ;
563+ }
564+ catch ( Exception ex )
565+ {
566+ _logger . LogError ( ex , "Error verifying PayPal webhook signature" ) ;
567+ return false ;
568+ }
505569 }
506570
507571 #endregion
@@ -754,5 +818,36 @@ private class PayPalMoney
754818 public string ? Value { get ; set ; }
755819 }
756820
821+ // Models for webhook signature verification
822+ private class PayPalWebhookVerificationRequest
823+ {
824+ [ JsonPropertyName ( "auth_algo" ) ]
825+ public string ? AuthAlgo { get ; set ; }
826+
827+ [ JsonPropertyName ( "cert_url" ) ]
828+ public string ? CertUrl { get ; set ; }
829+
830+ [ JsonPropertyName ( "transmission_id" ) ]
831+ public string ? TransmissionId { get ; set ; }
832+
833+ [ JsonPropertyName ( "transmission_sig" ) ]
834+ public string ? TransmissionSig { get ; set ; }
835+
836+ [ JsonPropertyName ( "transmission_time" ) ]
837+ public string ? TransmissionTime { get ; set ; }
838+
839+ [ JsonPropertyName ( "webhook_id" ) ]
840+ public string ? WebhookId { get ; set ; }
841+
842+ [ JsonPropertyName ( "webhook_event" ) ]
843+ public JsonElement WebhookEvent { get ; set ; }
844+ }
845+
846+ private class PayPalVerificationResponse
847+ {
848+ [ JsonPropertyName ( "verification_status" ) ]
849+ public string ? VerificationStatus { get ; set ; }
850+ }
851+
757852 #endregion
758853}
0 commit comments