Friday, March 2, 2012

Handling Fraudulent In-App Purchases on iOS

The Audiogalaxy mobile apps for iOS and Android have some in-app purchases which allow users to buy add-ons to augment the free service. I've written previously about some interesting observations about implementing each. Continuing that series, here are some more interesting things I've seen when dealing with in-app purchases on iOS, specifically related to fraudulent purchases.

Apple's in-app purchases programming guide describes the steps needed to verify store receipts once you're notified of a successful transaction in the SKRequestDelegate callback. It's tempting to just unlock the feature when you get notified of a successful transaction being complete, but you really should do the extra work required to send over the receipt to Apple for verification, or you risk getting defrauded pretty easily. Ideally, you want to send the receipt up to your own server first (over SSL, obviously) and then verify the receipt using Apple's iTunes receipt verification service.

Since releasing the in-app purchase for iOS, I've seen several fraudulent purchase attempts. For the first version, I took a conservative approach - when notified by StoreKit that a transaction was successfully completed, I unlocked the feature and then sent the receipt up to my server for verification. Additionally, I recorded verification responses from Apple's service but didn't make the device reject the add-on if it failed. The reason I did this was a combination of naivete and being unsure of how things would really work in the wild. What if my server could not be reached? What if Apple's service was down? I didn't want the user to be charged money and be left hanging because of connectivity problems. (Not knowing better, I also assumed that the 'purchase complete' callback was pretty bullet-proof and meant that the user was charged, and that verification was just added formality).

Well, it turns out that StoreKit on jailbroken iOS devices can be easily fooled into believing that a local transaction completed successfully. So with my conservative approach, users with jailbroken phones could easily get in-app features unlocked without paying for them. Thankfully, I caught this before it did serious damage and pushed an app update. Because of the dumb-client architecture of my system, I was also able to easily revoke the purchases made fraudulently (need more reasons to have a server be in control?) What I had originally failed to account for is that the system is built to handle verification failures - if you legitimately fail to verify a transaction because of connectivity problems on either end, just don't mark the transaction as 'finished' and it will automatically get restored the next time StoreKit's queue is initialized (which is why it's also a good idea to write code to handle restored transactions, even if your in-app purchases are only of the consumable variety). This might mean that an add-on won't be available to a user for a while if they paid and you failed to verify the transaction because of networking problems, but they won't be re-charged for it and it will eventually be unlocked correctly.

In case you're interested, here are some examples of fake base64 encoded receipts:
The response from Apple, when these types of receipts are sent for verification, looks like:
{"status":21002, "exception":"java.lang.ClassCastException"}
FWIW, Apple's documentation states that any non-0 response is considered a failure for purchases other than auto-renewing subscriptions. For those, they have a set of well-defined error codes. In a future post, I'll try to cover my experiences implementing auto-renewing subscriptions (and the non-technical, app approval overhead that goes with it).

If you're curious, here's the distribution of where fraudulent purchase attempts came from:
United States - 38.46%
Israel - 14.11%
Canada - 5.45%
United Kingdom - 4.49%
Saudi Arabia - 3.81%
Netherlands - 3.53%
Germany - 1.60%
Mexico - 1.60%
United Arab Emirates - 1.28%
Japan - 1.28%
Others - rest