From d03638a7f81dde8062fdba51126732fcca04353f Mon Sep 17 00:00:00 2001 From: Leonardo da Silva Date: Sat, 14 Dec 2024 07:01:03 -0300 Subject: [PATCH] feat: webhook validation --- .../payment/handle/HandlePaymentUseCase.java | 5 +- .../payment/handle/PaymentSuccessHandler.java | 3 +- .../domain/financial/payment/Payment.java | 18 ++++++-- .../ifsp/tickets/infra/api/BillingAPI.java | 4 +- .../api/controllers/BillingController.java | 46 ++++++++++++++++++- .../payment/persistence/PaymentJpaEntity.java | 6 +-- 6 files changed, 71 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/br/com/ifsp/tickets/app/financial/payment/handle/HandlePaymentUseCase.java b/application/src/main/java/br/com/ifsp/tickets/app/financial/payment/handle/HandlePaymentUseCase.java index 3a5248b..b10c796 100644 --- a/application/src/main/java/br/com/ifsp/tickets/app/financial/payment/handle/HandlePaymentUseCase.java +++ b/application/src/main/java/br/com/ifsp/tickets/app/financial/payment/handle/HandlePaymentUseCase.java @@ -32,7 +32,10 @@ public HandlePaymentUseCase(IPaymentGateway paymentGateway, IOrderGateway orderG public HandlePaymentOutput execute(HandlePaymentInput anIn) { Payment payment; final Optional paymentOptional = this.paymentGateway.findByExternalId(anIn.externalId()); - if (paymentOptional.isPresent()) payment = paymentOptional.get(); + if (paymentOptional.isPresent()) { + payment = paymentOptional.get(); + payment.changeStatus(anIn.status()); + } else { final OrderID orderID = OrderID.with(anIn.orderId()); final LocalDateTime createdAt = anIn.createdAt(); diff --git a/application/src/main/java/br/com/ifsp/tickets/app/financial/payment/handle/PaymentSuccessHandler.java b/application/src/main/java/br/com/ifsp/tickets/app/financial/payment/handle/PaymentSuccessHandler.java index 8289863..d7b3fe7 100644 --- a/application/src/main/java/br/com/ifsp/tickets/app/financial/payment/handle/PaymentSuccessHandler.java +++ b/application/src/main/java/br/com/ifsp/tickets/app/financial/payment/handle/PaymentSuccessHandler.java @@ -73,9 +73,8 @@ public void handle(Order order) { final boolean alreadyExists = this.enrollmentGateway.existsByEmailAndEventID(emailString, event.getId()); - if (alreadyExists) { + if (alreadyExists) Notification.create("Validation Error").append("User already enrolled in this event").throwAnyErrors(); - } final Enrollment enrollment = Enrollment .newEnrollment(name, emailString, document, birthDate, diff --git a/domain/src/main/java/br/com/ifsp/tickets/domain/financial/payment/Payment.java b/domain/src/main/java/br/com/ifsp/tickets/domain/financial/payment/Payment.java index 43ae69c..36f349b 100644 --- a/domain/src/main/java/br/com/ifsp/tickets/domain/financial/payment/Payment.java +++ b/domain/src/main/java/br/com/ifsp/tickets/domain/financial/payment/Payment.java @@ -12,14 +12,14 @@ public class Payment extends AggregateRoot { private final String externalId; - private final PaymentStatus status; + private PaymentStatus status; private final OrderID orderId; private final String currency; private final BigDecimal amount; private final String paymentType; private final LocalDateTime createdAt; - private final LocalDateTime updatedAt; - private final LocalDateTime approvalDate; + private LocalDateTime updatedAt; + private LocalDateTime approvalDate; public Payment(PaymentID id, String externalId, PaymentStatus status, OrderID orderId, String currency, BigDecimal amount, String paymentType, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime approvalDate) { super(id); @@ -34,6 +34,18 @@ public Payment(PaymentID id, String externalId, PaymentStatus status, OrderID or this.approvalDate = approvalDate; } + public void changeStatus(PaymentStatus status) { + if (this.status == status) { + return; + } + this.status = status; + this.updatedAt = LocalDateTime.now(); + if (status == PaymentStatus.APPROVED) { + this.approvalDate = LocalDateTime.now(); + } + } + + public static Payment with(PaymentID id, String externalId, PaymentStatus status, OrderID orderId, String currency, BigDecimal amount, String paymentType, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime approvalDate) { return new Payment(id, externalId, status, orderId, currency, amount, paymentType, createdAt, updatedAt, approvalDate); } diff --git a/infrastructure/src/main/java/br/com/ifsp/tickets/infra/api/BillingAPI.java b/infrastructure/src/main/java/br/com/ifsp/tickets/infra/api/BillingAPI.java index 0285712..35c1f54 100644 --- a/infrastructure/src/main/java/br/com/ifsp/tickets/infra/api/BillingAPI.java +++ b/infrastructure/src/main/java/br/com/ifsp/tickets/infra/api/BillingAPI.java @@ -7,8 +7,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @RequestMapping("/v1/billing") @@ -24,6 +26,6 @@ public interface BillingAPI { @ApiResponse(responseCode = "400", description = "Invalid request", content = @Content(schema = @Schema(implementation = APIErrorResponse.class), mediaType = "application/json")) } ) - ResponseEntity listener(PaymentListenerRequest request); + ResponseEntity listener(@RequestBody PaymentListenerRequest request, HttpHeaders headers); } diff --git a/infrastructure/src/main/java/br/com/ifsp/tickets/infra/api/controllers/BillingController.java b/infrastructure/src/main/java/br/com/ifsp/tickets/infra/api/controllers/BillingController.java index c56e0e4..95fa525 100644 --- a/infrastructure/src/main/java/br/com/ifsp/tickets/infra/api/controllers/BillingController.java +++ b/infrastructure/src/main/java/br/com/ifsp/tickets/infra/api/controllers/BillingController.java @@ -11,18 +11,34 @@ import com.mercadopago.resources.payment.Payment; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; + @RestController @RequiredArgsConstructor(onConstructor_ = @__(@Autowired)) public class BillingController implements BillingAPI { private final PaymentService paymentService; + @Value("${mercadopago.access.token}") + private String mercadoPagoAccessToken; @Override - public ResponseEntity listener(PaymentListenerRequest request) { + public ResponseEntity listener(PaymentListenerRequest request, HttpHeaders headers) { + try { + if (!this.verifyWebhook(headers, request.data().id().toString(), mercadoPagoAccessToken)) + return ResponseEntity.status(401).build(); + } catch (Exception e) { + return ResponseEntity.status(401).build(); + } + final PaymentClient paymentClient = new PaymentClient(); + final Payment payment; try { payment = paymentClient.get(request.data().id()); @@ -45,4 +61,32 @@ public ResponseEntity listener(PaymentListenerRequest request) { this.paymentService.handle(input); return ResponseEntity.ok().build(); } + + private boolean verifyWebhook(HttpHeaders headers, String id, String secret) throws Exception { + final String xSignature = headers.getFirst("x-signature"); + final String xRequestId = headers.getFirst("x-request-id"); + + assert xSignature != null; + final String[] parts = xSignature.split(","); + String ts = null; + String hash = null; + for (String part : parts) { + final String[] keyValue = part.split("="); + if ("ts".equals(keyValue[0])) { + ts = keyValue[1]; + } else if ("v1".equals(keyValue[0])) { + hash = keyValue[1]; + } + } + + final String manifest = String.format("id:%s;request-id:%s;ts:%s;", id, xRequestId, ts); + + final Mac hmac = Mac.getInstance("HmacSHA256"); + final SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256"); + hmac.init(secretKeySpec); + byte[] signature = hmac.doFinal(manifest.getBytes()); + + final String computedHash = Base64.getEncoder().encodeToString(signature); + return computedHash.equals(hash); + } } diff --git a/infrastructure/src/main/java/br/com/ifsp/tickets/infra/contexts/financial/payment/persistence/PaymentJpaEntity.java b/infrastructure/src/main/java/br/com/ifsp/tickets/infra/contexts/financial/payment/persistence/PaymentJpaEntity.java index 790f8ff..2457709 100644 --- a/infrastructure/src/main/java/br/com/ifsp/tickets/infra/contexts/financial/payment/persistence/PaymentJpaEntity.java +++ b/infrastructure/src/main/java/br/com/ifsp/tickets/infra/contexts/financial/payment/persistence/PaymentJpaEntity.java @@ -22,7 +22,7 @@ public class PaymentJpaEntity implements Serializable { private Long id; @Column(name = "external_id", nullable = false, updatable = false) private String externalId; - @Column(name = "status", nullable = false, updatable = false) + @Column(name = "status", nullable = false) @Enumerated(EnumType.STRING) private PaymentStatus status; @Column(name = "order_id", nullable = false, updatable = false) @@ -35,9 +35,9 @@ public class PaymentJpaEntity implements Serializable { private String paymentType; @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; - @Column(name = "updated_at", nullable = false, updatable = false) + @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; - @Column(name = "approval_date", nullable = false, updatable = false) + @Column(name = "approval_date", nullable = false) private LocalDateTime approvalDate; public PaymentJpaEntity(