I'm implementing security in my iOS 7 application for iOS In-App purchases. When testing with LocalIAPStore hack I see that when I call the method [[NSBundle mainBundle] appStoreReceiptURL] the returned receipt is null, so it's all right here.
The problem comes when the user purchases an option without having activated LocalIAPStore. If this is done, a receipt is generated. Then, I send it to my server to check that everything is correct and show purchase, but if active again LocalIAPStore, I keep returning a correct receipt, but has actually purchased another option and this is returned as a calidate.
To verify receipt, what else can I do? What should I check locally to avoid it? Any tips? How do you do it?
Thanks!
Related
I have created an app which has auto-renewable subscriptions.
The following is the logic that I use to know if the user has an active subscription.
Whenever paymentQueue(_:updatedTransactions:) of SKPaymentQueue is called, I try to perform receipt validation using following steps
I check if the local receipt is present. If it is not present I use SKReceiptRefreshRequest to refresh the receipt.
I send the receipt information to verifyReceipt endpoint of the App Store server.
The server returns response which contains information about the subscription expiration date.
I store the expiration date in the app and present the appropriate UI based on whether the user has an active subscription or not.
The App Store review has rejected my app multiple times because the SKReceiptRefreshRequest errors out. I am unable to reproduce the error faced by the App Store review board.
While searching the internet to solve the problem, I got to know the following facts about the local receipt-
The local receipt is always present in the production mode. The local receipt may not present if the app is installed using Testflight or during testing. (link)
The App Store server will return the latest subscription information even it it sent an old local receipt (link)
From the above 2 pieces of information, I deduce that there is no need to ever call SKReceiptRefreshRequest in production because the App Store server will provide the latest details even if the local receipt is old and the local receipt is always present in production.
In order to get my app through the App Store review, I have decided to remove the SKReceiptRefreshRequest as it gives errors in the Testflight builds and is not required in the production.
Can anyone confirm if I am correct to do this?
Your logic has multiple flaws:
1) paymentQueue(_:updatedTransactions:) is called in the background and (as far as I know) updates already the local receipt. Also an app downloaded from the App Store always contains the receipt. So there is no need to call SKReceiptRefreshRequest in that method.
2) SKReceiptRefreshRequest requires the users to input his password to allow the receipt refresh. Since you triggered the method within paymentQueue(_:updatedTransactions:), which was called in the background, I reckon this is the problem why the refresh request failed and Apple rejected your app. Nevertheless this method has its reason for being: in production you need it to allow users to restore purchases after reinstalling the app or on other devices and for debug and TestFlight builds you need it to get the latest receipt.
3) You shouldn't send the receipt from your app to Apple's endpoint
Warning
Do not call the App Store server verifyReceipt endpoint from your app. You can't build a trusted connection between a user’s device and the App Store directly, because you don’t control either end of that connection, which makes it susceptible to a man-in-the-middle attack.
Source
How to proceed?
I would recommend to do the following things:
1) Do not trigger SKReceiptRefreshRequest in paymentQueue(_:updatedTransactions:)
2) If not already done provide a "restore purchases" button in your app (which calls SKReceiptRefreshRequest)
3) Implement local or server-to-server receipt validation
I have used the following way to generate the receipt and send the generated receipt to server for verification:
https://developer.apple.com/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store
I am new to iOS programming and new to in-app purchase.
I am using auto renewal subscription in my app, in which after successful subscription I am verifying receipt through which I update my server for services, so basically my problem is that is user uninstall app or change device then if I restore transaction or refresh receipt it prompt for app store credentials, by this I got the receipt but I don't want to do that instead I want to verify receipt without prompt. So, is there any way to verify receipt?
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
If am using this it gives nil on new app installation, also let me know if I send receiptURL data to my server and through which how can I rewrite that data to [NSBundle mainBundle] on new device or new installation.
Also I am using RMStore library.
Thanks in advance.
There's really only one work-around for this. You link the purchase to a user account on your system and store the purchase date. When they launch your app you know if their subscription is active or not. If it's not active in your system then you need to prompt them to renew.
Otherwise there is no way to do this without querying iTunes, which requires them to authenticate.
It's very common, many of the apps I use require me to "validate purchases" from time to time.
In application I need to implement auto-renewable subscriptions purchases.
As I understand documentation, I should check AppStore receipt on app launch and then validate that receipt with my server.
In my code I do following to retrieve receipt:
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
Sometimes receipt is becoming nil.
For example:
I've launched an app and made purchase. After that the code above returns non empty receipt.
App is stopped with Xcode.
On next app launch receipt is nil.
I do not understand, why this happens. receiptURL points to file with receipt, but [NSData dataWithContentsOfURL:receiptURL] returns nil.
If I request receipt refresh, then app shows dialog for entering Itunes credentials. This is not good, because this dialog will appear on app launch and this is unexpected behavior.
Am I doing something wrong?
Or maybe this is sandbox-specific case?
In production there will always be a receipt (even if the app is free), that can contain your additional purchase, in case you have bought something or restored from an already purchased item.
In test environment until you buy something the receipt is nil, if you want to test a case close to production, you should first try a restore (now you will have a receipt) and do what you need.
Here it is suggested that your payment procedure is being hacked, by calling the SKPaymentTransactionStatePurchased regardless. You need to validate the receipt on a server before accepting any receipts.
You can find a great description of receipts and hacks here.
We have issues fully understanding the receipt validation flow in iOS.
Here is what we currently do (in development):
In applicationDidFinishLaunching and in applicationWillEnterForeground we validate the receipt on the server side, if there is no receipt or the receipt is invalid, we try to refresh the receipt and revalidate it.
Here are some issues/questions:
What are the cases where there is no receipt available on the device ?
Should we always issue a receipt refresh request when there is no receipt ?
Why is this alert box sometimes shown on startup ? I understand this is shown on a receipt refresh request ?
When should a receipt verification happen ? We currently do it whenever a purchase is made to verify the purchase, is this correct usage ?
In production a receipt is always available on device. In test after
the first install there is not. So if you want to do a correct test,
you must restore a purchase even if it doesn't exist a purchase on that user in the test
environment. Why is that? App downloaded from the appstore always comes with a receipt even if they are free.
Depends on the business logic you want to apply. If you are validating the receipt against a server each time the use launch the app, of course you need the receipt. If it is not present (but in production is always) or not valid, you can ask for a refresh or restore, but as far as I remember you should always ask the user first if he/she want to do that (can be a reason for reject).
Restore and Refresh are not the same thing.
This usually appear in purchase/restor/refresh. But also If the account has some pending requests because the app has crashed or you interrupted the debugging before the request end somehow, you will be bored by a lot of that. There is no way to flush them programmatically, just login until they stop. Of course it will not be a valid test.
It's up to you and about the kind of purchase. If it is an autorenewable subscription, you can validate the receipt against a server, then store the the "end date" on the client and make another check after the date is expired. Pay attention that receipts can be quite big, because the have also all history values.
As Zhang mentioned, if there is no purchase or restore took place, there will be no receipt in the store
Locate the receipt. If no receipt is found, then the validation fails and you should not request receipt refresh again. Only when you will restore process by yourself, you should request for the receipt again.
This will be shown always when you will try to refresh the receipt (or you will pick from settings that you want not to ask for a password for 15 minutes).
Yes.
For more information, look here:
https://www.objc.io/issues/17-security/receipt-validation/#about-validation
If a user downloaded the app from the App Store – yes, receipt always exists.
However, in sandbox if your app was installed via Xcode or Testflight, then there won’t be a receipt until you make a purchase or restore.
Take a look at this complete FAQ about receipt validation in our blog:
https://blog.apphud.com/receipt-validation/
1.No purchase/Restore took place.
2.Nope.See 1
4.Sure.For consumable products,remember to save hash on your server,in order to defeat replay attack.
I made a game. I didnt expect it to become wildly popular so I didn't care to write complicated cryptography code for receipt validation. My game's IAP (to remove ads) was working perfectly in sandbox mode. The app has been released live for about the last 16 hours and there is a problem. The app is behaving as if everyone has purchased the "remove ads" IAP.
this is my extremely basic receipt validation code:
override init(){
super.init()
// storekit delegation
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
if SKPaymentQueue.canMakePayments() {
let request = SKProductsRequest(productIdentifiers: NSSet(object: self.productID))
request.delegate = self
request.start()
}
self.checkReceipt()
}
func checkReceipt(){
if let url = NSBundle.mainBundle().appStoreReceiptURL {
if let receipt = NSData(contentsOfURL: url) {
self.adsRemoved = true
}
}
}
Questions:
in my sandbox environment.. the receipt variable wouldn't get unwrapped unless I had purchased the IAP. self.adsRemoved wasn't being set to true. It seems like on the app store itself, these variables always get unwrapped. What's up with that? What should i do.
I know now that it's standard for IAP to become active a day or two after app submission. Could this be related to that? I know if I hit my restore button nothing happens. In my sandbox environment it asks for credentials.. shows a success message, etc.
in my sandbox environment.. the receipt variable wouldn't get
unwrapped unless I had purchased the IAP. self.adsRemoved wasn't
being set to true. It seems like on the app store itself, these
variables always get unwrapped. What's up with that? What should i do.
Your checkReceipt() method checks for the presence of the receipt at the URL given by -appStoreReceiptURL.
This is problematic because every app that is downloaded from the App Store will contain a receipt, whether the app is free or paid.
This is documented in Apple's Receipt Validation Programming Guide:
When an application is installed from the App Store, it contains an
application receipt that is cryptographically signed, ensuring that
only Apple can create valid receipts. The receipt is stored inside the
application bundle. Call the appStoreReceiptURL method of the NSBundle
class to locate the receipt.
So why did it work for you in sandbox mode? This is because sandbox apps installed via Xcode contains no receipt, until either of the following occurs:
A receipt is requested via SKReceiptRefreshRequest (link)
When an in-app purchase is made, or
When previous transactions are restored via -restoreCompletedTransactions
So in a nut shell, your checkReceipt method will definitely set adsRemoved to true for anyone who has downloaded your game from the App Store, because you're checking for the PRESENCE of a receipt, which for reasons above, does not mean anything at all to you.
What you need to be doing is validating whether the receipt is (1) legit, and (2) whether it indicates that the correct in-app purchase has been made.
I know now that it's standard for IAP to become active a day or two after app submission. Could this be related to that? I know if I hit
my restore button nothing happens. In my sandbox environment it asks
for credentials.. shows a success message, etc.
Not true that IAP takes a day or two to activate after app approval, and no—the problem occurred because you're checking for the presence of the receipt, and not validating the receipt.
Unfortunately, receipt validation is complex, but necessary if you want to achieve what you want to achieve (i.e. check if an IAP has been made). You'd want to refer to this objc.io article on receipt validation for more information about how to actually validate the receipt. At bare minimum, the steps involved are:
Locate the receipt, which is what you're already doing.
Verify the receipt is legit and not tampered with.
Parse the receipt to figure out the IAP has been made, because a receipt exist even without purchasing the IAP
You have to validate the receipt sending the receipt data for Apple provided url. Later, based on the response you will receive you have to set the value self.adsRemoved = true/false.