mirror of
https://github.com/bitwarden/server.git
synced 2025-04-11 08:08:14 -05:00
[AC-1571] Handle null reference exception in stripe controller (#3147)
* Added null checks when getting customer metadata * Added additional logging around paypal payments * Refactor region validation in StripeController * Update region retrieval method in StripeController Refactored the method GetCustomerRegionFromMetadata in StripeController. Previously, it returned null in case of nonexisting region key. Now, it checks all keys with case-insensitive comparison, and if no "region" key is found, it defaults to "US". This was done to handle cases where the region key might not be properly formatted or missing. * Updated switch expression to be switch statement * Updated new log to not log user input
This commit is contained in:
parent
8de9f4d10d
commit
fae4d3ca1b
@ -10,12 +10,18 @@ using Bit.Core.Tools.Enums;
|
|||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Braintree;
|
||||||
|
using Braintree.Exceptions;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
|
using Customer = Stripe.Customer;
|
||||||
using Event = Stripe.Event;
|
using Event = Stripe.Event;
|
||||||
|
using Subscription = Stripe.Subscription;
|
||||||
using TaxRate = Bit.Core.Entities.TaxRate;
|
using TaxRate = Bit.Core.Entities.TaxRate;
|
||||||
|
using Transaction = Bit.Core.Entities.Transaction;
|
||||||
|
using TransactionType = Bit.Core.Enums.TransactionType;
|
||||||
|
|
||||||
namespace Bit.Billing.Controllers;
|
namespace Bit.Billing.Controllers;
|
||||||
|
|
||||||
@ -490,60 +496,87 @@ public class StripeController : Controller
|
|||||||
/// <exception cref="Exception"></exception>
|
/// <exception cref="Exception"></exception>
|
||||||
private async Task<bool> ValidateCloudRegionAsync(Event parsedEvent)
|
private async Task<bool> ValidateCloudRegionAsync(Event parsedEvent)
|
||||||
{
|
{
|
||||||
string customerRegion;
|
|
||||||
|
|
||||||
var serverRegion = _globalSettings.BaseServiceUri.CloudRegion;
|
var serverRegion = _globalSettings.BaseServiceUri.CloudRegion;
|
||||||
var eventType = parsedEvent.Type;
|
var eventType = parsedEvent.Type;
|
||||||
|
var expandOptions = new List<string> { "customer" };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, string> customerMetadata;
|
||||||
switch (eventType)
|
switch (eventType)
|
||||||
{
|
{
|
||||||
case HandledStripeWebhook.SubscriptionDeleted:
|
case HandledStripeWebhook.SubscriptionDeleted:
|
||||||
case HandledStripeWebhook.SubscriptionUpdated:
|
case HandledStripeWebhook.SubscriptionUpdated:
|
||||||
{
|
customerMetadata = (await GetSubscriptionAsync(parsedEvent, true, expandOptions))?.Customer
|
||||||
var subscription = await GetSubscriptionAsync(parsedEvent, true, new List<string> { "customer" });
|
?.Metadata;
|
||||||
customerRegion = GetCustomerRegionFromMetadata(subscription.Customer.Metadata);
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case HandledStripeWebhook.ChargeSucceeded:
|
case HandledStripeWebhook.ChargeSucceeded:
|
||||||
case HandledStripeWebhook.ChargeRefunded:
|
case HandledStripeWebhook.ChargeRefunded:
|
||||||
{
|
customerMetadata = (await GetChargeAsync(parsedEvent, true, expandOptions))?.Customer?.Metadata;
|
||||||
var charge = await GetChargeAsync(parsedEvent, true, new List<string> { "customer" });
|
|
||||||
customerRegion = GetCustomerRegionFromMetadata(charge.Customer.Metadata);
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case HandledStripeWebhook.UpcomingInvoice:
|
case HandledStripeWebhook.UpcomingInvoice:
|
||||||
var eventInvoice = await GetInvoiceAsync(parsedEvent);
|
customerMetadata = (await GetInvoiceAsync(parsedEvent))?.Customer?.Metadata;
|
||||||
var customer = await GetCustomerAsync(eventInvoice.CustomerId);
|
|
||||||
customerRegion = GetCustomerRegionFromMetadata(customer.Metadata);
|
|
||||||
break;
|
break;
|
||||||
case HandledStripeWebhook.PaymentSucceeded:
|
case HandledStripeWebhook.PaymentSucceeded:
|
||||||
case HandledStripeWebhook.PaymentFailed:
|
case HandledStripeWebhook.PaymentFailed:
|
||||||
case HandledStripeWebhook.InvoiceCreated:
|
case HandledStripeWebhook.InvoiceCreated:
|
||||||
{
|
customerMetadata = (await GetInvoiceAsync(parsedEvent, true, expandOptions))?.Customer?.Metadata;
|
||||||
var invoice = await GetInvoiceAsync(parsedEvent, true, new List<string> { "customer" });
|
break;
|
||||||
customerRegion = GetCustomerRegionFromMetadata(invoice.Customer.Metadata);
|
default:
|
||||||
|
customerMetadata = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
|
if (customerMetadata is null)
|
||||||
{
|
{
|
||||||
// For all Stripe events that we're not listening to, just return 200
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
var customerRegion = GetCustomerRegionFromMetadata(customerMetadata);
|
||||||
|
|
||||||
return customerRegion == serverRegion;
|
return customerRegion == serverRegion;
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Encountered unexpected error while validating cloud region");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the region from the customer metadata. If no region is present, defaults to "US"
|
/// Gets the customer's region from the metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="customerMetadata"></param>
|
/// <param name="customerMetadata">The metadata of the customer.</param>
|
||||||
/// <returns></returns>
|
/// <returns>The region of the customer. If the region is not specified, it returns "US", if metadata is null,
|
||||||
private static string GetCustomerRegionFromMetadata(Dictionary<string, string> customerMetadata)
|
/// it returns null. It is case insensitive.</returns>
|
||||||
|
private static string GetCustomerRegionFromMetadata(IDictionary<string, string> customerMetadata)
|
||||||
{
|
{
|
||||||
return customerMetadata.TryGetValue("region", out var value)
|
const string defaultRegion = "US";
|
||||||
? value
|
|
||||||
: "US";
|
if (customerMetadata is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customerMetadata.TryGetValue("region", out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var miscasedRegionKey = customerMetadata.Keys
|
||||||
|
.FirstOrDefault(key =>
|
||||||
|
key.Equals("region", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (miscasedRegionKey is null)
|
||||||
|
{
|
||||||
|
return defaultRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = customerMetadata.TryGetValue(miscasedRegionKey, out var regionValue);
|
||||||
|
|
||||||
|
return !string.IsNullOrWhiteSpace(regionValue)
|
||||||
|
? regionValue
|
||||||
|
: defaultRegion;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tuple<Guid?, Guid?> GetIdsFromMetaData(IDictionary<string, string> metaData)
|
private Tuple<Guid?, Guid?> GetIdsFromMetaData(IDictionary<string, string> metaData)
|
||||||
@ -708,8 +741,11 @@ public class StripeController : Controller
|
|||||||
|
|
||||||
private async Task<bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer)
|
private async Task<bool> AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice, Customer customer)
|
||||||
{
|
{
|
||||||
|
_logger.LogDebug("Attempting to pay invoice with Braintree");
|
||||||
if (!customer?.Metadata?.ContainsKey("btCustomerId") ?? true)
|
if (!customer?.Metadata?.ContainsKey("btCustomerId") ?? true)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Attempted to pay invoice with Braintree but btCustomerId wasn't on Stripe customer metadata");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,6 +754,8 @@ public class StripeController : Controller
|
|||||||
var ids = GetIdsFromMetaData(subscription?.Metadata);
|
var ids = GetIdsFromMetaData(subscription?.Metadata);
|
||||||
if (!ids.Item1.HasValue && !ids.Item2.HasValue)
|
if (!ids.Item1.HasValue && !ids.Item2.HasValue)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Attempted to pay invoice with Braintree but Stripe subscription metadata didn't contain either a organizationId or userId");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -740,7 +778,10 @@ public class StripeController : Controller
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var transactionResult = await _btGateway.Transaction.SaleAsync(
|
Result<Braintree.Transaction> transactionResult;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
transactionResult = await _btGateway.Transaction.SaleAsync(
|
||||||
new Braintree.TransactionRequest
|
new Braintree.TransactionRequest
|
||||||
{
|
{
|
||||||
Amount = btInvoiceAmount,
|
Amount = btInvoiceAmount,
|
||||||
@ -750,7 +791,8 @@ public class StripeController : Controller
|
|||||||
SubmitForSettlement = true,
|
SubmitForSettlement = true,
|
||||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||||
{
|
{
|
||||||
CustomField = $"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
|
CustomField =
|
||||||
|
$"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
CustomFields = new Dictionary<string, string>
|
CustomFields = new Dictionary<string, string>
|
||||||
@ -759,6 +801,13 @@ public class StripeController : Controller
|
|||||||
["region"] = _globalSettings.BaseServiceUri.CloudRegion
|
["region"] = _globalSettings.BaseServiceUri.CloudRegion
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
catch (NotFoundException e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e,
|
||||||
|
"Attempted to make a payment with Braintree, but customer did not exist for the given btCustomerId present on the Stripe metadata");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
if (!transactionResult.IsSuccess())
|
if (!transactionResult.IsSuccess())
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user