2024-06-26 16:34:16 +01:00
|
|
|
|
using Bit.Billing.Models;
|
2023-10-11 15:57:51 -04:00
|
|
|
|
using Bit.Billing.Services;
|
2019-08-09 23:56:26 -04:00
|
|
|
|
using Bit.Core.Utilities;
|
2017-04-26 16:14:15 -04:00
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2017-04-26 14:29:25 -04:00
|
|
|
|
using Microsoft.Extensions.Options;
|
2017-04-26 16:14:15 -04:00
|
|
|
|
using Stripe;
|
2023-07-20 17:00:40 -04:00
|
|
|
|
using Event = Stripe.Event;
|
2024-01-31 08:19:29 -05:00
|
|
|
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
2017-03-18 18:52:44 -04:00
|
|
|
|
|
|
|
|
|
|
namespace Bit.Billing.Controllers;
|
2022-08-29 16:06:55 -04:00
|
|
|
|
|
2017-03-18 18:52:44 -04:00
|
|
|
|
[Route("stripe")]
|
|
|
|
|
|
public class StripeController : Controller
|
|
|
|
|
|
{
|
2017-04-26 14:29:25 -04:00
|
|
|
|
private readonly BillingSettings _billingSettings;
|
2017-03-18 18:52:44 -04:00
|
|
|
|
private readonly IWebHostEnvironment _hostingEnvironment;
|
|
|
|
|
|
private readonly ILogger<StripeController> _logger;
|
2023-10-11 15:57:51 -04:00
|
|
|
|
private readonly IStripeEventService _stripeEventService;
|
2024-06-26 16:34:16 +01:00
|
|
|
|
private readonly IStripeEventProcessor _stripeEventProcessor;
|
2022-08-29 16:06:55 -04:00
|
|
|
|
|
2017-03-18 18:52:44 -04:00
|
|
|
|
public StripeController(
|
2019-02-03 00:00:21 -05:00
|
|
|
|
IOptions<BillingSettings> billingSettings,
|
|
|
|
|
|
IWebHostEnvironment hostingEnvironment,
|
|
|
|
|
|
ILogger<StripeController> logger,
|
2023-10-23 13:46:29 -04:00
|
|
|
|
IStripeEventService stripeEventService,
|
2024-06-26 16:34:16 +01:00
|
|
|
|
IStripeEventProcessor stripeEventProcessor)
|
2017-03-18 18:52:44 -04:00
|
|
|
|
{
|
2017-04-26 14:29:25 -04:00
|
|
|
|
_billingSettings = billingSettings?.Value;
|
2020-01-10 08:47:58 -05:00
|
|
|
|
_hostingEnvironment = hostingEnvironment;
|
2019-02-03 00:00:21 -05:00
|
|
|
|
_logger = logger;
|
2023-10-11 15:57:51 -04:00
|
|
|
|
_stripeEventService = stripeEventService;
|
2024-06-26 16:34:16 +01:00
|
|
|
|
_stripeEventProcessor = stripeEventProcessor;
|
2017-08-12 22:30:44 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2017-03-18 18:52:44 -04:00
|
|
|
|
[HttpPost("webhook")]
|
2020-03-27 14:36:37 -04:00
|
|
|
|
public async Task<IActionResult> PostWebhook([FromQuery] string key)
|
2022-08-29 16:06:55 -04:00
|
|
|
|
{
|
2019-02-22 09:31:05 -05:00
|
|
|
|
if (!CoreHelpers.FixedTimeEquals(key, _billingSettings.StripeWebhookKey))
|
2017-04-26 16:14:15 -04:00
|
|
|
|
{
|
2024-09-06 13:30:39 -04:00
|
|
|
|
_logger.LogError("Stripe webhook key does not match configured webhook key");
|
2017-04-26 16:14:15 -04:00
|
|
|
|
return new BadRequestResult();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-04-19 09:15:48 -04:00
|
|
|
|
var parsedEvent = await TryParseEventFromRequestBodyAsync();
|
|
|
|
|
|
if (parsedEvent is null)
|
2022-08-29 15:53:48 -04:00
|
|
|
|
{
|
2024-09-06 13:30:39 -04:00
|
|
|
|
return Ok(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Processed = false,
|
|
|
|
|
|
Message = "Could not find a configured webhook secret to process this event with"
|
|
|
|
|
|
});
|
2024-01-31 08:19:29 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2024-09-06 13:30:39 -04:00
|
|
|
|
return Ok(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Processed = false,
|
|
|
|
|
|
Message = "SDK API version does not match the event's API version"
|
|
|
|
|
|
});
|
2017-04-26 16:14:15 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2022-05-31 10:55:56 -04:00
|
|
|
|
if (string.IsNullOrWhiteSpace(parsedEvent?.Id))
|
2022-08-29 15:53:48 -04:00
|
|
|
|
{
|
2022-05-31 10:55:56 -04:00
|
|
|
|
_logger.LogWarning("No event id.");
|
|
|
|
|
|
return new BadRequestResult();
|
2022-08-29 15:53:48 -04:00
|
|
|
|
}
|
2018-04-05 21:56:36 -04:00
|
|
|
|
|
2019-08-20 11:09:02 -04:00
|
|
|
|
if (_hostingEnvironment.IsProduction() && !parsedEvent.Livemode)
|
2022-08-29 15:53:48 -04:00
|
|
|
|
{
|
2017-08-12 22:16:42 -04:00
|
|
|
|
_logger.LogWarning("Getting test events in production.");
|
|
|
|
|
|
return new BadRequestResult();
|
2022-08-29 15:53:48 -04:00
|
|
|
|
}
|
2017-08-12 22:16:42 -04:00
|
|
|
|
|
2023-07-20 17:00:40 -04:00
|
|
|
|
// If the customer and server cloud regions don't match, early return 200 to avoid unnecessary errors
|
2023-10-11 15:57:51 -04:00
|
|
|
|
if (!await _stripeEventService.ValidateCloudRegion(parsedEvent))
|
2023-07-20 17:00:40 -04:00
|
|
|
|
{
|
2024-09-06 13:30:39 -04:00
|
|
|
|
return Ok(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Processed = false,
|
|
|
|
|
|
Message = "Event is not for this cloud region"
|
|
|
|
|
|
});
|
2023-07-20 17:00:40 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-06-26 16:34:16 +01:00
|
|
|
|
await _stripeEventProcessor.ProcessEventAsync(parsedEvent);
|
2024-09-06 13:30:39 -04:00
|
|
|
|
return Ok(new
|
|
|
|
|
|
{
|
|
|
|
|
|
Processed = true,
|
|
|
|
|
|
Message = "Processed"
|
|
|
|
|
|
});
|
2024-04-19 09:15:48 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2024-06-26 16:34:16 +01:00
|
|
|
|
/// Selects the appropriate Stripe webhook secret based on the API version specified in the webhook body.
|
2024-04-19 09:15:48 -04:00
|
|
|
|
/// </summary>
|
2024-06-26 16:34:16 +01:00
|
|
|
|
/// <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>
|
2024-01-31 08:19:29 -05:00
|
|
|
|
private string PickStripeWebhookSecret(string webhookBody)
|
|
|
|
|
|
{
|
2024-09-06 13:30:39 -04:00
|
|
|
|
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);
|
2024-01-31 08:19:29 -05:00
|
|
|
|
|
2024-09-06 13:30:39 -04:00
|
|
|
|
return deliveryContainer.ApiVersion switch
|
2024-01-31 08:19:29 -05:00
|
|
|
|
{
|
2024-09-06 13:30:39 -04:00
|
|
|
|
"2024-06-20" => HandleVersionWith(_billingSettings.StripeWebhookSecret20240620),
|
|
|
|
|
|
"2023-10-16" => HandleVersionWith(_billingSettings.StripeWebhookSecret20231016),
|
|
|
|
|
|
"2022-08-01" => HandleVersionWith(_billingSettings.StripeWebhookSecret),
|
|
|
|
|
|
_ => HandleDefault(deliveryContainer.ApiVersion)
|
2024-01-31 08:19:29 -05:00
|
|
|
|
};
|
|
|
|
|
|
|
2024-09-06 13:30:39 -04:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-01-31 08:19:29 -05:00
|
|
|
|
string HandleDefault(string version)
|
|
|
|
|
|
{
|
|
|
|
|
|
_logger.LogWarning(
|
2024-09-06 13:30:39 -04:00
|
|
|
|
"Stripe webhook contained an API version ({APIVersion}) we do not process",
|
2024-01-31 08:19:29 -05:00
|
|
|
|
version);
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2024-04-19 09:15:48 -04:00
|
|
|
|
|
|
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-09-06 13:30:39 -04:00
|
|
|
|
return EventUtility.ConstructEvent(
|
2024-04-19 09:15:48 -04:00
|
|
|
|
json,
|
|
|
|
|
|
Request.Headers["Stripe-Signature"],
|
|
|
|
|
|
webhookSecret,
|
|
|
|
|
|
throwOnApiVersionMismatch: false);
|
|
|
|
|
|
}
|
2017-03-18 18:52:44 -04:00
|
|
|
|
}
|