Why do we need receipt validation for IAP?

Hi,

I'm starting to work on adding a auto-renewable subscription in my app to unlock certain features. I understand the StoreKit and iTC details, but one thing that's not clear to me is *why* we need to do receipt validation for in-app purchases? There seems to be a lot of empasis on doing this, and it gets complicated because you need to either import different libraries to do this client-side (which isn't the recommended option) or do it server-side, which adds a lot of overhead. In my case, I don't have any server running at all, and my whole app relies on CloudKit instead.


So I'm wondering why it's so essential in case of IAP.

a) if it's for preventing piracy, shouldn't that apply to paid-apps as well? But no one talks about reciept validation for paid-up-front apps.

b) when StoreKit's paymentQueue "updatedTransactions" delegate tells us that someone has purchased an IAP, is that something that can be spoofed? I would imagine not, and even so, this would only be possible with jailbroken devices. Is that what people are worried about? How much of a problem is this in the real world?

c) is there certain information in the reciept that is essential for subscriptions to work correctly?


In my mind, the simplest implementation would be this: display the IAP products, implement the storekit delegate to see when the user purchased the product and mark that user's CloudKit "User" record with the type and date of subscription (this can also be used to restore the IAP on another device) and unlock the features. When the next billing cycle comes, I can wait for StoreKit to tell me whether the user cancelled or continued the subscription, and lock/unlock the feature accordingly. I don't see the necessity of receipt validation in this case. But I might be wrong and misguided about my assumptions.


Would love some comments about this.

Post not yet marked as solved Up vote post of zulfishah Down vote post of zulfishah
11k views

Replies

It is essential because there are a massive amount of users out there trying to purchase IAPs with invalid receipts. So this is not a theoretical issue, it is a real problem. For example **** broken devices are often already modified to automatically send a, let's call it a "recycled" iTunes receipt. In practice, many users of **** broken devices are not even aware that their devices are doing this by default. So even when they try to restore IAPs that they really bought before, their device would often use a bogus receipt instead by default.


Also this is not solely about piracy, you are talking about subscriptions. And when you decode a receipt it also contains the information for which time frame the user purchased the subscription, and also whether the subscription has already been terminated by the user via Apple's iTunes support (think about restoring purchases for instance).


And no, these issues are not soley limited to **** broken devices. There are also tools out there which allow you to bypass certain mechanisms on regular iOS devices without **** breaking them, simply by accessing them with USB. There are even videos shared on social networks demonstrating also to average users how to use them to achieve their goals within minutes.

Couple of comments:


1) The problem is limited if your cost-of-goods is $0. By that I mean that most 'theft' is not a lost sale but rather a theft of an IAP that would not otherwise have been purchased. So if a new subscription/IAP doesn't cost you anything then you aren't losing anything since there is no lost sale.


2) StoreKit makes a call to updatedTransactions to trigger a sale. It was, and may still be possible to set up any device so that a fake call is sent into updatedTransactions with an associated object that could trigger a sale. That object will include a transaction that looks like a real sale. This was a big problem a few years ago. To stop that problem Apple announced that the transactionIdentifier was meant to be unique and then added a signed, encoded object called a receipt. They did not otherwise restrict the call to updatedTransactions. Therefore your method can be easily spoofed (but see #1 above).


3) You don't need to use iCloud for anything (other than restoring non-renewable subscriptions). You can rely on restoreCompletedTransactions.


4) All of the information you need for all transactions (except perhaps an autorenewable subscription) is contained in the transaction object. For an autorenewable you may have troubles figuring out certain introductory pricing and initial free periods from the transaction object since all it contains is the productIdentifier (i.e. what IAP was selected) and the transactionDate (when it was purchased). But absent these fine points, everytime an autorenewable renews it sends a transaction with a new purchase date and productIdentifier into updatedTransactions.


5) I STRONGLY disagree that the preferred method of decoding the receipt is sending it to the Apple servers. Decoding on the device itself is the ONLY currently secure approach. Because the Apple servers do not check identifierForVendor there is no way of knowing that the receipt is really signed for the device. I have offered to explain this security issue to Apple but they have never responded. So here are the options:

