From a2290fbb46cfc7694127fa7cb5cb2bca418e7636 Mon Sep 17 00:00:00 2001
From: David Brownman <1231935+xavdid@users.noreply.github.com>
Date: Thu, 6 Aug 2026 16:07:10 -0700
Subject: [PATCH 1/2] add/adjust event parsing helpers (#2256)
* Add cloud provider event parsing methods to StripeClient
* add missing methods & tests
* PR feedback
---
src/main/java/com/stripe/StripeClient.java | 47 +++-
.../model/v2/core/EventNotification.java | 33 ++-
src/main/java/com/stripe/net/Webhook.java | 121 ++++++++-
.../stripe/net/CloudProviderEventTest.java | 236 ++++++++++++++++++
src/test/java/com/stripe/net/WebhookTest.java | 164 ++++++++----
5 files changed, 533 insertions(+), 68 deletions(-)
create mode 100644 src/test/java/com/stripe/net/CloudProviderEventTest.java
diff --git a/src/main/java/com/stripe/StripeClient.java b/src/main/java/com/stripe/StripeClient.java
index 5be81eff127..c1cb86882f9 100644
--- a/src/main/java/com/stripe/StripeClient.java
+++ b/src/main/java/com/stripe/StripeClient.java
@@ -2,6 +2,7 @@
import com.stripe.exception.SignatureVerificationException;
import com.stripe.exception.StripeException;
+import com.stripe.model.Event;
import com.stripe.model.StripeObject;
import com.stripe.model.v2.core.EventNotification;
import com.stripe.net.*;
@@ -58,9 +59,10 @@ public EventNotification parseEventNotification(String payload, String sigHeader
}
/**
- * Returns an StripeEvent instance using the provided JSON payload. Throws a JsonSyntaxException
- * if the payload is not valid JSON, and a SignatureVerificationException if the signature
- * verification fails for any reason.
+ * Constructs a thin event
+ * notification from an incoming webhook after verifying its authenticity. To work with a
+ * webhook that has already been verified (i.e. one from a cloud provider, an asynchronous queue,
+ * or during testing), see {@code parseEventNotificationWithoutVerification}.
*
* @param payload the payload sent by Stripe.
* @param sigHeader the contents of the signature header sent by Stripe.
@@ -117,6 +119,45 @@ public com.stripe.model.Event constructEvent(
return event;
}
+ /**
+ * Constructs a snapshot
+ * event from an incoming webhook without first verifying its authenticity. Should be used
+ * after calling {@code Webhook.Signature.verifyHeader(...)} or with input from a trusted source
+ * (such as AWS EventBridge,
+ * or Azure Event Grid
+ * payload). Or, to verify & construct in a single call, use {@code constructEvent(...)}
+ * instead.
+ *
+ * @param payload the JSON payload: a raw Stripe Event or an AWS EventBridge/Azure Event Grid
+ * envelope.
+ * @return the Event instance.
+ * @throws IllegalArgumentException if the payload is a thin event notification, or if the format
+ * is not recognized.
+ */
+ public com.stripe.model.Event constructEventWithoutVerification(String payload) {
+ Event event = Webhook.constructEventWithoutVerification(payload);
+ event.setResponseGetter(this.getResponseGetter());
+ return event;
+ }
+
+ /**
+ * Constructs a thin event
+ * notification from an incoming webhook without first verifying its authenticity. Should be
+ * used after calling {@code Webhook.Signature.verifyHeader(...)} or with input from a trusted
+ * source (such as AWS
+ * EventBridge, or Azure Event
+ * Grid payload). Or, to verify & parse in a single call, use {@code
+ * parseEventNotification(...)} instead.
+ *
+ * @param payload the JSON payload: a raw Stripe Event, or an AWS EventBridge/Azure Event Grid
+ * envelope.
+ * @return the EventNotification instance.
+ * @throws IllegalArgumentException if the payload format is not recognized.
+ */
+ public EventNotification parseEventNotificationWithoutVerification(String payload) {
+ return EventNotification.fromJson(Webhook.maybeExtractFromCloudProviderEnvelope(payload), this);
+ }
+
// The beginning of the section generated from our OpenAPI spec
public com.stripe.service.V1Services v1() {
return new com.stripe.service.V1Services(this.getResponseGetter());
diff --git a/src/main/java/com/stripe/model/v2/core/EventNotification.java b/src/main/java/com/stripe/model/v2/core/EventNotification.java
index 374abbb60f8..2e98698a9a5 100644
--- a/src/main/java/com/stripe/model/v2/core/EventNotification.java
+++ b/src/main/java/com/stripe/model/v2/core/EventNotification.java
@@ -86,17 +86,34 @@ public static class Reason {
/**
* Helper for constructing an Event Notification. Doesn't perform signature validation, so you
* should use {@link com.stripe.StripeClient#parseEventNotification} instead for initial handling.
- * This is useful in unit tests and working with EventNotifications that you've already validated
- * the authenticity of.
+ * This is useful in unit tests and working with EventNotifications whose authenticity you've
+ * already validated.
*/
public static EventNotification fromJson(String payload, StripeClient client) {
// don't love the double json parse here, but I don't think we can avoid it?
- JsonObject jsonObject = ApiResource.GSON.fromJson(payload, JsonObject.class).getAsJsonObject();
+ return EventNotification.fromJson(
+ ApiResource.GSON.fromJson(payload, JsonObject.class).getAsJsonObject(), client);
+ }
- if (jsonObject.has("object") && "event".equals(jsonObject.get("object").getAsString())) {
- throw new IllegalArgumentException(
- "You passed a webhook payload to StripeClient.parseEventNotification, which expects an event notification."
- + " Use StripeClient.constructEvent instead.");
+ /**
+ * Helper for constructing an Event Notification. Doesn't perform signature validation, so you
+ * should use {@link com.stripe.StripeClient#parseEventNotification} instead for initial handling.
+ * This is useful in unit tests and working with EventNotifications whose authenticity you've
+ * already validated.
+ */
+ public static EventNotification fromJson(JsonObject jsonObject, StripeClient client) {
+ if (jsonObject.has("object")) {
+ String object = jsonObject.get("object").getAsString();
+ if ("event".equals(object)) {
+ throw new IllegalArgumentException(
+ "You passed a webhook payload to a method that expects an event notification. Use the corresponding constructEvent method instead.");
+ }
+ if (!"v2.core.event".equals(object)) {
+ throw new IllegalArgumentException(
+ "Unexpected object type '"
+ + object
+ + "'. Expected 'v2.core.event' for an event notification.");
+ }
}
Class extends EventNotification> cls =
@@ -105,7 +122,7 @@ public static EventNotification fromJson(String payload, StripeClient client) {
cls = UnknownEventNotification.class;
}
- EventNotification e = ApiResource.GSON.fromJson(payload, cls);
+ EventNotification e = ApiResource.GSON.fromJson(jsonObject, cls);
e.client = client;
return e;
diff --git a/src/main/java/com/stripe/net/Webhook.java b/src/main/java/com/stripe/net/Webhook.java
index 09505f70873..e5b8b5d0681 100644
--- a/src/main/java/com/stripe/net/Webhook.java
+++ b/src/main/java/com/stripe/net/Webhook.java
@@ -1,5 +1,6 @@
package com.stripe.net;
+import com.google.gson.JsonObject;
import com.stripe.exception.SignatureVerificationException;
import com.stripe.model.Event;
import com.stripe.model.StripeObject;
@@ -53,9 +54,10 @@ public static Event constructEvent(
}
/**
- * Returns an Event instance using the provided JSON payload. Throws a JsonSyntaxException if the
- * payload is not valid JSON, a SignatureVerificationException if the signature verification fails
- * for any reason, and an IllegalArgumentException if you pass the wrong type of input.
+ * Constructs a snapshot
+ * event from an incoming webhook after verifying its authenticity. To work with a webhook
+ * that has already been verified (i.e. one from a cloud provider, an asynchronous queue, or
+ * during testing), see {@code constructEventWithoutVerification}.
*
* @param payload the payload sent by Stripe.
* @param sigHeader the contents of the signature header sent by Stripe.
@@ -69,28 +71,90 @@ public static Event constructEvent(
public static Event constructEvent(
String payload, String sigHeader, String secret, long tolerance, Clock clock)
throws SignatureVerificationException {
- Event event =
- StripeObject.deserializeStripeObject(
- payload, Event.class, ApiResource.getGlobalResponseGetter());
+ Signature.verifyHeader(payload, sigHeader, secret, tolerance, clock);
+
+ return buildV1Event(payload);
+ }
+
+ /**
+ * Constructs a snapshot
+ * event from an incoming webhook without first verifying its authenticity. Should be used
+ * after calling {@code Webhook.Signature.verifyHeader(...)} or with input from a trusted source
+ * (such as AWS EventBridge,
+ * or Azure Event Grid
+ * payload). Or, to verify & construct in a single call, use {@code
+ * Webhook.constructEvent(...)} instead.
+ *
+ * @param payload the payload sent by Stripe, or a cloud provider envelope wrapping it.
+ * @return the Event instance
+ * @throws IllegalArgumentException if the payload is a v2 thin event notification.
+ */
+ public static Event constructEventWithoutVerification(String payload) {
+ return buildV1Event(maybeExtractFromCloudProviderEnvelope(payload));
+ }
- if ("v2.core.event".equals(event.getObject())) {
+ private static Event buildV1Event(String payload) {
+ return buildV1Event(ApiResource.GSON.fromJson(payload, JsonObject.class));
+ }
+
+ private static Event buildV1Event(JsonObject jsonObject) {
+ if (jsonObject.has("object")
+ && "v2.core.event".equals(jsonObject.get("object").getAsString())) {
throw new IllegalArgumentException(
- "You passed an event notification to Webhook.constructEvent, which expects a webhook payload."
- + " Use StripeClient.parseEventNotification instead.");
+ "You passed an event notification to Webhook method, which expects a webhook payload. Use the corresponding parseEventNotification method instead.");
}
- Signature.verifyHeader(payload, sigHeader, secret, tolerance, clock);
+ Event event =
+ StripeObject.deserializeStripeObject(
+ jsonObject, Event.class, ApiResource.getGlobalResponseGetter());
+
// StripeObjects source their raw JSON object from their last response, but constructed webhooks
// don't have that
// in order to make the raw object available on parsed events, we fake the response.
if (event.getLastResponse() == null) {
event.setLastResponse(
- new StripeResponse(200, HttpHeaders.of(Collections.emptyMap()), payload));
+ new StripeResponse(200, HttpHeaders.of(Collections.emptyMap()), jsonObject.toString()));
}
return event;
}
+ /**
+ * Parses a JSON payload (or cloud provider envelope) and returns the inner Stripe event JSON
+ * object. If the payload is already a raw Stripe event (object is "event" or "v2.core.event"), it
+ * is returned as-is. If it is an AWS EventBridge or Azure Event Grid envelope, the inner event is
+ * extracted. Throws {@link IllegalArgumentException} for unrecognized formats.
+ *
+ * @param payload the raw JSON string.
+ * @return the inner event as a {@link JsonObject}.
+ */
+ public static JsonObject maybeExtractFromCloudProviderEnvelope(String payload) {
+ JsonObject root = ApiResource.GSON.fromJson(payload, JsonObject.class);
+
+ // AWS
+ // https://docs.stripe.com/event-destinations/eventbridge#event-structure
+ if (root.has("detail")) {
+ return root.get("detail").getAsJsonObject();
+ }
+
+ // Azure
+ // https://docs.stripe.com/event-destinations/eventgrid#event-structure
+ if (root.has("specversion") && root.has("data")) {
+ return root.get("data").getAsJsonObject();
+ }
+
+ // Raw Stripe event passed directly: pass through as-is
+ if (root.has("object") && root.get("object").isJsonPrimitive()) {
+ String object = root.get("object").getAsString();
+ if ("event".equals(object) || "v2.core.event".equals(object)) {
+ return root;
+ }
+ }
+
+ throw new IllegalArgumentException(
+ "Unrecognized event format. The payload must be an AWS EventBridge/Azure Event Grid event envelope or a Stripe webhook (thin event notification or snapshot).");
+ }
+
public static final class Signature {
public static final String EXPECTED_SCHEME = "v1";
@@ -112,8 +176,10 @@ public static boolean verifyHeader(
}
/**
- * Verifies the signature header sent by Stripe. Throws a SignatureVerificationException if the
- * verification fails for any reason.
+ * Verifies the authenticity (and recency) of a webhook, throwing a {@code
+ * SignatureVerificationException} if there's a mismatch. Useful for quickly validating incoming
+ * webhooks before storing them for later processing (at which time you can use the {@code
+ * *WithoutVerification} methods for parsing).
*
* @param payload the payload sent by Stripe.
* @param sigHeader the contents of the signature header sent by Stripe.
@@ -171,6 +237,35 @@ public static boolean verifyHeader(
return true;
}
+ /**
+ * Generates a {@code Stripe-Signature} header for the given payload and secret using the
+ * current timestamp.
+ *
+ * @param payload the payload to sign.
+ * @param secret the webhook secret.
+ * @return the generated signature header string.
+ */
+ public static String generateSignatureHeader(String payload, String secret)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ return generateSignatureHeader(payload, secret, Util.getTimeNow());
+ }
+
+ /**
+ * Compute the {@code Stripe-Signature} header for a given webhook body & secret. Useful for
+ * signing payloads in unit tests.
+ *
+ * @param payload the payload to sign.
+ * @param secret the webhook secret.
+ * @param timestamp the timestamp to use (seconds since epoch).
+ * @return the generated signature header string.
+ */
+ public static String generateSignatureHeader(String payload, String secret, long timestamp)
+ throws NoSuchAlgorithmException, InvalidKeyException {
+ String payloadToSign = String.format("%d.%s", timestamp, payload);
+ String signature = computeSignature(payloadToSign, secret);
+ return String.format("t=%d,%s=%s", timestamp, EXPECTED_SCHEME, signature);
+ }
+
/**
* Extracts the timestamp in a signature header.
*
diff --git a/src/test/java/com/stripe/net/CloudProviderEventTest.java b/src/test/java/com/stripe/net/CloudProviderEventTest.java
new file mode 100644
index 00000000000..11234e560df
--- /dev/null
+++ b/src/test/java/com/stripe/net/CloudProviderEventTest.java
@@ -0,0 +1,236 @@
+package com.stripe.net;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import com.stripe.BaseStripeTest;
+import com.stripe.StripeClient;
+import com.stripe.model.Event;
+import com.stripe.model.v2.core.EventNotification;
+import org.junit.jupiter.api.Test;
+
+public class CloudProviderEventTest extends BaseStripeTest {
+
+ private static final String EVENTBRIDGE_PAYLOAD =
+ "{\"version\":\"0\",\"id\":\"17e8dff5-d6cd-3770-ace9-aeac02b6ac3f\","
+ + "\"detail-type\":\"customer.created\","
+ + "\"source\":\"aws.partner/stripe.com/ed_123\","
+ + "\"account\":\"506417113029\","
+ + "\"time\":\"2024-03-07T18:27:56Z\","
+ + "\"region\":\"us-west-2\","
+ + "\"resources\":[],"
+ + "\"detail\":{"
+ + "\"id\":\"evt_test_123\","
+ + "\"object\":\"event\","
+ + "\"api_version\":\"2023-10-16\","
+ + "\"created\":1709836076,"
+ + "\"data\":{\"object\":{\"id\":\"cus_123\",\"object\":\"customer\"}},"
+ + "\"livemode\":true,"
+ + "\"pending_webhooks\":0,"
+ + "\"request\":{\"id\":\"req_123\",\"idempotency_key\":null},"
+ + "\"type\":\"customer.created\"}}";
+
+ private static final String EVENTGRID_PAYLOAD =
+ "{\"specversion\":\"1.0\","
+ + "\"type\":\"customer.created\","
+ + "\"source\":\"/providers/stripe/ed_test_123\","
+ + "\"id\":\"9aeb0fdf-c01e-0131-0922-9eb54906e209\","
+ + "\"time\":\"2025-07-11T14:30:00Z\","
+ + "\"subject\":null,"
+ + "\"dataContentType\":\"application/cloudevents+json\","
+ + "\"data\":{"
+ + "\"id\":\"evt_test_456\","
+ + "\"object\":\"event\","
+ + "\"api_version\":\"2023-10-16\","
+ + "\"created\":1709836076,"
+ + "\"data\":{\"object\":{\"id\":\"cus_456\",\"object\":\"customer\"}},"
+ + "\"livemode\":false,"
+ + "\"pending_webhooks\":0,"
+ + "\"request\":{\"id\":\"req_456\",\"idempotency_key\":null},"
+ + "\"type\":\"customer.created\"}}";
+
+ private static final String EVENTBRIDGE_NOTIFICATION_PAYLOAD =
+ "{\"version\":\"0\",\"id\":\"17e8dff5-d6cd-3770-ace9-aeac02b6ac3f\","
+ + "\"detail-type\":\"v2.core.event_destination.ping\","
+ + "\"source\":\"aws.partner/stripe.com/ed_123\","
+ + "\"detail\":{"
+ + "\"id\":\"evt_test_789\","
+ + "\"object\":\"v2.core.event\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"created\":\"2024-03-07T18:27:56.000Z\","
+ + "\"livemode\":true}}";
+
+ private static final String EVENTGRID_NOTIFICATION_PAYLOAD =
+ "{\"specversion\":\"1.0\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"source\":\"/providers/stripe/ed_test_123\","
+ + "\"id\":\"9aeb0fdf-c01e-0131-0922-9eb54906e209\","
+ + "\"data\":{"
+ + "\"id\":\"evt_test_789\","
+ + "\"object\":\"v2.core.event\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"created\":\"2024-03-07T18:27:56.000Z\","
+ + "\"livemode\":true}}";
+
+ private static final String RAW_EVENT_PAYLOAD =
+ "{\"id\":\"evt_test_123\","
+ + "\"object\":\"event\","
+ + "\"api_version\":\"2023-10-16\","
+ + "\"created\":1709836076,"
+ + "\"data\":{\"object\":{\"id\":\"cus_123\",\"object\":\"customer\"}},"
+ + "\"livemode\":true,"
+ + "\"pending_webhooks\":0,"
+ + "\"request\":{\"id\":\"req_123\",\"idempotency_key\":null},"
+ + "\"type\":\"customer.created\"}";
+
+ // constructEventWithoutVerification tests
+
+ @Test
+ public void testEventBridgeViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ Event event = client.constructEventWithoutVerification(EVENTBRIDGE_PAYLOAD);
+ assertNotNull(event);
+ assertEquals("evt_test_123", event.getId());
+ assertEquals("customer.created", event.getType());
+ }
+
+ @Test
+ public void testEventGridViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ Event event = client.constructEventWithoutVerification(EVENTGRID_PAYLOAD);
+ assertNotNull(event);
+ assertEquals("evt_test_456", event.getId());
+ assertEquals("customer.created", event.getType());
+ }
+
+ @Test
+ public void testRawEventViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ Event event = client.constructEventWithoutVerification(RAW_EVENT_PAYLOAD);
+ assertNotNull(event);
+ assertEquals("evt_test_123", event.getId());
+ assertEquals("customer.created", event.getType());
+ }
+
+ @Test
+ public void testInvalidJsonViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ assertThrows(
+ JsonSyntaxException.class,
+ () -> client.constructEventWithoutVerification("not valid json"));
+ }
+
+ @Test
+ public void testUnrecognizedFormatViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.constructEventWithoutVerification("{\"foo\":\"bar\"}"));
+ assertTrue(ex.getMessage().contains("Unrecognized event format"));
+ }
+
+ // parseEventNotificationWithoutVerification tests
+
+ @Test
+ public void testEventBridgeNotificationViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ EventNotification notification =
+ client.parseEventNotificationWithoutVerification(EVENTBRIDGE_NOTIFICATION_PAYLOAD);
+ assertNotNull(notification);
+ assertEquals("evt_test_789", notification.getId());
+ }
+
+ @Test
+ public void testEventGridNotificationViaClient() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ EventNotification notification =
+ client.parseEventNotificationWithoutVerification(EVENTGRID_NOTIFICATION_PAYLOAD);
+ assertNotNull(notification);
+ assertEquals("evt_test_789", notification.getId());
+ }
+
+ @Test
+ public void testParseNotificationWithV1EventSuggestsConstructEventWithoutVerification() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.parseEventNotificationWithoutVerification(EVENTBRIDGE_PAYLOAD));
+ }
+
+ @Test
+ public void testParseNotificationInvalidJsonThrows() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ assertThrows(
+ JsonSyntaxException.class,
+ () -> client.parseEventNotificationWithoutVerification("not valid json"));
+ }
+
+ @Test
+ public void testParseNotificationUnrecognizedFormatThrows() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.parseEventNotificationWithoutVerification("{\"foo\":\"bar\"}"));
+ assertTrue(ex.getMessage().contains("Unrecognized event format"));
+ }
+
+ @Test
+ public void testParseNotificationRawV2NotificationPassthrough() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ String rawV2Payload =
+ "{\"id\":\"evt_234\","
+ + "\"object\":\"v2.core.event\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"created\":\"2024-03-07T18:27:56.000Z\","
+ + "\"livemode\":true}";
+ EventNotification notification = client.parseEventNotificationWithoutVerification(rawV2Payload);
+ assertNotNull(notification);
+ assertEquals("evt_234", notification.getId());
+ }
+
+ @Test
+ public void testConstructEventWithoutVerificationRejectsV2ThinEvent() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ String v2Payload =
+ "{\"id\":\"evt_234\","
+ + "\"object\":\"v2.core.event\","
+ + "\"type\":\"v2.core.event_destination.ping\","
+ + "\"created\":\"2024-03-07T18:27:56.000Z\","
+ + "\"livemode\":true}";
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.constructEventWithoutVerification(v2Payload));
+ assertTrue(ex.getMessage().contains("parseEventNotification"));
+ }
+
+ @Test
+ public void testAzureEnvelopeMissingDataThrows() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ String payloadMissingData =
+ "{\"specversion\":\"1.0\","
+ + "\"type\":\"customer.created\","
+ + "\"source\":\"/providers/stripe/ed_test_123\","
+ + "\"id\":\"test-missing-data\"}";
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> client.constructEventWithoutVerification(payloadMissingData));
+ assertTrue(ex.getMessage().contains("Unrecognized event format"));
+ }
+
+ @Test
+ public void testFromJsonUnexpectedObjectTypeThrows() {
+ StripeClient client = new StripeClient("sk_test_fake");
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("object", "customer");
+ jsonObject.addProperty("type", "customer.created");
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class, () -> EventNotification.fromJson(jsonObject, client));
+ assertTrue(ex.getMessage().contains("Unexpected object type"));
+ }
+}
diff --git a/src/test/java/com/stripe/net/WebhookTest.java b/src/test/java/com/stripe/net/WebhookTest.java
index 3e1357b6bf3..cf7a7c7d9c7 100644
--- a/src/test/java/com/stripe/net/WebhookTest.java
+++ b/src/test/java/com/stripe/net/WebhookTest.java
@@ -5,6 +5,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import com.stripe.BaseStripeTest;
import com.stripe.Stripe;
@@ -76,7 +77,7 @@ public static String generateSigHeader(Map options)
@Test
public void testValidJsonAndHeader()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final String sigHeader = generateSigHeader();
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
final Event event = Webhook.constructEvent(payload, sigHeader, secret);
@@ -86,10 +87,7 @@ public void testValidJsonAndHeader()
@Test
public void testValidJsonAndHeaderButOutsideTimeTolerance()
throws NoSuchAlgorithmException, InvalidKeyException {
- final Map options = new HashMap<>();
- options.put("timestamp", 1L);
-
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret, 1L);
final Clock clock = Clock.fixed(Instant.ofEpochMilli(12000), ZoneId.of("UTC"));
assertThrows(
@@ -109,9 +107,7 @@ public void testValidJsonAndHeaderCanMakeRequestsOnDataObject()
+ "\","
+ "\"object\": \"event\",\"data\": {\"object\": {\"id\": \"acct_123\",\"object\": \"account\"}}}";
- final Map options = new HashMap<>();
- options.put("payload", payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
final Event event = Webhook.constructEvent(payload, sigHeader, secret);
Account modelViaData = ((Account) event.getData().getObject());
@@ -125,9 +121,7 @@ public void testValidJsonAndHeaderCanMakeRequestsOnDataObject()
public void testInvalidJson()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
final String payload = "this is not valid JSON";
- final Map options = new HashMap<>();
- options.put("payload", payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
assertThrows(
JsonSyntaxException.class,
@@ -196,9 +190,8 @@ public void testNoValidSignatureForPayload()
@Test
public void testTimestampOutsideTolerance()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final Map options = new HashMap<>();
- options.put("timestamp", Webhook.Util.getTimeNow() - 15);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader =
+ Webhook.Signature.generateSignatureHeader(payload, secret, Webhook.Util.getTimeNow() - 15);
Throwable exception =
assertThrows(
@@ -212,7 +205,7 @@ public void testTimestampOutsideTolerance()
@Test
public void testValidHeaderAndSignature()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final String sigHeader = generateSigHeader();
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 10, null));
}
@@ -220,7 +213,9 @@ public void testValidHeaderAndSignature()
@Test
public void testHeaderContainsValidSignature()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final String sigHeader = String.format("%s,v1=bad_signature", generateSigHeader());
+ final String sigHeader =
+ String.format(
+ "%s,v1=bad_signature", Webhook.Signature.generateSignatureHeader(payload, secret));
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 10, null));
}
@@ -228,9 +223,7 @@ public void testHeaderContainsValidSignature()
@Test
public void testTimestampOffButNoTolerance()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
- final Map options = new HashMap<>();
- options.put("timestamp", Long.valueOf(12345L));
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret, 12345L);
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 0, null));
}
@@ -238,11 +231,7 @@ public void testTimestampOffButNoTolerance()
@Test
public void testTimestampWithClock()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
-
- final Map options = new HashMap<>();
- options.put("timestamp", 11L);
-
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret, 11L);
final Clock clock = Clock.fixed(Instant.ofEpochMilli(1), ZoneId.of("UTC"));
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 10, clock));
@@ -251,11 +240,7 @@ public void testTimestampWithClock()
@Test
public void testTimestampWithClockOutsideTolerance()
throws SignatureVerificationException, NoSuchAlgorithmException, InvalidKeyException {
-
- final Map options = new HashMap<>();
- options.put("timestamp", 11L);
-
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret, 11L);
final Clock clock = Clock.fixed(Instant.ofEpochMilli(12), ZoneId.of("UTC"));
assertTrue(Webhook.Signature.verifyHeader(payload, sigHeader, secret, 10, clock));
@@ -282,9 +267,7 @@ public void testStripeClientConstructEvent()
+ "\","
+ "\"object\": \"event\",\"data\": {\"object\": {\"id\": \"rdr_123\",\"object\": \"terminal.reader\"}}}";
- final Map options = new HashMap<>();
- options.put("payload", payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
final Event event = client.constructEvent(payload, sigHeader, secret);
@@ -315,9 +298,7 @@ public void testStripeClientConstructEventWithTolerance()
+ "\","
+ "\"object\": \"event\",\"data\": {\"object\": {\"id\": \"rdr_123\",\"object\": \"terminal.reader\"}}}";
- final Map options = new HashMap<>();
- options.put("payload", payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(payload, secret);
final Event event = client.constructEvent(payload, sigHeader, secret, 500);
@@ -331,19 +312,116 @@ public void testStripeClientConstructEventWithTolerance()
public void testConstructEventWithRawJson()
throws StripeException, NoSuchAlgorithmException, InvalidKeyException {
- final Event event = Webhook.constructEvent(payload, generateSigHeader(), secret);
+ final Event event =
+ Webhook.constructEvent(
+ payload, Webhook.Signature.generateSignatureHeader(payload, secret), secret);
assertNotNull(event.getRawJsonObject());
}
+ @Test
+ public void testGenerateSignatureHeaderWithTimestamp()
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
+ final long timestamp = 1609459200L;
+ final String header = Webhook.Signature.generateSignatureHeader(payload, secret, timestamp);
+
+ assertTrue(Webhook.Signature.verifyHeader(payload, header, secret, 0, null));
+ assertTrue(header.startsWith(String.format("t=%d,v1=", timestamp)));
+ }
+
+ @Test
+ public void testGenerateSignatureHeaderWithCurrentTimestamp()
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
+ final String header = Webhook.Signature.generateSignatureHeader(payload, secret);
+
+ assertTrue(Webhook.Signature.verifyHeader(payload, header, secret, 10, null));
+ }
+
+ @Test
+ public void testGenerateSignatureHeaderRoundtrip()
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
+ String payload = "test_payload";
+ String secret = "whsec_test_secret";
+ String header = Webhook.Signature.generateSignatureHeader(payload, secret);
+ // Should not throw:
+ assertTrue(Webhook.Signature.verifyHeader(payload, header, secret, 300));
+ }
+
+ @Test
+ public void testParseEventNotification()
+ throws NoSuchAlgorithmException, InvalidKeyException, SignatureVerificationException {
+ StripeClient client = new StripeClient(new LiveStripeResponseGetter());
+
+ final String v2Payload =
+ "{\n"
+ + " \"id\": \"evt_test_webhook\",\n"
+ + " \"object\": \"v2.core.event\",\n"
+ + " \"type\": \"v1.billing.meter.no_meter_found\",\n"
+ + " \"livemode\": false,\n"
+ + " \"created\": \"2022-02-15T00:27:45.330Z\"\n"
+ + "}";
+
+ final String header = Webhook.Signature.generateSignatureHeader(v2Payload, secret);
+
+ final com.stripe.model.v2.core.EventNotification notification =
+ client.parseEventNotification(v2Payload, header, secret);
+
+ assertNotNull(notification);
+ assertEquals("evt_test_webhook", notification.getId());
+ }
+
+ @Test
+ public void testMaybeExtractPassesThroughV1Event() {
+ final String v1Payload = "{\"id\": \"evt_test_webhook\", \"object\": \"event\"}";
+ final JsonObject result = Webhook.maybeExtractFromCloudProviderEnvelope(v1Payload);
+ assertEquals("event", result.get("object").getAsString());
+ }
+
+ @Test
+ public void testMaybeExtractPassesThroughV2Event() {
+ final String v2Payload = "{\"id\": \"evt_test_webhook\", \"object\": \"v2.core.event\"}";
+ final JsonObject result = Webhook.maybeExtractFromCloudProviderEnvelope(v2Payload);
+ assertEquals("v2.core.event", result.get("object").getAsString());
+ }
+
+ @Test
+ public void testMaybeExtractThrowsForUnrecognizedFormat() {
+ final String unknownPayload = "{\"id\": \"evt_test_webhook\", \"object\": \"something_else\"}";
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ Webhook.maybeExtractFromCloudProviderEnvelope(unknownPayload);
+ });
+ }
+
+ @Test
+ public void testConstructEventWithoutVerification() {
+ final Event event = Webhook.constructEventWithoutVerification(payload);
+
+ assertNotNull(event);
+ assertEquals("evt_test_webhook", event.getId());
+ }
+
+ @Test
+ public void testConstructEventWithoutVerificationRejectsV2Payload() {
+ final String v2Payload =
+ "{\n \"id\": \"evt_test_webhook\",\n \"object\": \"v2.core.event\"\n}";
+
+ Throwable exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ Webhook.constructEventWithoutVerification(v2Payload);
+ });
+ assertTrue(exception.getMessage().contains("parseEventNotification"));
+ }
+
@Test
public void testConstructEventRejectsV2Payload()
throws NoSuchAlgorithmException, InvalidKeyException {
final String v2Payload =
"{\n \"id\": \"evt_test_webhook\",\n \"object\": \"v2.core.event\"\n}";
- final Map options = new HashMap<>();
- options.put("payload", v2Payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(v2Payload, secret);
Throwable exception =
assertThrows(
@@ -351,7 +429,7 @@ public void testConstructEventRejectsV2Payload()
() -> {
Webhook.constructEvent(v2Payload, sigHeader, secret);
});
- assertTrue(exception.getMessage().contains("StripeClient.parseEventNotification"));
+ assertTrue(exception.getMessage().contains("parseEventNotification"));
}
@Test
@@ -361,9 +439,7 @@ public void testClientConstructEventRejectsV2Payload()
final String v2Payload =
"{\n \"id\": \"evt_test_webhook\",\n \"object\": \"v2.core.event\"\n}";
- final Map options = new HashMap<>();
- options.put("payload", v2Payload);
- final String sigHeader = generateSigHeader(options);
+ final String sigHeader = Webhook.Signature.generateSignatureHeader(v2Payload, secret);
Throwable exception =
assertThrows(
@@ -371,6 +447,6 @@ public void testClientConstructEventRejectsV2Payload()
() -> {
client.constructEvent(v2Payload, sigHeader, secret);
});
- assertTrue(exception.getMessage().contains("StripeClient.parseEventNotification"));
+ assertTrue(exception.getMessage().contains("parseEventNotification"));
}
}
From 7bc55f1ea43d86d67b5d8081c84f57a315e5314b Mon Sep 17 00:00:00 2001
From: David Brownman <1231935+xavdid@users.noreply.github.com>
Date: Fri, 7 Aug 2026 12:23:50 -0700
Subject: [PATCH 2/2] add "object" to event notification (#2265)
---
src/main/java/com/stripe/model/v2/core/EventNotification.java | 4 ++++
src/test/java/com/stripe/StripeClientTest.java | 1 +
2 files changed, 5 insertions(+)
diff --git a/src/main/java/com/stripe/model/v2/core/EventNotification.java b/src/main/java/com/stripe/model/v2/core/EventNotification.java
index 2e98698a9a5..0544219a18a 100644
--- a/src/main/java/com/stripe/model/v2/core/EventNotification.java
+++ b/src/main/java/com/stripe/model/v2/core/EventNotification.java
@@ -60,6 +60,10 @@ public static class Reason {
@SerializedName("id")
public String id;
+ /** String representing the object's type. Objects of the same type share the same value. */
+ @SerializedName("object")
+ public String object;
+
/** The type of the event. */
@SerializedName("type")
public String type;
diff --git a/src/test/java/com/stripe/StripeClientTest.java b/src/test/java/com/stripe/StripeClientTest.java
index aa97216e5a4..d53110efe61 100644
--- a/src/test/java/com/stripe/StripeClientTest.java
+++ b/src/test/java/com/stripe/StripeClientTest.java
@@ -246,6 +246,7 @@ public void parsesEventNotificationWithRelatedObject()
client.parseEventNotification(v2EventNotificationWithRelatedObject, signature, secret);
assertNotNull(eventNotification);
assertEquals("evt_234", eventNotification.getId());
+ assertEquals("v2.core.event", eventNotification.getObject());
assertEquals("v1.billing.meter.error_report_triggered", eventNotification.getType());
assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), eventNotification.created);
assertEquals("org_123", eventNotification.getContext().toString());