Server-side verification of Google Play In-app billing version 3 purchase
Asked Answered
S

5

40

I'm unable to find a straight answer as to how I verify an in-app billing purchase on the server before making downloadable content available to the user.

I use in app-billing version 3. I purchase managed products using code based on the IabHelper class from the TrivialDrive sample code. Everything is fine and dandy and the purchase is successfully completed, I get a full Purchase object back and the following original JSON data:

{
    "orderId":"12999763169054705758.1364365967744519",
    "packageName":"my package name",
    "productId":"77",
    "purchaseTime":1366217534000,
    "purchaseState":0,
    "purchaseToken":"utfwimslnrrwvglktizikdcd.AO-J1OwZ4l5oXz_3d2SAWAAUgFE3QErKoyIX8WuSEnBW26ntsyDmlLgoUd5lshqIY2p2LnlV4tpH4NITB4mJMX98sCtZizH7wGf6Izw3tfW_GflJDKFyb-g"
}

As I understand it I need to pass the purchaseToken and something I see referred to as a signature to the server. The server then use a private key to verify the purchase. Is this correct? If so, where do I get the signature from and is there really no decent documentation concerning server-side verification of a purchase?

Sukey answered 17/4, 2013 at 18:12 Comment(4)
always remember one thing when you ask some question in a community don't put sensitive data. By the word sensitive I mean passwords, order number of any transactions etc. Just change them with some dummy values.Parallelize
Do you find any working sample for server side verification?Nonappearance
How to use and extract above json data from google? I want to see actual php code about this.Loyce
signature verification is done using the public key, not the private keyKirk
F
22
where do I get the signature from ?

Have a look at official docs,

It says that inside your onActivityResult() method you can get following data as shown in example,

    @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
   if (requestCode == 1001) {           
      int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
      String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
      String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");//this is the signature which you want

      if (resultCode == RESULT_OK) {
         try {
            JSONObject jo = new JSONObject(purchaseData);//this is the JSONObject which you have included in Your Question right now
            String sku = jo.getString("productId");
            String purchaseToken = jo.getString("purchaseToken");
           //you need to send sku and purchaseToken to server for verification
          }
          catch (JSONException e) {
             alert("Failed to parse purchase data.");
             e.printStackTrace();
          }
      }
   }
}

For verification on server end, Have a look at official docs

As mentioned earlier, client app will send sku and purchaseToken to server API. Server will have to receive those values and will have to perform check with android publish api to verify purchase:

Server may call following GET request by adding necessary parameters:

https://www.googleapis.com/androidpublisher/v2/applications/packageName/purchases/products/productId/tokens/token

here,
packageName = packageName of the client app
productId = sku received from client app
token = purchaseToken received from client app

It will result into a JSONObject response as mentioned format:

{
  "kind": "androidpublisher#productPurchase",
  "purchaseTimeMillis": long,
  "purchaseState": integer,
  "consumptionState": integer,
  "developerPayload": string,
  "orderId": string,
  "purchaseType": integer
}

here, purchaseState = 0 means valid purchase

I hope it will be helpful !!

Fredric answered 17/4, 2013 at 18:37 Comment(11)
I can't believe I overlooked this. A schoolbook case of RTFM. Thank you!Sukey
So how to pass above json data to my php server code and how to use(verify) it from there?Loyce
This is not server-side verificationMalraux
You saved my day :) , thanks. For people who can't verify server side should contact me. I successfully verified it server side in php.Mooncalf
@KrunalPanchal can you please mention how you have implemented server-side verification?Phosphorus
I have updated my answer. I hope it is clear to everyone now. Have fun!Fredric
Google recommends to verify signature in a secure server but your code does not verify any signature. The developer should provide this server.Beastings
@PinkeshDarji #48393218Beastings
@LluisFelisart please feel free to update the answer and contribute your knowledge on it!Fredric
from demo example return Security.verifyPurchase(BASE_64_ENCODED_PUBLIC_KEY, signedData, signature); signedData is INAPP_PURCHASE_DATA and signature is INAPP_DATA_SIGNATURE or what?Washday
What does consumptionState signify? To verify a receipt, is it okay to validate both purchaseState & consumptionState or only purchaseState? i.e., if (purchaseState == 0 && consumptionState ==1) { return "valid purchase"}Cannula
B
7

My small contribution to reduce fraud in in-app purchases

Signature verification on an external server, on your Android code :

verifySignatureOnServer()

  private boolean verifySignatureOnServer(String data, String signature) {
        String retFromServer = "";
        URL url;
        HttpsURLConnection urlConnection = null;
        try {
            String urlStr = "https://www.example.com/verify.php?data=" + URLEncoder.encode(data, "UTF-8") + "&signature=" + URLEncoder.encode(signature, "UTF-8");

            url = new URL(urlStr);
            urlConnection = (HttpsURLConnection) url.openConnection();
            InputStream in = urlConnection.getInputStream();
            InputStreamReader inRead = new InputStreamReader(in);
            retFromServer = convertStreamToString(inRead);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }

        return retFromServer.equals("good");
    }

convertStreamToString()

 private static String convertStreamToString(java.io.InputStreamReader is) {
        java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A");
        return s.hasNext() ? s.next() : "";
    }

verify.php on the root directory of web hosting

<?php
// get data param
$data = $_GET['data'];

// get signature param
$signature = $_GET['signature'];

// get key
$key_64 = ".... put here the base64 encoded pub key from google play console , all in one row !! ....";