A) Forget about fraud, rely on updatedTransactions. (Easiest but vulnerable to simple hacks.)

B) Send the receipt from the app directly to the Apple servers for decoding. Get back all the autorenewable info you could ever want. (Easy but subject to man-in-the-middle hacks.)

C) Send the receipt via a signed transmission (i.e. trusted and protected) to your server and then from your server to Apple for decoding. (Requires a server; allows your server to know independently whether the subscription is still active; difficult but not impossible to hack.)

D) Decode the receipt on the device. (Hard. Requires some security coding and OpenSSL but not impossible. Secure.)

Post not yet marked as solved Up vote reply of PBK Down vote reply of PBK

Thanks a lot for your thoughts. Really helpful.

Thanks for your solid analysis, which I fully agree with. Unfortunately, the cost for running a server or including OpenSSL (and developing and maintaining the software around it in both cases) often exceeds the benefit for in-app purchases.


Why does Apple not just provide an API for on-device verification?

OpenSSL is complicated but once programmed it works without any expense.


>Why does Apple not just provide an API for on-device verification?

I believe they are concerned that anything they provide could be hacked. The OpenSSL decoding of the receipt is close to an API.


If OpenSSL is too complicated - go with 5b above and check the transaction_identifier field coming back to be sure they got it correctly.

Not so much that the Apple libraries could be hacked (modified), but that on a **** broken device it would be simple enough to replace the standard libraries with arbitrary wrapper libraries. Library substitution can even be a valid debugging technique, that's how you get things like stack protection and Malloc-guard for C style languages on some platforms, so the technique for doing that is well documented.


The point in Apple's documentation where they say that non-standardized code is more work to crack is fairly important.

Hi (from France) and thanks a lot for your analysis.


I've chosen B for testing the validity of an auto-renewable in-App and I've made tests on both sandbox and production environment.

For production environment, I've subscribed with my own money and then canceled the subscribtion so I DO know that it is no more available and I can see it on iTunes.


B works on both environment until I launch a restore operation on production environment : then updatedTransactions replies that the subscribtion product is restored but it should not from my point of view, this is a different information than the result of B...


So I've this this simple question : how to interpret the updatedTransactions information provided in case of an auto-renewable subscription ?


Thanks a lot !

In updatedTransactions you get certain limited information on the transaction. But that information should be enough top extend the subscription. If you send the receipt to the Apple servers you get lots more information.


What are you asking? Is the answer here:

https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1


https://developer.apple.com/documentation/storekit/skpaymenttransaction?language=objc


https://developer.apple.com/documentation/storekit/skpayment?language=objc

I should have responded earlier on this issue, but hope the following will be useful.


With regards to the question about the security of appStoreReceipt validation - the appStoreReceipt is signed by Apple. If the appStoreReceipt is validated by the App Store verifyReceipt server, the results indicate what App Store charges in the receipt. For the case where the in_app array indicates that there is an auto-renewing subscription item, then there is additional information provided in the "latest_receipt", "latest_receipt_info", and in the "pending_renewal_info" fields.


The following is a canned response I provide to developers who submit questions on how to support apps which offer auto-renewing subscription in-app purchases items.


To understand the in app purchase of the auto-renewing subscription item, lets start at the beginning. The user launches the in app purchase application. As per Tech Note 2387 IAP Best Practices

<https://developer.apple.com/library/content/technotes/tn2387/_index.html#//apple_ref/doc/uid/DTS40014795>

the application will call addTransactionObserver - which will activate the StoreKit transactionObserver. The transactionObserver checks the iTunes Store for incompleteTranactions. Refer to the StoreKitSuite iOSInAppPurchaseSample code for an example on implementing the StoreKit API in a shared process, rather than in the ApplicationDelegate class.


The application will validate the registered in app purchase identifiers using the SKProductsRequest to obtain the current description and pricing for the in app purchase identifiers - for example “mycompany.myapp.autorenewsubscription1month”.


At some point, the user decides to make the in app purchase. The user presses the buy button and the addPayment call is made to the iTunes Store to charge the current user for the purchase of “mycompany.myapp.autorenewsubscription1month”. Let’s assume that this is the first time the user has attempted to purchase the 1 mo auto-renewing subscription item and that there is a free trial associated with the purchase. The user’s account is charged and the updatedTransactions delegate method is called with the SKPaymentTransactionStatePurchased.


