Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
Bartmax opened this issue Mar 17, 2017 · 8 comments

Comments

@Bartmax
Copy link

Bartmax commented Mar 17, 2017

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);

@Bartmax Bartmax changed the title BUG: No verification with remote server of local receipt on iOS. BUG: No verification with remote server of local receipt on iOS when calling GetPurchasesAsync. Mar 17, 2017
@jamesmontemagno
Copy link
Owner

How would you normally do this in iOS?

Do you have apple docs on this?

@Bartmax
Copy link
Author

Bartmax commented Mar 17, 2017

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
Copy link
Owner

jamesmontemagno commented Mar 18, 2017

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
Copy link
Author

Bartmax commented Mar 18, 2017

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
Copy link
Owner

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
Copy link
Author

Bartmax commented Mar 18, 2017

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
Copy link

jonathanpeppers commented Mar 20, 2017

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
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants