• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stripe / stripe-dotnet / 11519002614

25 Oct 2024 01:18PM UTC coverage: 46.558% (-0.05%) from 46.606%
11519002614

Pull #3011

coveralls.net

web-flow
Merge e47f89dd2 into 2c30f9d61
Pull Request #3011: Do not allow setting API Version directly on StripeConfiguration

5194 of 11156 relevant lines covered (46.56%)

96.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.78
/src/Stripe.net/Services/Events/EventUtility.cs
1
namespace Stripe
2
{
3
    using System;
4
    using System.Collections.Generic;
5
    using System.Linq;
6
    using System.Security.Cryptography;
7
    using System.Text;
8
    using Stripe.Infrastructure;
9

10
    /// <summary>
11
    /// This class contains utility methods to process event objects in Stripe's webhooks.
12
    /// </summary>
13
    public static class EventUtility
14
    {
15
        internal static readonly UTF8Encoding SafeUTF8
1✔
16
            = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
1✔
17

18
        public const int DefaultTimeTolerance = 300;
19

20
        public static bool IsCompatibleApiVersion(string eventApiVersion)
21
        {
4✔
22
            // If the event api version is from before we started adding
23
            // a release train, there's no way its compatible with this
24
            // version
25
            if (!eventApiVersion.Contains("."))
4✔
26
            {
1✔
27
                return false;
1✔
28
            }
29

30
            // versions are yyyy-MM-dd.train
31
            var eventReleaseTrain = eventApiVersion.Split('.')[1];
3✔
32
            var currentReleaseTrain = ApiVersion.Current.Split('.')[1];
3✔
33
            return eventReleaseTrain == currentReleaseTrain;
3✔
34
        }
4✔
35

36
        /// <summary>
37
        /// Parses a JSON string from a Stripe webhook into a <see cref="Event"/> object.
38
        /// </summary>
39
        /// <param name="json">The JSON string to parse.</param>
40
        /// <param name="throwOnApiVersionMismatch">
41
        /// If <c>true</c> (default), the method will throw a <see cref="StripeException"/> if the
42
        /// API version of the event doesn't match Stripe.net's default API version (see
43
        /// <see cref="StripeConfiguration.ApiVersion"/>).
44
        /// </param>
45
        /// <returns>The deserialized <see cref="Event"/>.</returns>
46
        /// <exception cref="StripeException">
47
        /// Thrown if the API version of the event doesn't match Stripe.net's default API version.
48
        /// </exception>
49
        /// <remarks>
50
        /// This method doesn't verify <a href="https://stripe.com/docs/webhooks/signatures">webhook
51
        /// signatures</a>. It's recommended that you use
52
        /// <see cref="ConstructEvent(string, string, string, long, bool)"/> instead.
53
        /// </remarks>
54
        public static Event ParseEvent(string json, bool throwOnApiVersionMismatch = true)
55
        {
7✔
56
            var stripeEvent = JsonUtils.DeserializeObject<Event>(
7✔
57
                json,
7✔
58
                StripeConfiguration.SerializerSettings);
7✔
59

60
            if (throwOnApiVersionMismatch &&
7✔
61
                !IsCompatibleApiVersion(stripeEvent.ApiVersion))
7✔
62
            {
2✔
63
                throw new StripeException(
2✔
64
                    $"Received event with API version {stripeEvent.ApiVersion}, but Stripe.net "
2✔
65
                    + $"{StripeConfiguration.StripeNetVersion} expects API version "
2✔
66
                    + $"{ApiVersion.Current}. We recommend that you create a "
2✔
67
                    + "WebhookEndpoint with this API version. Otherwise, you can disable this "
2✔
68
                    + "exception by passing `throwOnApiVersionMismatch: false` to "
2✔
69
                    + "`Stripe.EventUtility.ParseEvent` or `Stripe.EventUtility.ConstructEvent`, "
2✔
70
                    + "but be wary that objects may be incorrectly deserialized.");
2✔
71
            }
72

73
            return stripeEvent;
5✔
74
        }
5✔
75

76
        /// <summary>
77
        /// Parses a JSON string from a Stripe webhook into a <see cref="Event"/> object, while
78
        /// verifying the <a href="https://stripe.com/docs/webhooks/signatures">webhook's
79
        /// signature</a>.
80
        /// </summary>
81
        /// <param name="json">The JSON string to parse.</param>
82
        /// <param name="stripeSignatureHeader">
83
        /// The value of the <c>Stripe-Signature</c> header from the webhook request.
84
        /// </param>
85
        /// <param name="secret">The webhook endpoint's signing secret.</param>
86
        /// <param name="tolerance">The time tolerance, in seconds (default 300).</param>
87
        /// <param name="throwOnApiVersionMismatch">
88
        /// If <c>true</c> (default), the method will throw a <see cref="StripeException"/> if the
89
        /// API version of the event doesn't match Stripe.net's default API version (see
90
        /// <see cref="StripeConfiguration.ApiVersion"/>).
91
        /// </param>
92
        /// <returns>The deserialized <see cref="Event"/>.</returns>
93
        /// <exception cref="StripeException">
94
        /// Thrown if the signature verification fails for any reason, of if the API version of the
95
        /// event doesn't match Stripe.net's default API version.
96
        /// </exception>
97
        public static Event ConstructEvent(
98
            string json,
99
            string stripeSignatureHeader,
100
            string secret,
101
            long tolerance = DefaultTimeTolerance,
102
            bool throwOnApiVersionMismatch = true)
103
        {
4✔
104
            return ConstructEvent(
4✔
105
                json,
4✔
106
                stripeSignatureHeader,
4✔
107
                secret,
4✔
108
                tolerance,
4✔
109
                DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
4✔
110
                throwOnApiVersionMismatch);
4✔
111
        }
1✔
112

113
        /// <summary>
114
        /// Parses a JSON string from a Stripe webhook into a <see cref="Event"/> object, while
115
        /// verifying the <a href="https://stripe.com/docs/webhooks/signatures">webhook's
116
        /// signature</a>.
117
        /// </summary>
118
        /// <param name="json">The JSON string to parse.</param>
119
        /// <param name="stripeSignatureHeader">
120
        /// The value of the <c>Stripe-Signature</c> header from the webhook request.
121
        /// </param>
122
        /// <param name="secret">The webhook endpoint's signing secret.</param>
123
        /// <param name="tolerance">The time tolerance, in seconds.</param>
124
        /// <param name="utcNow">The timestamp to use for the current time.</param>
125
        /// <param name="throwOnApiVersionMismatch">
126
        /// If <c>true</c> (default), the method will throw a <see cref="StripeException"/> if the
127
        /// API version of the event doesn't match Stripe.net's default API version (see
128
        /// <see cref="StripeConfiguration.ApiVersion"/>).
129
        /// </param>
130
        /// <returns>The deserialized <see cref="Event"/>.</returns>
131
        /// <exception cref="StripeException">
132
        /// Thrown if the signature verification fails for any reason, of if the API version of the
133
        /// event doesn't match Stripe.net's default API version.
134
        /// </exception>
135
        public static Event ConstructEvent(
136
            string json,
137
            string stripeSignatureHeader,
138
            string secret,
139
            long tolerance,
140
            long utcNow,
141
            bool throwOnApiVersionMismatch = true)
142
        {
6✔
143
            ValidateSignature(json, stripeSignatureHeader, secret, tolerance, utcNow);
6✔
144
            return ParseEvent(json, throwOnApiVersionMismatch);
2✔
145
        }
2✔
146

147
        public static void ValidateSignature(string json, string stripeSignatureHeader, string secret, long tolerance = DefaultTimeTolerance)
148
        {
7✔
149
            ValidateSignature(json, stripeSignatureHeader, secret, tolerance, DateTimeOffset.UtcNow.ToUnixTimeSeconds());
7✔
150
        }
×
151

152
        public static void ValidateSignature(string json, string stripeSignatureHeader, string secret, long tolerance, long utcNow)
153
        {
28✔
154
            var signatureItems = ParseStripeSignature(stripeSignatureHeader);
28✔
155
            var signature = string.Empty;
21✔
156

157
            try
158
            {
21✔
159
                signature = ComputeSignature(secret, signatureItems["t"].FirstOrDefault(), json);
21✔
160
            }
19✔
161
            catch (EncoderFallbackException ex)
2✔
162
            {
2✔
163
                throw new StripeException(
2✔
164
                    "The webhook cannot be processed because the signature cannot be safely calculated.",
2✔
165
                    ex);
2✔
166
            }
167

168
            if (!IsSignaturePresent(signature, signatureItems["v1"]))
19✔
169
            {
2✔
170
                throw new StripeException(
2✔
171
                    "The expected signature was not found in the Stripe-Signature header. " +
2✔
172
                    "Make sure you're using the correct webhook secret (whsec_) and confirm the incoming request came from Stripe.");
2✔
173
            }
174

175
            var webhookUtc = Convert.ToInt32(signatureItems["t"].FirstOrDefault());
17✔
176

177
            if (Math.Abs(utcNow - webhookUtc) > tolerance)
17✔
178
            {
1✔
179
                throw new StripeException(
1✔
180
                    "The webhook cannot be processed because the current timestamp is outside of the allowed tolerance.");
1✔
181
            }
182
        }
16✔
183

184
        private static ILookup<string, string> ParseStripeSignature(string stripeSignatureHeader)
185
        {
28✔
186
            (string Key, string Value) ParseItem(string item)
187
            {
188
                string[] parts = item.Trim().Split(new[] { '=' }, 2);
189
                if (parts.Length != 2)
190
                {
191
                    throw new StripeException(
192
                        "The signature header format is unexpected.");
193
                }
194

195
                return (parts[0], parts[1]);
196
            }
197

198
            return stripeSignatureHeader.Trim()
28✔
199
                .Split(',')
28✔
200
                .Select(item => ParseItem(item))
28✔
201
                .ToLookup(item => item.Key, item => item.Value);
28✔
202
        }
21✔
203

204
        private static bool IsSignaturePresent(string signature, IEnumerable<string> signatures)
205
        {
19✔
206
            return signatures.Any(key => StringUtils.SecureEquals(key, signature));
19✔
207
        }
19✔
208

209
        private static string ComputeSignature(string secret, string timestamp, string payload)
210
        {
21✔
211
            var secretBytes = SafeUTF8.GetBytes(secret);
21✔
212
            var payloadBytes = SafeUTF8.GetBytes($"{timestamp}.{payload}");
20✔
213

214
            using (var cryptographer = new HMACSHA256(secretBytes))
19✔
215
            {
19✔
216
                var hash = cryptographer.ComputeHash(payloadBytes);
19✔
217
                return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
19✔
218
            }
219
        }
19✔
220
    }
221
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc