flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
163.17k stars 26.86k forks source link

[in_app_purchase] Support InApp purchase local verification #52522

Open rahulraj64 opened 4 years ago

rahulraj64 commented 4 years ago

InApp purchase plugin should support local/offline purchase verification Android Purchase Verification: https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device iOS Receipt Validation: https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html

This is not only a security enhancement but also a functional bug fix. I can explain why. In Android, when we purchase an auto-renewing subscription, the plugin returns that subscription when we query the past purchases and when the subscription is expired or the user cancels it, the plugin will not return the purchase when we query it. So in Android, this works as expected. But in iOS there is a behaviour change when the subscription is expired. The plugin still returns the purchase when we query the past purchase, which is unexpected in the perspective of the plugin user, especially if he successfully implemented the Android part and according to the user, this is a bug in the plugin.

This may be a problem of the iOS ecosystem that the validity of the receipt may be checked locally in order to determine whether the subscription is active or not. But since this is a plugin, it should abstract away all of the complexities like this and provide idiomatic APIs to handle the situation like this.

So the plugin should provide some basic offline verification of the purchase for security and functional consistency.

Tested in in_app_purchase: 0.2.0+7

LHLL commented 4 years ago

Please see Apple document for restoring transaction part, quote here: You can sync and restore non-consumables and auto-renewable subscriptions across devices using StoreKit. When a user purchases an auto-renewable or non-renewing subscription, your app is responsible for making it available across all the user's devices, and for enabling users to restore past purchases If your app is selling subscription on iOS, it doesn't matter whether or not a subscription is expired, as long as it's a valid purchase, Apple will restore this transaction for you.

Local receipt validation only provides:

  1. If a receipt is signed by Apple.
  2. If a receipt belongs to this device.
  3. If a receipt belongs to this app.

Comparing system time with receipt locally to determine whether or not a subscription is expired will never work, since user can adjust iOS system time manually.

rahulraj64 commented 4 years ago

Hi @LHLL Thanks for your input. I understand that it is not safe to validate the receipt locally. But what about providing a minimal support for parsing the receipts and providing the human readable properties to the developer? Some fields like (https://developer.apple.com/documentation/appstorereceipts/responsebody/latest_receipt_info). And let the developers take a decision on whether they wanted to trust the receipt or not (say, if there is no connectivity, use offline verification, otherwise online verification, depending on their use case. In my case, for auto renewable subscription, if the user adjust the system clock while offline, its okay since they are viewing irrelevant content inside app). This helps to create apps with full offline support. Please correct me if I am wrong.

odonc commented 3 years ago

Hi @LHLL Thanks for your input. I understand that it is not safe to validate the receipt locally. But what about providing a minimal support for parsing the receipts and providing the human readable properties to the developer? Some fields like (https://developer.apple.com/documentation/appstorereceipts/responsebody/latest_receipt_info). And let the developers take a decision on whether they wanted to trust the receipt or not (say, if there is no connectivity, use offline verification, otherwise online verification, depending on their use case. In my case, for auto renewable subscription, if the user adjust the system clock while offline, its okay since they are viewing irrelevant content inside app). This helps to create apps with full offline support. Please correct me if I am wrong.

Agree with this, let us take on the risk! Something to parse the receipt would be great, I can't seem to work out how to parse that ANS1 string.

renefloor commented 3 years ago

I'm not sure if I agree that local verification should be supported.

First of all:

say, if there is no connectivity, use offline verification, otherwise online verification

To have an up to date receipt you need a network connection. So the offline verification will be as good as the last verification when the device was online. At that moment online verification was possible. This sounds like you would want a way to store the result of the online verification. I would recommend using flutter_secure_storage for that.

When verifying the receipt on the device Apple recommends (among other things) to

I don't see enough benefits and to many risks implementing local verification. I think it might be better to create an example cloud functions app that does this online.

I can recommend this video on local and server receipt verification: https://developer.apple.com/videos/play/wwdc2017/305

ludwiktrammer commented 3 years ago

My use case is as simple as this:

queryProductDetails when used with subscriptions returns only active (not expired) subscriptions on Android, but returns all (even expired) subscriptions on iOS. I want to perform server verification, but I would like to be able to filter out the expired purchases first, locally, and only send the active subscription for the online verification. I don't see a way to do this currently.

renefloor commented 3 years ago

Seeing the use cases this sounds like a duplicate of #43720

renefloor commented 3 years ago

I had a thorough look into receipt verification. I think it is possible to do in pure dart, but it would be really nice if somebody with more asn1 decoding experience can give a helping hand.

This is a gist of some example code that prints all the asn1 objects in the receipt: https://gist.github.com/renefloor/9be7aecec1005779b3878e008b335d64

You can use the example receipt in this online parser to see how the structure actually is.

There are 2 element tags that are not recognised by either parser, 160 / 0xA0 and 163 / 0xA3, but I chose to just parse the bytes in those objects.

This is a really useful example in Swift which helps understanding how things work: https://github.com/roop/AppStoreReceiptDecoder/blob/master/AppStoreReceipt.swift https://github.com/roop/AppStoreReceiptDecoder/blob/master/main.swift

The official documentation is really technical, but at least these two pages are really helpful in combination with the swift example: https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW19 https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1

espbee commented 2 years ago

@renefloor thanks for this info. have you been doing such validation / come to any new conclusions ?

renefloor commented 2 years ago

@espbee my conclusion was that it's not really recommended to do validation on the device and it's way easier to setup a simple backend, for example using firebase functions. In the codelab we wrote we also covered this: https://codelabs.developers.google.com/codelabs/flutter-in-app-purchases

I didn't do any further research on the local verification.

flutter-triage-bot[bot] commented 7 months ago

This issue is assigned to @LHLL but has had no recent status updates. Please consider unassigning this issue if it is not going to be addressed in the near future. This allows people to have a clearer picture of what work is actually planned. Thanks!

flutter-triage-bot[bot] commented 5 months ago

This issue was assigned to @LHLL but has had no status updates in a long time. To remove any ambiguity about whether the issue is being worked on, the assignee was removed.