Skip to content

Commit

Permalink
try to use the most sensible context when processing stripe webhooks (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
cbellone authored Jul 2, 2021
1 parent 6c01b12 commit 3ccbccd
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 21 deletions.
16 changes: 13 additions & 3 deletions src/main/java/alfio/manager/TicketReservationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -2362,9 +2362,9 @@ public PaymentWebhookResult processTransactionWebhook(String body, String signat
return processTransactionWebhook(body, signature, paymentProxy, additionalInfo, new PaymentContext());
}

public PaymentWebhookResult processTransactionWebhook(String body, String signature, PaymentProxy paymentProxy, Map<String, String> additionalInfo, PaymentContext paymentContext) {
public PaymentWebhookResult processTransactionWebhook(String body, String signature, PaymentProxy paymentProxy, Map<String, String> additionalInfo, PaymentContext pc) {
//load the payment provider using given configuration
var paymentProviderOptional = paymentManager.streamActiveProvidersByProxyAndCapabilities(paymentProxy, paymentContext, List.of(WebhookHandler.class)).findFirst();
var paymentProviderOptional = paymentManager.streamActiveProvidersByProxyAndCapabilities(paymentProxy, pc, List.of(WebhookHandler.class)).findFirst();
if(paymentProviderOptional.isEmpty()) {
return PaymentWebhookResult.error("payment provider not found");
}
Expand All @@ -2374,7 +2374,17 @@ public PaymentWebhookResult processTransactionWebhook(String body, String signat
return PaymentWebhookResult.error("signature is missing");
}

var optionalTransactionWebhookPayload = ((WebhookHandler)paymentProvider).parseTransactionPayload(body, signature, additionalInfo);
PaymentContext paymentContext;
if(pc.getConfigurationLevel().isSystem()) {
// https://github.com/alfio-event/alf.io/issues/1019
// if the current PaymentContext is System, and if the provider supports it,
// we try to narrow the payment context by pre-parsing the JSON body
paymentContext = ((WebhookHandler) paymentProvider).detectPaymentContext(body).orElse(pc);
} else {
paymentContext = pc;
}

var optionalTransactionWebhookPayload = ((WebhookHandler)paymentProvider).parseTransactionPayload(body, signature, additionalInfo, paymentContext);
if(optionalTransactionWebhookPayload.isEmpty()) {
return PaymentWebhookResult.error("payload not recognized");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,17 +379,15 @@ private boolean connectOptionsPresent(Map<ConfigurationKeys, MaybeConfiguration>
return configuration.get(MOLLIE_CONNECT_CLIENT_ID).isPresent() && configuration.get(MOLLIE_CONNECT_CLIENT_SECRET).isPresent();
}

@Override
public String getWebhookSignatureKey() {
return null;
}

public static final String ADDITIONAL_INFO_PURCHASE_CONTEXT_TYPE = "purchaseContextType";
public static final String ADDITIONAL_INFO_PURCHASE_IDENTIFIER = "purchaseContextPublicIdentifier";
public static final String ADDITIONAL_INFO_RESERVATION_ID = "reservationId";

@Override
public Optional<TransactionWebhookPayload> parseTransactionPayload(String body, String signature, Map<String, String> additionalInfo) {
public Optional<TransactionWebhookPayload> parseTransactionPayload(String body,
String signature,
Map<String, String> additionalInfo,
PaymentContext paymentContext) {
try(var reader = new StringReader(body)) {
Properties properties = new Properties();
properties.load(reader);
Expand Down
10 changes: 4 additions & 6 deletions src/main/java/alfio/manager/payment/SaferpayManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,10 @@ private String authorizationHeader(Map<ConfigurationKeys, ConfigurationManager.M
}

@Override
public String getWebhookSignatureKey() {
return null;
}

@Override
public Optional<TransactionWebhookPayload> parseTransactionPayload(String body, String signature, Map<String, String> additionalInfo) {
public Optional<TransactionWebhookPayload> parseTransactionPayload(String body,
String signature,
Map<String, String> additionalInfo,
PaymentContext paymentContext) {
return Optional.of(new EmptyWebhookPayload(additionalInfo.get("reservationId"), TransactionWebhookPayload.Status.SUCCESS));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import alfio.manager.support.PaymentResult;
import alfio.manager.support.PaymentWebhookResult;
import alfio.manager.system.ConfigurationLevel;
import alfio.manager.system.ConfigurationManager;
import alfio.model.Audit;
import alfio.model.PaymentInformation;
Expand All @@ -32,6 +33,7 @@
import alfio.repository.*;
import alfio.repository.system.ConfigurationRepository;
import alfio.util.ClockProvider;
import com.google.gson.JsonParser;
import com.stripe.Stripe;
import com.stripe.exception.StripeException;
import com.stripe.model.BalanceTransaction;
Expand All @@ -45,6 +47,7 @@
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.io.StringReader;
import java.time.ZonedDateTime;
import java.util.*;

Expand Down Expand Up @@ -207,14 +210,17 @@ private StripeSCACreditCardToken createNewToken(PaymentSpecification paymentSpec
}

@Override
public String getWebhookSignatureKey() {
return configurationManager.getForSystem(STRIPE_WEBHOOK_PAYMENT_KEY).getRequiredValue();
public String getWebhookSignatureKey(ConfigurationLevel configurationLevel) {
return configurationManager.getFor(STRIPE_WEBHOOK_PAYMENT_KEY, configurationLevel).getRequiredValue();
}

@Override
public Optional<TransactionWebhookPayload> parseTransactionPayload(String body, String signature, Map<String, String> additionalInfo) {
public Optional<TransactionWebhookPayload> parseTransactionPayload(String body,
String signature,
Map<String, String> additionalInfo,
PaymentContext paymentContext) {
try {
var stripeEvent = Webhook.constructEvent(body, signature, getWebhookSignatureKey());
var stripeEvent = Webhook.constructEvent(body, signature, getWebhookSignatureKey(paymentContext.getConfigurationLevel()));
String eventType = stripeEvent.getType();
if(eventType.startsWith("charge.")) {
return deserializeObject(stripeEvent).map(obj -> new StripeChargeTransactionWebhookPayload(eventType, (Charge)obj));
Expand Down Expand Up @@ -429,4 +435,29 @@ public PaymentWebhookResult forceTransactionCheck(TicketReservation reservation,
return PaymentWebhookResult.error("failed");
}
}

/**
* Detects {@link PaymentContext} by parsing Stripe's Webhook payload.
* If anything goes wrong, it returns an empty PaymentContext
* @param payload Stripe Webhook payload
* @return PaymentContext
*/
@Override
public Optional<PaymentContext> detectPaymentContext(String payload) {
try (var stringReader = new StringReader(payload)) {
var reservationId = JsonParser.parseReader(stringReader)
.getAsJsonObject()
.getAsJsonObject("data")
.getAsJsonObject("object")
.getAsJsonObject("metadata")
.get("reservationId")
.getAsString();

var event = eventRepository.findByReservationId(reservationId);
return Optional.of(new PaymentContext(event, reservationId));
} catch(Exception ex) {
log.warn("Cannot detect PaymentContext from the webhook body. Using a generic one", ex);
return Optional.empty();
}
}
}
4 changes: 4 additions & 0 deletions src/main/java/alfio/manager/system/ConfigurationLevel.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ default OptionalInt getTicketCategoryId() {
return OptionalInt.empty();
}

default boolean isSystem() {
return getPathLevel() == ConfigurationPathLevel.SYSTEM;
}

static ConfigurationLevel external() {
return new ConfigurationLevels.ExternalLevel();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package alfio.model.transaction.capabilities;

import alfio.manager.support.PaymentWebhookResult;
import alfio.manager.system.ConfigurationLevel;
import alfio.model.TicketReservation;
import alfio.model.transaction.Capability;
import alfio.model.transaction.PaymentContext;
Expand All @@ -28,9 +29,11 @@

public interface WebhookHandler extends Capability {

String getWebhookSignatureKey();
default String getWebhookSignatureKey(ConfigurationLevel configurationLevel) {
return null;
}

Optional<TransactionWebhookPayload> parseTransactionPayload(String body, String signature, Map<String, String> additionalInfo);
Optional<TransactionWebhookPayload> parseTransactionPayload(String body, String signature, Map<String, String> additionalInfo, PaymentContext paymentContext);

PaymentWebhookResult processWebhook(TransactionWebhookPayload payload, Transaction transaction, PaymentContext paymentContext);

Expand All @@ -40,4 +43,8 @@ default boolean requiresSignedBody() {
return true;
}

default Optional<PaymentContext> detectPaymentContext(String payload) {
return Optional.empty();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import org.junit.jupiter.api.Test;
import org.springframework.core.env.Environment;

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneId;
import java.util.*;

Expand Down Expand Up @@ -261,4 +263,33 @@ void stripeConfigurationCompletePlatformModeOn() {
assertTrue(stripeWebhookPaymentManager.accept(PaymentMethod.CREDIT_CARD, new PaymentContext(null, configurationLevel), TransactionRequest.empty()));
}

@Test
void detectPaymentContextValidJson() throws Exception {
var resource = getClass().getResource("/transaction-json/stripe-success-valid.json");
assertNotNull(resource);
var payload = Files.readString(Path.of(resource.toURI()));
when(eventRepository.findByReservationId(eq(RESERVATION_ID))).thenReturn(event);
var paymentContextOptional = stripeWebhookPaymentManager.detectPaymentContext(payload);
assertTrue(paymentContextOptional.isPresent());
var paymentContext = paymentContextOptional.get();
assertSame(event, paymentContext.getPurchaseContext());
}

@Test
void detectPaymentContextMetadataMissing() throws Exception {
var resource = getClass().getResource("/transaction-json/stripe-success-metadata-missing.json");
assertNotNull(resource);
var payload = Files.readString(Path.of(resource.toURI()));
when(eventRepository.findByReservationId(eq(RESERVATION_ID))).thenReturn(event);
var paymentContextOptional = stripeWebhookPaymentManager.detectPaymentContext(payload);
assertTrue(paymentContextOptional.isEmpty());
}

@Test
void detectPaymentContextJsonInvalid() {
when(eventRepository.findByReservationId(eq(RESERVATION_ID))).thenReturn(event);
var paymentContextOptional = stripeWebhookPaymentManager.detectPaymentContext("invalid json");
assertTrue(paymentContextOptional.isEmpty());
}

}
Loading

0 comments on commit 3ccbccd

Please sign in to comment.