jamesmontemagno / InAppBillingPlugin

Cross-platform In App Billing Plugin for .NET
MIT License
651 stars 152 forks source link

BUG: No verification with remote server of local receipt on iOS when calling GetPurchasesAsync. #28

Closed Bartmax closed 7 years ago

Bartmax commented 7 years ago

Bug

When calling GetPurchasesAsync the receipt from the local device is not verified against the remote server.

https://github.com/jamesmontemagno/InAppBillingPlugin/blob/master/src/Plugin.InAppBilling.iOS/InAppBillingImplementation.cs#L85

Version Number of Plugin: Device Tested On: Simulator Tested On:

Expected Behavior

Call to VerifyPurchase

Actual Behavior

No call to VerifyPurchase

Steps to reproduce the Behavior

await CrossInAppBilling.Current.GetPurchasesAsync(ItemType.Subscription, inAppBillingVerifyPurchase);
jamesmontemagno commented 7 years ago

How would you normally do this in iOS?

Do you have apple docs on this?

Bartmax commented 7 years ago

Well, I'm not sure the apple docs are very clear. And if the restore is the right place for this. https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/Restoring.html

Let's say I validate purchases remotely.

  1. User installs client app.
  2. Buy renewable subscription.
  3. Validate receipt with remote server.
  4. Use service
  5. User goes to another device with same account
  6. Restore purchases
  7. Use service

What I see missing, it's a step after 6 to Validate the receipt with the remote server. As you can see, a new device doing restore purchases can get access to app content without asking the remote server at all.

In my specific case-scenario, I give the client an access token if the receipt is valid, so when it goes to another device and call restore purchases. I have no instance to exchange the access token with the receipt.

Also (not this issue, but somehow related), in my specific case, if the client doesn't have a valid token anymore (this may happen for several reasons) I need to validate the receipt with my server again. This means I don't need a restore process, only a way to revalidate the current receipt again.

To solve both this issues, I forked the project and made this adjustments to the InAppBillingImplementation for iOS

public async Task<IEnumerable<InAppBillingPurchase>> GetPurchasesAsync(
                  ItemType itemType, 
                  IInAppBillingVerifyPurchase verifyPurchase = null)
{
    var purchases = await RestoreAsync();
    var validated = await ValidateReceipt(verifyPurchase);
    return validated 
               ? purchases.Where(p => p != null).Select(p => p.ToIABPurchase()) 
               : null;
}

public Task<bool> ValidateReceipt(IInAppBillingVerifyPurchase verifyPurchase)
{
    if (verifyPurchase == null) return Task.FromResult(false);
    // Get the receipt data for (server-side) validation.
    // See: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573
    var receiptUrl = NSData.FromUrl(NSBundle.MainBundle.AppStoreReceiptUrl);
    string receipt = receiptUrl.GetBase64EncodedString(NSDataBase64EncodingOptions.None);
    return verifyPurchase.VerifyPurchase(receipt, string.Empty);
}

Hope this makes sense. If you see that I'm thinking/doing this wrong or need more information please let me know. I'm doing iOS first and will implement Android and UWP in the following days.

sidenote: I can spot another issue with the auto renewal process on this library but I will open a new issue when i have sorted it out.

jamesmontemagno commented 7 years ago

So, I think I would need to do this:

 public async Task<IEnumerable<InAppBillingPurchase>> GetPurchasesAsync(ItemType itemType, IInAppBillingVerifyPurchase verifyPurchase = null)
        {
            var purchases = await RestoreAsync();

            var converted = purchases.Where(p => p != null).Select(p => p.ToIABPurchase());

            var items = new List<InAppBillingPurchase>();
            foreach (var purchase in converted)
            {
                var validated = await ValidateReceipt(verifyPurchase, purchase.ProductId, purchase.Id);
                if (validated)
                    items.Add(purchase);
            }

            return items;
        }

As you would need to validate each purchase... I would assume

Bartmax commented 7 years ago

I don't think there's a way to validate the receipt for each individual purchase separately. There's only one receipt with all transactions that can be validated remotely as far as I know, (i might be wrong here!) so I think the code should be more like what I shown:

When you have N purchases, validate the only receipt you have and if it's valid, all purchases are valid, else treat all as not valid.

jamesmontemagno commented 7 years ago

ahhh got it... so does it need the transaction id and the product id? I was reading through http://jonathanpeppers.com/Blog/securing-in-app-purchases-for-xamarin-with-azure-functions

maybe @jonathanpeppers has an idea?

Bartmax commented 7 years ago

Nope, no transactionId nor productId are required beside the receipt. Everything is contained inside the receipt. I don't know why @jonathanpeppers is doing all the checks after this line

if (result.Status == AppleStatus.Success)

at that point you are dealing with your server and apple's. Unless you don't trust the connection between your server and apple, the success from them should be more than enough, but that is server validation not related to this particular library.

:trollface: Now I see where you got the receipt processing for only user initiated purchases. His method have the same issue.

jonathanpeppers commented 7 years ago

Hi @Bartmax

This code was ported from a real app, so there is some stuff in there that could be removed for the simplest case.

I would recommend verifying the app bundle id, purchase id, and that the transaction id is unique so that:

  1. A valid receipt for another app, or another purchase cannot be used
  2. Valid receipts can only be used once

A simple way to do this is to just hardcode accepted bundle ids/purchase ids on your server.

But if you are just trying to make it a little harder to hack, you can only use the receipt data and simplify my example a bit.

jamesmontemagno commented 7 years ago

I am now propagating up the ids of transaction and product id so that will help perhaps and then bundle id the dev can grab in their source code