Search code examples
c#asp.net-web-apiidempotent

How To Make C# Web API Payment Method Idempotent?


Let's say I have a site that sells credits to let you use its services. A clientside payment script (Stripe, PayPal) returns order data as a response. After that, server side payment method MakePayment() is called. It does the following:

  1. Retrieve the log of an already created order, for which payment is being made: OrderLog.
  2. Verify the payment serverside.
  3. If it's ok, create a CreditPurchase entity, which gets stored in the database. This is what the user purchased.
  4. Update the OrderLog entity to point to the freshly created CreditPurchase entity.

It seems to me that a user could hack my client side scripts to fire multiple times in a row, and my server would create multiple CreditPurchase entities, all of which go to the user. Whichever method execution runs last, that's the CreditPurchase that gets put on the OrderLog entity. But the user still gets all those CreditPurchases.

I suppose I could, on retrieval of the OrderLog, check if the OrderLog already has a CreditPurchase set to it. This would mean the purchased product was already given to this user, for that particular order. But the problem is that if the user hacks the client side script to run multiple times at once, then MakePayment() will execute multiple times in parallel. Multiple times it will pass the check for whether CreditPurchase isn't yet on the OrderLog.

The user would still get multiple CreditPurchases.

So how, exactly, does one make a method like this idempotent?

A lock will lock it for every user. But I only want a single user to be able to call a WebAPI method once at a time.

Is there no server side configuration available for this?

If you look at how the big boys, such as Stripe, do it... it's through letting the client generate an idempotency key.

There are 2 problems with this approach:

  1. If the serverside 'idempotent' method is called many times in rapid succession, multiple instances of it might execute up until the moment where the idempotency key is stored in a cache, based upon which it checks whether to execute or return the previous response. So it's not really idempotent.

  2. If the client gets to choose its own idempotency key, a malicious hacker can simply generate multiple different idempotency keys, to be used with requests that operate on the same payment.

Thinking about it and frantically Googling it, it almost seems like idempotency is an unsolved (and unsolveable?) problem.


Solution

  • Your use case needs a slightly different processing flow than you describe. This is based on the Stripe documentation, but any payment provider should be able to do this:

    1. Set up the intent to pay with provider (API call directly from your server to the provider API). This sets up what Stripe calls a PaymentIntent. In doing this, you send the provider the details of the transaction, and also a callback webhook on your server for payment completion information. You get a secret value to pass to the client as the response to this request.
    2. Your server stores the order request into your order queue along with the identifier of the PaymentIntent that you got above.
    3. Your server sends the client to the provider's payment page, including the secret key you got from the provider to identify the transaction. The key uniquely identifies the payment to the provider, so they now know where to send the payment completion status.
    4. Client performs payment.
    5. The payment provider calls your server webhook directly to advise the payment status. You put the status into a queue for processing the order.
    6. Your order processing queue performs the actual adding of services into the user's account.
    7. The payment provider redirects the client back to your site.
    8. You check for payment completion status in the order queue. If it isn't completed yet, refresh the request as needed until you have a result. If it is completed, show status. Once you've got to this point, the order processing will have added the services to the user account, so they continue as needed to do what they want.

    If the user resubmits the payment redirect, they aren't inserting anything into the order queue, and you can see by referencing the completed order identifiers that they are doing it.