Lan2Play / eventula-manager

Event Manager App for Eventula
https://www.eventula.com
GNU General Public License v3.0
26 stars 13 forks source link

Payment via PayPal sometimes results in duplicate purchases #673

Open icewindow opened 7 months ago

icewindow commented 7 months ago

Issue description

When a user purchases a ticket via PayPal, sometimes two Purchases are created, resulting in the user gaining an extra ticket they did not pay for.

Details/Observations

It is currently unknown when or why duplicate purchases are created. So far duplicate purchases have only been observed to be created when the user checks out via PayPal, suggesting the problem lies within the PayPal handler or with PayPal themselves. However, this could be a result of almost all of our users using PayPal instead of Stripe, thus biasing the problem towards PayPal.

The creation date of the duplicate purchases are also not identical, but rather a few seconds (usually between 2 and 3) apart, suggesting the callback gets called multiple times. Other than the creation date, the purchases are identical (transaction ID, token, etc.).

Ideas for mitigation

Without having a clear idea why some purchases are duplicated, there are some possible ways to mitigate the problem.

"Proper" solution

The above described ideas are really only a temporary fix. To properly address the issue the payment flow needs to be investigated and pinned down, why some purchases are duplicated.
Furthermore, some additional measures could be put into place to prevent duplicate purchases from being created. A check if the same transaction ID is already present, similar to the suggestion above, would probably be a good idea. Or perhaps the use of a nonce, attached to the payment processing callback. If the nonce has already been used, meaning the callback was already executed, no further action is taken, instead the result of the previous callback is send again.

icewindow commented 7 months ago

Looking in the access logs reveals that multiple calls are being made to the callback:

This one resulted in multiple purchases being created.

194.94.76.66 - - [19/Nov/2023:09:27:48 +0000] "GET /payment/callback?gate=paypal_express&type=return&token=EC-2xxxxxxxxxxxxxxxD&PayerID=Zxxxxxxxxxxx4 HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
194.94.76.66 - - [19/Nov/2023:09:27:48 +0000] "GET /payment/callback?gate=paypal_express&type=return&token=EC-2xxxxxxxxxxxxxxxD&PayerID=Zxxxxxxxxxxx4 HTTP/1.1" 301 162 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
194.94.76.66 - - [19/Nov/2023:09:27:50 +0000] "GET /payment/callback?gate=paypal_express&type=return&token=EC-2xxxxxxxxxxxxxxxD&PayerID=Zxxxxxxxxxxx4 HTTP/1.1" 302 414 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
194.94.76.66 - - [19/Nov/2023:09:27:51 +0000] "GET /payment/successful/366 HTTP/1.1" 200 3023 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"

Of note are the the purchase IDs, which are 365 and 366, the latter being accessed via payment/successful/366, whereas the former presumably would've been used after the request that resulted in a 499 status code.


This one resulted in just a single purchase being created, despite the callback being called multiple times.

194.94.79.249 - - [19/Nov/2023:09:42:47 +0000] "GET /payment/callback?gate=paypal_express&type=return&token=EC-9xxxxxxxxxxxxxxxW&PayerID=5xxxxxxxxxxxU HTTP/1.1" 302 414 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
194.94.79.249 - - [19/Nov/2023:09:42:47 +0000] "GET /payment/successful/367 HTTP/1.1" 499 0 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
194.94.79.249 - - [19/Nov/2023:09:42:47 +0000] "GET /payment/callback?gate=paypal_express&type=return&token=EC-9xxxxxxxxxxxxxxxW&PayerID=5xxxxxxxxxxxU HTTP/1.1" 301 162 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
194.94.79.249 - - [19/Nov/2023:09:42:48 +0000] "GET /payment/callback?gate=paypal_express&type=return&token=EC-9xxxxxxxxxxxxxxxW&PayerID=5xxxxxxxxxxxU HTTP/1.1" 302 414 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
194.94.79.249 - - [19/Nov/2023:09:42:48 +0000] "GET /payment/successful/367 HTTP/1.1" 302 322 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"

Presumably the payment/successful/ request removed the payment parameters from the session and further requests to the callback route couldn't due to this.


There are instances where only a single request to the callback route is made though:

194.94.76.66 - - [19/Nov/2023:09:43:49 +0000] "GET /payment/callback?gate=paypal_express&type=return&token=EC-8xxxxxxxxxxxxxxx3&PayerID=JxxxxxxxxxxxC HTTP/1.1" 302 430 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0"
194.94.76.66 - - [19/Nov/2023:09:43:50 +0000] "GET /payment/successful/368 HTTP/1.1" 200 3990 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0"

There's also this one, which calls the callback route twice but with different return codes. This one also resulted in just a single purchase being created...

194.94.79.249 - - [19/Nov/2023:10:35:59 +0000] "GET /payment/callback?gate=paypal_express&type=return&token=EC-5xxxxxxxxxxxxxxxW&PayerID=Vxxxxxxxxxxx2 HTTP/1.1" 301 162 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.1 Mobile/15E148 Safari/604.1"
194.94.79.249 - - [19/Nov/2023:10:36:03 +0000] "GET /payment/callback?gate=paypal_express&type=return&token=EC-5xxxxxxxxxxxxxxxW&PayerID=Vxxxxxxxxxxx2 HTTP/1.1" 302 414 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.1 Mobile/15E148 Safari/604.1"
194.94.79.249 - - [19/Nov/2023:10:36:03 +0000] "GET /payment/successful/371 HTTP/1.1" 200 3107 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1.1 Mobile/15E148 Safari/604.1"

This seems to be the normal behavior for the site, a 301 redirect followed by the 302 to (presumably) the success page.

I think this might actually be a misconfiguration of our web server, or rather the proxy in front of it, and the payment callback URLs are generated with the HTTP scheme instead of HTTPS. Could this also play a part in the duplicate entries being generated?