The app then validates the applicationReceipt, if it chooses to do so - by accessing the receipt at appStoreReceiptURL. The app encodes the receipt in base64, then forwards it to the developer server for processing. (some apps send the receipt directly to the Apple Receipt Validation server process - but this is against recommendation as it can lead to a man-in-the-middle attach). The developer server forwards the base64 encoded receipt with shared secret to the Apple Receipt Validation server process. The validated receipt is returned. The app inspects the in_app array and should find one item with the product_id field set to “mycompany.myapp.autorenewsubscription1month”. The app calculates the length of the subscription by the difference of the expires_date and the purchase_date. The result should be the length of the free trial period - not the one month period plus the free trial period. The app notes that the expire_date is later than the current date so it knows that the subscription is active.


One note on detecting whether the auto-renewing subscription is in the free trial period. You will find in the in_app array record, (as well as in the latest_receipt_info record) that there is the "is_trial_period" field. The value of this field is either "true" or "false". This field can be present in all in-app purchase types, not just for auto-renewing subscription records and has no current meaning for non auto-renewing subscription records.


Lets assume that the user does not cancel the free trial. On the last day of the free trial, the iTunes store will bill the user for the first month of the auto-renewing subscription (I may be off as to the exact time that the user account is billed). The iTunes store will set up an incompleteTransaction on the store for the user and applicationID. At some point, the user will launch the application and assuming that the application has called addTransactionObserver in the startup process, and there is an active network connection, the application immediately detects the incomplete transaction. The updatedTransactions delegate method is called with the SKPaymentTransactionStatePurchased. The applicationReceipt is validated. Now when the app inspects the applicationReceipt, there will be a second item in the in_app array. The app cannot assume that the in_app array contents are in any order. The current purchase might be the first element in the in_app array. The app finds the iAP item where the expire_date is later than the current date and knows that the subscription is still active. This is how the app will detect that the auto-renewing subscription is current. When the validation process is complete, make sure to call finishTransaction on the transactionIdentifier so that the store will mark the incompleteTransaction as “completed”


So what happens when a user cancels a subscription. There are 2 cases to study here. There is the case that the user decides to let the current subscription expire. this happens when the user enters their iTunes account, to the “Manage Subscriptions” section and chooses to allow the current subscription to expire. In this case, on the next auto-renew date, iTunes will not post an incompleteTransaction to the user account. When the application validates the applicationReceipt, there will no longer be an in_app array item with the expire_date greater than the current date. If the application knows that the subscription was active, it can alert the user that the receipt does not show that the subscription is active and can ask the user whether to refresh the receipt using the SKReceiptRefreshRequest. If the user agrees, then the app makes the call, validates the updated receipt and scans for a current subscription item in the in_app array. So long as there is no in_app array item where the expire_date is greater than the current date, the subscription is expired.


The other cancellation case is where the user contacts Apple Care and requests a refund for the in_app purchase. When this happens, the date of the refund is noted. When the applicationReceipt is validated following this event, the cancellation_date field for the corresponding in_app array item will be set. Note that as per TN 2413, iAP FAQ <https://developer.apple.com/library/ios/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPT-HOW_DO_I_USE_THE_CANCELLATION_DATE_FIELD_>

the cancellation_date field is only added for non-consumable and auto-renewing subscription purchase types. Note that the cancellation_date applies to the specific in_app array item, and not to all of the in_app array items. It’s important to note - the cancellation_date field is set, only for the auto-renewing subscription in app purchase item for the period in which the cancellation occurred. For example, if the applicationReceipt contents are current, and the user contacts Apple Care and receives a refund, then the cancellation_date will be set for the most current auto-renewing subscription item in the in_app array. However, if the applicationReceipt has not been updated since the original purchase and the in_app array does not contain the most current auto-renewing subscription item, then the cancellation_date field will not be set in the original auto-renewing subscription purchase item.


However, in this latter case where there is only the original applicationReceipt, see the following.


