Files
server/src/Billing/Controllers/StripeController.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

181 lines
6.3 KiB
C#
Raw Normal View History

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;
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
{
private readonly BillingSettings _billingSettings;
2017-03-18 18:52:44 -04:00
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly ILogger<StripeController> _logger;
private readonly IStripeEventService _stripeEventService;
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,
IStripeEventService stripeEventService,
IStripeEventProcessor stripeEventProcessor)
2017-03-18 18:52:44 -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;
_stripeEventService = stripeEventService;
_stripeEventProcessor = stripeEventProcessor;
}
2017-03-18 18:52:44 -04:00
[HttpPost("webhook")]
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))
{
2024-09-06 13:30:39 -04:00
_logger.LogError("Stripe webhook key does not match configured webhook key");
return new BadRequestResult();
}
[AC-2427] update discount logic for complimentary password manager (#3990) * Refactored the charge succeeded handler a bit * If refund charge is received, and we don't have a parent transaction stored already, attempt to create one * Converted else if structure to switch-case * Moved logic for invoice.upcoming to a private method * Moved logic for charge.succeeded to a private method * Moved logic for charge.refunded to a private method * Moved logic for invoice.payment_succeeded to a private method * Updated invoice.payment_failed to match the rest * Updated invoice.created to match the rest with some light refactors * Added method comment to HandlePaymentMethodAttachedAsync * Moved logic for customer.updated to a private method * Updated logger in default case * Separated customer.subscription.deleted and customer.subscription.updated to be in their own blocks * Moved logic for customer.subscription.deleted to a private method * Moved logic for customer.subscription.updated to a private method * Merged customer sub updated or deleted to switch * No longer checking if the user has premium before disabling it since the service already checks * Moved webhook secret parsing logic to private method * Moved casting of event to specific object down to handler * Reduced nesting throughout * When removing secrets manager, now deleting 100% off password manager discount for SM trials * Added method comment and reduced nesting in RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync
2024-04-19 09:15:48 -04:00
var parsedEvent = await TryParseEventFromRequestBodyAsync();
if (parsedEvent is null)
{
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"
});
}
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"
});
}
if (string.IsNullOrWhiteSpace(parsedEvent?.Id))
{
_logger.LogWarning("No event id.");
return new BadRequestResult();
}
if (_hostingEnvironment.IsProduction() && !parsedEvent.Livemode)
{
2017-08-12 22:16:42 -04:00
_logger.LogWarning("Getting test events in production.");
return new BadRequestResult();
}
2017-08-12 22:16:42 -04:00
// If the customer and server cloud regions don't match, early return 200 to avoid unnecessary errors
if (!await _stripeEventService.ValidateCloudRegion(parsedEvent))
{
2024-09-06 13:30:39 -04:00
return Ok(new
{
Processed = false,
Message = "Event is not for this cloud region"
});
}
await _stripeEventProcessor.ProcessEventAsync(parsedEvent);
2024-09-06 13:30:39 -04:00
return Ok(new
{
Processed = true,
Message = "Processed"
});
[AC-2427] update discount logic for complimentary password manager (#3990) * Refactored the charge succeeded handler a bit * If refund charge is received, and we don't have a parent transaction stored already, attempt to create one * Converted else if structure to switch-case * Moved logic for invoice.upcoming to a private method * Moved logic for charge.succeeded to a private method * Moved logic for charge.refunded to a private method * Moved logic for invoice.payment_succeeded to a private method * Updated invoice.payment_failed to match the rest * Updated invoice.created to match the rest with some light refactors * Added method comment to HandlePaymentMethodAttachedAsync * Moved logic for customer.updated to a private method * Updated logger in default case * Separated customer.subscription.deleted and customer.subscription.updated to be in their own blocks * Moved logic for customer.subscription.deleted to a private method * Moved logic for customer.subscription.updated to a private method * Merged customer sub updated or deleted to switch * No longer checking if the user has premium before disabling it since the service already checks * Moved webhook secret parsing logic to private method * Moved casting of event to specific object down to handler * Reduced nesting throughout * When removing secrets manager, now deleting 100% off password manager discount for SM trials * Added method comment and reduced nesting in RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync
2024-04-19 09:15:48 -04:00
}
/// <summary>
/// Selects the appropriate Stripe webhook secret based on the API version specified in the webhook body.
[AC-2427] update discount logic for complimentary password manager (#3990) * Refactored the charge succeeded handler a bit * If refund charge is received, and we don't have a parent transaction stored already, attempt to create one * Converted else if structure to switch-case * Moved logic for invoice.upcoming to a private method * Moved logic for charge.succeeded to a private method * Moved logic for charge.refunded to a private method * Moved logic for invoice.payment_succeeded to a private method * Updated invoice.payment_failed to match the rest * Updated invoice.created to match the rest with some light refactors * Added method comment to HandlePaymentMethodAttachedAsync * Moved logic for customer.updated to a private method * Updated logger in default case * Separated customer.subscription.deleted and customer.subscription.updated to be in their own blocks * Moved logic for customer.subscription.deleted to a private method * Moved logic for customer.subscription.updated to a private method * Merged customer sub updated or deleted to switch * No longer checking if the user has premium before disabling it since the service already checks * Moved webhook secret parsing logic to private method * Moved casting of event to specific object down to handler * Reduced nesting throughout * When removing secrets manager, now deleting 100% off password manager discount for SM trials * Added method comment and reduced nesting in RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync
2024-04-19 09:15:48 -04:00
/// </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)
{
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-09-06 13:30:39 -04:00
return deliveryContainer.ApiVersion switch
{
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-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;
}
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",
version);
return null;
}
}
[AC-2427] update discount logic for complimentary password manager (#3990) * Refactored the charge succeeded handler a bit * If refund charge is received, and we don't have a parent transaction stored already, attempt to create one * Converted else if structure to switch-case * Moved logic for invoice.upcoming to a private method * Moved logic for charge.succeeded to a private method * Moved logic for charge.refunded to a private method * Moved logic for invoice.payment_succeeded to a private method * Updated invoice.payment_failed to match the rest * Updated invoice.created to match the rest with some light refactors * Added method comment to HandlePaymentMethodAttachedAsync * Moved logic for customer.updated to a private method * Updated logger in default case * Separated customer.subscription.deleted and customer.subscription.updated to be in their own blocks * Moved logic for customer.subscription.deleted to a private method * Moved logic for customer.subscription.updated to a private method * Merged customer sub updated or deleted to switch * No longer checking if the user has premium before disabling it since the service already checks * Moved webhook secret parsing logic to private method * Moved casting of event to specific object down to handler * Reduced nesting throughout * When removing secrets manager, now deleting 100% off password manager discount for SM trials * Added method comment and reduced nesting in RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync
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(
[AC-2427] update discount logic for complimentary password manager (#3990) * Refactored the charge succeeded handler a bit * If refund charge is received, and we don't have a parent transaction stored already, attempt to create one * Converted else if structure to switch-case * Moved logic for invoice.upcoming to a private method * Moved logic for charge.succeeded to a private method * Moved logic for charge.refunded to a private method * Moved logic for invoice.payment_succeeded to a private method * Updated invoice.payment_failed to match the rest * Updated invoice.created to match the rest with some light refactors * Added method comment to HandlePaymentMethodAttachedAsync * Moved logic for customer.updated to a private method * Updated logger in default case * Separated customer.subscription.deleted and customer.subscription.updated to be in their own blocks * Moved logic for customer.subscription.deleted to a private method * Moved logic for customer.subscription.updated to a private method * Merged customer sub updated or deleted to switch * No longer checking if the user has premium before disabling it since the service already checks * Moved webhook secret parsing logic to private method * Moved casting of event to specific object down to handler * Reduced nesting throughout * When removing secrets manager, now deleting 100% off password manager discount for SM trials * Added method comment and reduced nesting in RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync
2024-04-19 09:15:48 -04:00
json,
Request.Headers["Stripe-Signature"],
webhookSecret,
throwOnApiVersionMismatch: false);
}
2017-03-18 18:52:44 -04:00
}