You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
6.3 KiB
180 lines
6.3 KiB
using Bit.Billing.Models; |
|
using Bit.Billing.Services; |
|
using Bit.Core.Utilities; |
|
using Microsoft.AspNetCore.Mvc; |
|
using Microsoft.Extensions.Options; |
|
using Stripe; |
|
using Event = Stripe.Event; |
|
using JsonSerializer = System.Text.Json.JsonSerializer; |
|
|
|
namespace Bit.Billing.Controllers; |
|
|
|
[Route("stripe")] |
|
public class StripeController : Controller |
|
{ |
|
private readonly BillingSettings _billingSettings; |
|
private readonly IWebHostEnvironment _hostingEnvironment; |
|
private readonly ILogger<StripeController> _logger; |
|
private readonly IStripeEventService _stripeEventService; |
|
private readonly IStripeEventProcessor _stripeEventProcessor; |
|
|
|
public StripeController( |
|
IOptions<BillingSettings> billingSettings, |
|
IWebHostEnvironment hostingEnvironment, |
|
ILogger<StripeController> logger, |
|
IStripeEventService stripeEventService, |
|
IStripeEventProcessor stripeEventProcessor) |
|
{ |
|
_billingSettings = billingSettings?.Value; |
|
_hostingEnvironment = hostingEnvironment; |
|
_logger = logger; |
|
_stripeEventService = stripeEventService; |
|
_stripeEventProcessor = stripeEventProcessor; |
|
} |
|
|
|
[HttpPost("webhook")] |
|
public async Task<IActionResult> PostWebhook([FromQuery] string key) |
|
{ |
|
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.StripeWebhookKey)) |
|
{ |
|
_logger.LogError("Stripe webhook key does not match configured webhook key"); |
|
return new BadRequestResult(); |
|
} |
|
|
|
var parsedEvent = await TryParseEventFromRequestBodyAsync(); |
|
if (parsedEvent is null) |
|
{ |
|
return Ok(new |
|
{ |
|
Processed = false, |
|
Message = "Could not find a configured webhook secret to process this event with" |
|
}); |
|
} |
|
|
|
if (StripeConfiguration.ApiVersion != parsedEvent.ApiVersion) |
|
{ |
|
_logger.LogWarning( |
|
"Stripe {WebhookType} webhook's API version ({WebhookAPIVersion}) does not match SDK API Version ({SDKAPIVersion})", |
|
parsedEvent.Type, |
|
parsedEvent.ApiVersion, |
|
StripeConfiguration.ApiVersion); |
|
|
|
return Ok(new |
|
{ |
|
Processed = false, |
|
Message = "SDK API version does not match the event's API version" |
|
}); |
|
} |
|
|
|
if (string.IsNullOrWhiteSpace(parsedEvent?.Id)) |
|
{ |
|
_logger.LogWarning("No event id."); |
|
return new BadRequestResult(); |
|
} |
|
|
|
if (_hostingEnvironment.IsProduction() && !parsedEvent.Livemode) |
|
{ |
|
_logger.LogWarning("Getting test events in production."); |
|
return new BadRequestResult(); |
|
} |
|
|
|
// If the customer and server cloud regions don't match, early return 200 to avoid unnecessary errors |
|
if (!await _stripeEventService.ValidateCloudRegion(parsedEvent)) |
|
{ |
|
return Ok(new |
|
{ |
|
Processed = false, |
|
Message = "Event is not for this cloud region" |
|
}); |
|
} |
|
|
|
await _stripeEventProcessor.ProcessEventAsync(parsedEvent); |
|
return Ok(new |
|
{ |
|
Processed = true, |
|
Message = "Processed" |
|
}); |
|
} |
|
|
|
/// <summary> |
|
/// Selects the appropriate Stripe webhook secret based on the API version specified in the webhook body. |
|
/// </summary> |
|
/// <param name="webhookBody">The body of the webhook request received from Stripe.</param> |
|
/// <returns> |
|
/// The Stripe webhook secret corresponding to the API version found in the webhook body. |
|
/// Returns null if the API version is unrecognized. |
|
/// </returns> |
|
private string PickStripeWebhookSecret(string webhookBody) |
|
{ |
|
var deliveryContainer = JsonSerializer.Deserialize<StripeWebhookDeliveryContainer>(webhookBody); |
|
|
|
_logger.LogInformation( |
|
"Picking secret for Stripe webhook | {EventID}: {EventType} | Version: {APIVersion} | Initiating Request ID: {RequestID}", |
|
deliveryContainer.Id, |
|
deliveryContainer.Type, |
|
deliveryContainer.ApiVersion, |
|
deliveryContainer.Request?.Id); |
|
|
|
return deliveryContainer.ApiVersion switch |
|
{ |
|
"2024-06-20" => HandleVersionWith(_billingSettings.StripeWebhookSecret20240620), |
|
"2023-10-16" => HandleVersionWith(_billingSettings.StripeWebhookSecret20231016), |
|
"2022-08-01" => HandleVersionWith(_billingSettings.StripeWebhookSecret), |
|
_ => HandleDefault(deliveryContainer.ApiVersion) |
|
}; |
|
|
|
string HandleVersionWith(string secret) |
|
{ |
|
if (string.IsNullOrEmpty(secret)) |
|
{ |
|
_logger.LogError("No webhook secret is configured for API version {APIVersion}", deliveryContainer.ApiVersion); |
|
return null; |
|
} |
|
|
|
if (!secret.StartsWith("whsec_")) |
|
{ |
|
_logger.LogError("Webhook secret configured for API version {APIVersion} does not start with whsec_", |
|
deliveryContainer.ApiVersion); |
|
return null; |
|
} |
|
|
|
var truncatedSecret = secret[..10]; |
|
|
|
_logger.LogInformation("Picked webhook secret {TruncatedSecret}... for API version {APIVersion}", truncatedSecret, deliveryContainer.ApiVersion); |
|
|
|
return secret; |
|
} |
|
|
|
string HandleDefault(string version) |
|
{ |
|
_logger.LogWarning( |
|
"Stripe webhook contained an API version ({APIVersion}) we do not process", |
|
version); |
|
|
|
return null; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Attempts to pick the Stripe webhook secret from the JSON payload. |
|
/// </summary> |
|
/// <returns>Returns the event if the event was parsed, otherwise, null</returns> |
|
private async Task<Event> TryParseEventFromRequestBodyAsync() |
|
{ |
|
using var sr = new StreamReader(HttpContext.Request.Body); |
|
|
|
var json = await sr.ReadToEndAsync(); |
|
var webhookSecret = PickStripeWebhookSecret(json); |
|
|
|
if (string.IsNullOrEmpty(webhookSecret)) |
|
{ |
|
return null; |
|
} |
|
|
|
return EventUtility.ConstructEvent( |
|
json, |
|
Request.Headers["Stripe-Signature"], |
|
webhookSecret, |
|
throwOnApiVersionMismatch: false); |
|
} |
|
}
|
|
|