Note that the applicationReceipt is only updated when the receipt is refreshed, when the updatedTransactions delegate method is called for a successful or restored transaction or when the application is installed from the iTunes store (in the case that another copy of the app is to a second device).


The above description describes the process envisioned by StoreKit engineering for how an application works with the iTunes Store server to detect auto-renewing subscription renewal.


It turns out that there is a second method - which is not so clearly documented; it is hinted at via comments in the Receipt Validation Programming Guide. The alternate means for receipt validation allows for a server based validation process independent of the app. The app must be used to make the initial purchase of the auto-renewing subscription item so that a copy of the applicationReceipt can be passed to the server. The server passes the base64 encoded applicationReceipt to the iTunes Store verifyReceipt server. On a successful validation, the server looks for the latest_receipt and latest_receipt_info fields of the JSON results. In addition, there is the new field - "pending_renewal_info" to assist developers in understanding the status of auto-renewing subscription item pending renewal. Within this field there is


"expiration_intent" - documents the reason why a subscription has expired

"is_in_billing_retry_period" - documents whether or not Apple is still attempting to automatically renew the subscription

"auto_renew_status" - This key is only present for auto-renewable subscription receipts, for active or expired subscriptions. The value for this key should not be interpreted as the customer’s subscription status. You can use this value to display an alternative subscription product in your app, for example, a lower level subscription plan that the customer can downgrade to from their current plan.



As documented in the Receipt Validation PG

<https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html>


latest_receipt - Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions. The base-64 encoded transaction receipt for the most recent renewal.


latest_receipt_info - Only returned for iOS 6 style transaction receipts for auto-renewable subscriptions. The JSON representation of the receipt for the most recent renewal.


The value of the latest_receipt_info - once this information is captured for an auto-renewing subscription in-app purchase item, the latest_receipt_info can be used independently to view the renewal status of the auto-renewing subscription item (and items in the group). I refer you to the statement in the “Receipt Validation Programming Guide”


“The values of the latest_receipt and latest_receipt_info keys are useful when checking whether an auto-renewable subscription is currently active. By providing any transaction receipt for the subscription and checking these values, you can get information about the currently-active subscription period. If the receipt being validated is for the latest renewal, the value for latest_receipt is the same as receipt-data (in the request) and the value for latest_receipt_info is the same as receipt.”


The above statement is from the iTunes Store Server engineering team and explains that once the latest_receipt field has been captured, that field is base64 encoded and can be resent to the appropriate verifyReceipt server. The JSON response can be parsed and will provide the latest information about the auto-renewing subscription item. The result can contain multiple items, and for this reason. As such the status result 21006 does not apply. This is because there can be multiple items in the response and the expired status may not apply to all items. When the status of 0 is returned, the receipt was successfully validated and the app (or server process) must process the returned results individually.


Once you have the latest_receipt_field from the initial purchase, you can validate the latest_info_results contents and watch as the auto-renewing subscription item renewed from that point on. If a user switches to another auto-renewing subscription item in the group, then you validate the same original receipt, the change to the different auto-renewing subscription group item is recorded.


If all you keep is the initial latest_receipt contents from the first purchase, and continue to use this saved latest_receipt to validate the auto-renewing subscription you will find on validation that there will only be the single auto-renewing subscription item in the in_app array. However as the auto-renewing subscription renews, you can inspect the contents of the latest_receipt_info field to find that the renewals will be listed.


One additional note, a user has the option to terminate an auto-renewing subscription item in their iTunes account. The server process can only detect an expired auto-renewing subscription when there is no active auto-renewing subscription item in the JSON results (in_app array or in the latest_receipt_info). In addition, the user can restart an expired subscription in their iTunes account. By validating the latest_receipt, a server process can detect that the expired auto-renewing subscription item has been restarted.


