-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
StripeController.cs
180 lines (155 loc) · 6.26 KB
/
StripeController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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);
}
}