$key =  "-----BEGIN PUBLIC KEY-----\n".
        chunk_split($key_64, 64,"\n").
       '-----END PUBLIC KEY-----';   
//using PHP to create an RSA key
$key = openssl_get_publickey($key);


// state whether signature is okay or not
$ok = openssl_verify($data, base64_decode($signature), $key, OPENSSL_ALGO_SHA1);
if ($ok == 1) {
    echo "good";
} elseif ($ok == 0) {
    echo "bad";
} else {
    die ("fault, error checking signature");
}

// free the key from memory
openssl_free_key($key);

?>

NOTES:

  • You should encrypt the URL in your java code, if not the URL can be found easy with a simple text search in your decompressed app apk

  • Also better to change php file name, url arguments, good/bad reponses to something with no sense.

  • verifySignatureOnServer() should be run in a separated thread if not a network on main thread exception will be thrown.

Hope it will help ...

Beastings answered 6/2, 2018 at 14:22 Comment(9)
and what is String data, String signature?Washday
getPurchases() return a Bundle of owned items, data & signature are respectively members of "INAPP_PURCHASE_DATA_LIST" key & "INAPP_DATA_SIGNATURE_LIST" key on the BundleBeastings
so we should verify it only onActivityResult? String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA"); String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE"); p.s. there is no _LIST at the end but I guess it's still the same?Washday
Yes, you are right, onActivityResult you get the last purchase data & signature, only one. On getPurchases() you get a list of all purchased items and in this case the verification should be called on each one item. But there's a problem in onActivityResult due the main UI thread can't access network and can't call the external server, What I do is execute in an AsyncTaskBeastings
do you even verify for already purchased items every time? I mean you always need internet connection for thatWashday
Every 8 hours in an AsyncTask I call getPurchases (), if the UrlConnection fails I verify the signatures with java.security.Beastings
aren't we supposed to call getPurchases() every time our activity created? we get list of purchases user has and disable/enable some function based on this information, and I guess later we can call some additional thread if there is internet is on to verify if those purchases were really made, if they aren't real than disable function we previous enabled at app start. Aren't we supposed to use this logic for already purchased items?Washday
is it okay to use this verification on HTTP instead of HTTPS?Washday
Is far better to use Https, in addition on Android 9 and newer cleartext traffic is not permitted, then any code that uses plain text Http will stop to work on Android 9Beastings
G
4

1.Install Google client PHP libary with composer

composer require google/apiclient:"^2.0"
  1. Create service account and it will give you json and saved as credentials.json

enter image description here

  1. start code

    require $_SERVER['DOCUMENT_ROOT'] . '/client/vendor/autoload.php';
    
    $packageName='Your Package name'; //com.example.blahblah
    $productId='your_product_ID';
    $token='Purchase_Token_Form_Payment';
    
    $client = new \Google_Client();
    $client->setAuthConfig('credentials.json');
    $client->addScope('https://www.googleapis.com/auth/androidpublisher');
    
    $service = new \Google_Service_AndroidPublisher($client);
    $purchase = $service->purchases_products->get($packageName, $productId, $token);
    
    //echo $purchase['purchaseState'];
    //echo '<br>';
    echo json_encode($purchase);
    

result will be like this

{"acknowledgementState":1,"consumptionState":1,"developerPayload":"","kind":"androidpublisher#productPurchase","orderId":"GPA.3342-8146-5668-57982","productId":null,"purchaseState":0,"purchaseTimeMillis":"1586978561493","purchaseToken":null,"purchaseType":0,"quantity":null}
Gibberish answered 16/4, 2020 at 4:23 Comment(1)
Hi there, I like the approach with the official library to take care of auth. Question: how does the service account need to look like, i.e. what permissions does it need to be granted and where? I thought I had seen it in Google docs but I can't find it back... Thanks much!Maroon
H
3

This is too old question, but I hope my answer can help somebody.

You have to validate signature on client side, and then you have to pass purchaseToken to server side, and then server will contact Google's server and get all necessary information about purchase, such as purchaseState and consumptionState.

https://developers.google.com/android-publisher/api-ref/purchases/products

Helpful answered 30/3, 2016 at 10:14 Comment(3)
This is partially correct. Google actually recommends to perform signature validation on the server side when possible. This way you don't expose your public key in the android app.Jugular
How do you validate signature on client side.Dactylic
@Eran can you please post a link to google docs on server side signature validation?Smegma
C
0

This is very old question but I think it is still relevant.

My small contribution. Purchase object is extended for one new parameter "acknowledged" and now it looks:

{
  "orderId":"12999763169054705758.1364365967744519",
  "packageName":"my package name",
  "productId":"77",
  "purchaseTime":1366217534000,
  "purchaseState":0,
  "purchaseToken":"utfwimslnrrwvglktizikdcd.AO-J1OwZ4l5oXz_3d2SAWAAUgFE3QErKoyIX8WuSEnBW26ntsyDmlLgoUd5lshqIY2p2LnlV4tpH4NITB4mJMX98sCtZizH7wGf6Izw3tfW_GflJDKFyb-g",
  "acknowledged":true
}

So be careful when check signature to add this one extra parameter.

Chatty answered 6/5, 2019 at 22:39 Comment(2)
Does it not contain autoRenewing field?Newt
@KishanVaishnav maybe just for subscriptions not one-time products?Maroon

© 2022 - 2024 — McMap. All rights reserved.