Paypal Integration Gotcha!

January 2018 ยท 3 minute read

This post was initially a hit-piece on Paypal’s developer experience before transitioning into a guide to integrate paypal and finally flattened into showing a grave mistake to avoid whilst using paypal’s API.

Foundational concepts

To give Paypal credit where it is due, their APIs are nicely standardized. Which is to say that the front-end library has the same methods as the back-end libraries despite implementaiton differences. There are 3 main methods that you want to know:

The first one can be broken down further into paypal.payment.authorize() and paypal.payment.capture() but you can figure those out on your own.

Fundamentally, you are only really required to use .create() and .execute() to create and then execute the payment. What is interesting with Paypal is that you can move them around as you please. The most useful and reasonable way to organise it is (and the one that paypal recommends) is to create the payment on the front-end, send the information needed to execute the payment to the back-end then execute it.

This would look something like this:

// front-end.html
<script>
paypal.Button.render({
    env: "sandbox",
    client: {
        sandbox: "12345678",
        production: "123456789"
    },
    commit: true, // Show a 'Pay Now' button
    payment: function(data, actions) {
        return actions.payment.create({
            transactions: [{
                amount: {
                    total: '30',
                    currency: "usd"
                },
                item_list: {
                    items: [{
                                description: "Purchase description",
                                price: "30",
                                currency: "usd",
                                name: "Name of whatever",
                                quantity: 1
                            }]
                }
            }]
        });
    },

    onAuthorize: function(data) {
        console.log(data)
        $.post('/pay', {
                paymentID: data.paymentID,
                payerID: data.payerID
            })
            .done(function(data) {
                location.reload();
            })
            .fail(function(err) {
                alert('There was an error! Try again.');
                location.reload();
            });
    }
}, '#paypal-button');
</script>
// back-end.js //
// Do all the stuff
paypal.payment.execute(paymentId, payerId, function(error, payment){
    if (error) {
        // something went wrong
        return;
    }
    // payment object exists
})

Never trust user input

// back-end.js
paypal.payment.execute(paymentId, payerId, function(error, payment){
    if (error) {
        // something went wrong
        return;
    }
    paymentWasSuccessfulShipProduct();
})

In the above example, you know that the payment was successful. Congratulations! But what makes you think the proper sum was paid? What if the user modified the amout owned from 300$ to 1$? For that case, I see people adding checks for the total amount before executing it using .get. But that is not enough, the currency could have changed too and you will need to check that as well.

With those two precautions in place, this issue could be considered solved:

// back-end.js
paypal.payment.get(paymentId, (error, details)=>{
    if (error) return new Error("Payment does not exist");
    // Payment exists
    const transaction = details.transactions[0]
    const paidAmount = transaction.amount.total
    const paidAmountCurrency = const paidAmount = transaction.amount.currency

    if (paidAmount == expectedPaidAmount && paidAmountCurrency == expectedPaidAmountCurrency) {
        // All good, execute payment
        paypal.payment.execute(paymentId, payerId, function(error, payment){
            if (error) {
                // something went wrong
                return;
            }
            paymentWasSuccessfulShipProduct();
        })
    }
})

This could also be done after receiving the payment but that would require refunds which could become a pain.

What about other payment processors?

Other payment processors (at least Stripe), have a forced security mecanisms for this. With Stripe, for instance, will require you to essentially create the same charge twice. Discrepencies will make the charge fail. The first time this is done is on the front-end, when the user enters his credit card.

The second time it is done is on the back-end, when you recreate the payment with the amount owned and the currency.

 stripe.charges.create({
    amount: 3000,
    currency: "usd",
    description: "some description",
    source: token, //Sent from front-end
})

I wonder how many websites have failed to securely implement paypal because of naively following Paypal’s own documentation with the assumption that the official documentation of the world’s largest payment processor will be secure.