This mechanism makes it possible for a server process to track the auto-renewing subscription renewal process rather than do so on the app. It also turns out that the cancellation of the auto-renewing subscription item can be detected in the same manner. The Receipt Validation program reference describes that a cancellation_date field is set for an auto-renewing subscription in_app array item for the period which is cancelled. The same is true in the contents of the latest_receipt_info. Lets be clear as to the requirements for detecting the cancellation_date. The applicationReceipt must contain in the in_app array, the in-app purchase identifier for an auto-renewing subscription item. When the receipt is validated and the “shared-secret” is included in the validation request sent to the iTunes verifyReceipt server, the server will return in the latest_receipt_info, the latest information for that auto-renewing subscription item to include the cancellation_date for the period in which the cancellation was requested. You will also note that the contents of the in_app array element for the auto-renewing subscription doesn't reflect the cancellation_date since you are working with a non-updated appStoreReceipt. Were the app to have the user refresh the appStoreReceipt following the cancellation event, then the resulting appStoreReceipt would show the cancellation date for the auto-renewing subscription item in the in_app array.


Note also that the cancellation_date may be earlier than the expiration_date for the transaction record in which the cancellation_date field is set.


Also note that the cancellation_date only applies to the period in which the cancellation request was approved. The user can decide later to start the auto-renewing subscription item again.


rich kubota - rkubota@apple.com


developer technical support CoreOS/Hardware/MFI

Post not yet marked as solved Up vote reply of rich Down vote reply of rich

Hi rich,


The length of your explanations indicates the main issue with in-app purchases: they are too complex, not only for developers, but particularly also for end users. IAPs are a bad remedy for a fundamental issue in the app economy: users don't want to pay for apps they cannot try. So we make the app free to use but stuff them with ads and IAPs to get a revenue. Ads, payments for full functionality, and pay-to-win business models are a nuisance to end users. They also reduce the quality of the effective use of the app, are a burden to developers, and increase the risk of abuse.


A better solution would be to introduce trial periods on paid apps. Users could try the full functionality of an app for a limited time (selected by the developer) and then get a message to buy the app if they liked it and want to continue using it. The OS could handle the disabling at the end of the trial period, avoiding a man-in-the-middle attack.


Apple does already have the technology to disable apps after a trial period, in TestFlight. Extending app trial to the App Store could encourage end users to try quality apps and buy them when they are convinced that they bring them value.

Length doesn't matter. It's how you implement the functionality - (i.e. just avoid autorenewable subscriptions).


But your issue is about something completely different, free trial periods for apps. For that a 'lite' version and a 'full' version enabled through IAP is the solution together with a timed period for the 'full' version. If you forego receipt verification and just use a simple non-consumable IAP product to upgrade it's all pretty easy. It only gets complicated if you want an autorenewable subscription and insist upon checking the receipt.


Use the keychain or the Cloud key-value file to indicate the date the app was first downloaded. If that date is more distant than the free trial period then revert to lite.

I am a completely self taught and not very good developer.


Despite that I have had a simple app on the Mac App Store for a few years now and it has made me some money. I would like to make the app an auto renewable subscription.


Are you saying, and is it still the case, that apart from checking that there is a receipt I don’t need to read it at all and I can use upadtedTransactions to tell the app when the subscription has been renewed?


I can’t get my head around all of the openssl stuff and I would rather spend the time that I have to devote to developing working on new apps.


Thanks for any replies.


Francis

>Are you saying, and is it still the case, that apart from checking that there is a receipt I don’t need to read it at all and I can use upadtedTransactions to tell the app when the subscription has been renewed?


Yes. (You don't even need to check that there is a receipt.) You may be hacked, but you have to decide if you care about that. Hacking may do 2 things - 1) it will ever so slightly reduce the number of sales you might have made because most people who are willing to purchase a subscription do not engage in hacking and 2) it may give you many many non-paying hacked users who would otherwise not have paid anyway. But if your 'marginal cost-of-goods' (i.e. how much it costs you to support additional subscribers) is $0 why care?


You can't manage 'free trial subscriptions' without decoding the receipt. So don't opt for them.

Post not yet marked as solved Up vote reply of PBK Down vote reply of PBK
  • This is one of the best responses here - it actually sheds light on why you might want to verify (or not).

    In my case, my IAP is simply removing ads. Thanks to this comment, I now understand that there's little need to verify in this case, because it doesn't cost me anything to remove ads.

Add a Comment

This answer is really very good explanation of how IAP work. Perfect. Thanks.

Good grief this is all so over complicated.