j3k0 / cordova-plugin-purchase

In-App Purchase for Cordova on iOS, Android and Windows
https://purchase.cordova.fovea.cc
1.29k stars 535 forks source link

Whats the validation response JSON format? #210

Closed rwillett closed 9 years ago

rwillett commented 9 years ago

I feel bad raising two issues in a day so apologies.

We're trying to implement a store validator on our web server. We've set the validator URL correctly and can see data (a lot of it) come in into our web server. At the moment all we return from our web server is a status of 200 and a text field of "OK". We are not doing any processing at the moment on the data thats come in but simply trying to get the hand shaking working.

We have previously set store.validator to the right URL

Our logic is that wait for the "Approved" from the store which we get, we then call product.verify(). and can we see data coming into the Webserver.

        }).approved(function(product) {
            ConsoleLog("Store: Approved: " + JSON.stringify(product));

            product.verify().success(function (product , purchaseData) {
                ConsoleLog("Verified - Success: " + JSON.stringify(product));
            }).error(function(err) {
                ConsoleLog("Verified - Error: " + JSON.stringify(err));
    }).done(function(product) {
                ConsoleLog("Verified - Done: " + JSON.stringify(product));
            });

We have product.verify().success().error().done() set as shown above.

However we are unclear as to what needs it be sent back from the web server indicating a success or failure. We assume its a JSON response but we can't see anything in the docs nor in any of the issues that gives the JSON format.

[store.js] WARNING: ajax -> request to http://XXXX.XXXX.XXXXX/check-purchase failed with an exception: JSON Parse error: Unexpected identifier "ok"

Any pointers, links gratefully received.

Thanks,

Rob

j3k0 commented 9 years ago

@rafaellop

{ "ok": true, "data": { ... } }

https://github.com/j3k0/cordova-plugin-purchase/blob/master/src/js/validator.js#L60-L71

@rwillett error you have appears to be that your server doesn't reply with valid JSON in the body.

rwillett commented 9 years ago

The server doesn't reply with valid JOSN as we didn't know what it was. It only uses the HTTP status code at the moment.

We will use { "OK" : true , "data": { ....} }.

Thanks

Eob

rafaellop commented 9 years ago

@j3k0 Thanks. In the case of an error should { "ok": false } be passed or rather { "error": "error message" } or maybe { "ok": false, "data": { "error": "error message" } } ? Is there any special use for the "data" field? Would it be ok not to pass it at all?

j3k0 commented 9 years ago

data is passed back to the success promise of the verify() function, in case you need it for something.

in case of an error:

{
  "ok": false,
  "data": {
    "code": 6778003,
    "error": {
      "message": "blah blah"
    }
  }
}

(codes documented here: https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#validation-error-codes)

For now this is all mostly undocumented, because I think it still needs some cleanup... so it may change in the next major update of the plugin.

rafaellop commented 9 years ago

Hi @j3k0

Thanks for the help. However, I've got some troubles with the IAP purchase flow. It occurs that my app is trying to connect to my receipt verification backend each time the app is started. It doesn't matter the product was finished. If I reopen my app the product goes the same path again. Is it ok? And one important thing. The product I refer to is non-consumable and it is of course registered as this type.

My flow is:

init store (pseudocode):

store.validator = url;
store.register
store.when("product").approved -> p.verify()
store.when("product").verified -> p.finish()
store.when("product").unverified -> log(error)
store.when("product").updated -> if (p.state === store.OWNED) then unlockfeature()

The init store is called each time the app is started.

The purchase is just a call to the store.order and the event handlers defined in the init are fired. I can see that in the log and in the app. However, as far as I undrstand, the remote verification should be called only once or more if product is not finished.

Could you please advice?

Cheers, Rafal

j3k0 commented 9 years ago

Re-checking with your server each time is the way to go.

If not, one could hack into the local storage of the app so it believes validation was done on a previous run... so that validation never actually run.

To play smart and not re-validate with Google/Apple servers each time, your server should store a hash of the already checked receipts + store's response.

Getting server-side receipt validation right will be a sum of small details... You should also consider protecting it against replay-type attacks... and so on. (I'll let you google)

rafaellop commented 9 years ago

Yes, verifiying each run is the safest way, but the app is often used offline. I'd say 90%. Would it be possible to omit the verification if the product is in owned state? Or is it maybe switched to this state only when verified remotely (each time)? If not, then in that case I will still need a local storage option for saving purchased state to make the app usable in the offline mode.

I don't need very sophisticated protections. Just a simple one time verification would be enough. I can also limit remote google checks for already verified receipts (that should also help with the replay-type attacs), but this is not the trouble here. The issue is that the app works mostly offline (except the a purchase is done).

Cheers, Rafal

j3k0 commented 9 years ago

When offline, contacting the store will fail. The plugin won't succeed in initializing. No callback will be called, including the validation ones...

For offline support, you can use your own localStored isFeatureUnlocked.

rafaellop commented 9 years ago

Isn't the plugin contacting the local google service to get a cached store state with all owned items?

rwillett commented 9 years ago

Just to confirm that we have the server validation working. Well all it returns success which is enough for the moment.

Also just to pick up on previous comments in the thread from rafaellop regarding validation.

Our thinking was that since we have a Javascript based hybrid application, that going overboard on security simply isn't worth the effort. No matter what we do, somebody can get access to the JavaScript eventually and modify the JavaScript itself to get around whatever validation we put in place. It then becomes a cat and mouse games as to who wins. If its a case of breaking Apples PKCS7 transaction receipts of rooting the device and hacking the JavaScript, I know which way I'd go. I'm not advocating no security, but there's no value in building the worlds strongest PKI system and then running it on JavaScript.

I'd be interested in other peoples views.

Rob

j3k0 commented 9 years ago

I think there's a difference.

Tools like LocaliAPstore, iAPfree, iAP cracker will unlock purchases on all apps of the device, except for those with proper server side validation mechanisms.

If modifying the JS is necessary:

That makes the work a bit harder for the hacker... and for the consumer to find a modified version of your app (probably lagging behind for the latest updates).

Not to have to modify the JS (and if your app is worth the effort), a hacker can also resolve your validation server to one of their own (or even the phone itself), where they will proxy the requests to your own server, but with a valid receipt (paid for by some guy, once only), so you unlock the feature. Solution is then to detect and blacklist the suspicious "replayed" receipts server side.

With JS, if the feature's logic is client side, there's always gonna be a way for hackers to change your code and unlock it. You can only make it harder for the basic jailbreaker to access the content / feature.

If the feature's logic is server side, then you can protect yourself with proper server-side validation.

rwillett commented 9 years ago

Thanks for the considered reply.

I don't consider people hacking the app and publishing a new bundle that likely. As you say, we can modify the app, update things so that people are always playing catchup, thats what I mean by cat and mouse. We can go so far, but we end up possibly making life so difficult for the potential hacker that we lose the actual paying customer. Its always a balance and we have all experience of companies who make products that can be almost impossible to use because of their 'protections'.

What I was trying to say (inelegantly) is that Apples PKCS7 encryption of the transaction receipt is more difficult to break than some of the other protections around it and spending a lot of effort defending the receipt itself rather than the surrounding logic is a waste of time. I cannot see anybody breaking Apples receipt encryption, but as you say, I can see man in the middle attacks or replaying of transaction receipts or something else we hadn't thought of.

I am certainly not disagreeing with putting validations server side but stating that we should have proportionate responses to security.

We have functions on the client side that validates that the user is allowed to access some feature, it could be new levels, new functionality or a subscription. This function is a JavaScript call to a server validation routine and returns true or false. At the end of the day no matter how many layers of code we put down, we come down to an equality test somewhere :)

We can ensure that the transaction receipt on the server is valid, we can encrypt all the comms between server and client in SSL, we can uglify, scramble and minimise the JavaScript but the weak point here is (normally) the JavaScript as somebody will be able to get hold of the validation function and modify the return value to be a valid result even if the input was a fake request. We could make it difficult by having lots of traffic going across the network checking every so often but we're into making it difficult and my view is that adding obfuscated complexity is not the same as adding security.

Other options include encrypting the JavaScript, but that's a home made solution that I don't particularly like, we could also download the latest version of the JavaScript or portions of the application from the server every so often, but thats potentially a high bandwidth solution and requires the servers to be online, its also more things to go wrong. We've seen the Cloud Functions as advocated by some people but that looks just like an Ajax call with some nice sugar around it.

If there is a way to genuinely provide secure validation for the client app using server side validation, I'd be happy to talk further offline.

Best wishes,

Rob

j3k0 commented 9 years ago

Totally agree. And in my opinion most apps' main target customers don't bother jailbreaking their phone anyway. The protections only protects from users that wouldn't even be paying in the first place.

If that's needed, the only way I think exists to make your IAP "fully secure" (if that even exists) is by making the server necessary by design.

Online games or apps for which large part of the logic is server side can implement real protection.

Pay to contact another user of the app (eg. for a dating app), if only the server has the list of users and ability to send message. Pay to search within an history (being stored on the server, not locally). ...

Eventually, if you can implement a key part of the paid feature server side, then it becomes pretty complicated to break, whatever you change client side.

rwillett commented 9 years ago

I think we're on the same page here. Thanks for the response. I'll close the question down

Rob