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?

britzl
  • 10,132
  • 7
  • 41
  • 38
  • 19
    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. – Ankit Apr 20 '13 at 22:15
  • Do you find any working sample for server side verification? – Dr.jacky Feb 05 '15 at 11:06
  • How to use and extract above json data from google? I want to see actual php code about this. – creator May 24 '15 at 09:16
  • signature verification is done using the public key, not the private key – Gianluca Ghettini Oct 24 '17 at 21:48

5 Answers5

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 !!

Mehul Joisar
  • 15,348
  • 6
  • 48
  • 57
  • 1
    I can't believe I overlooked this. A schoolbook case of RTFM. Thank you! – britzl Apr 17 '13 at 20:14
  • 1
    So how to pass above json data to my php server code and how to use(verify) it from there? – creator May 24 '15 at 14:14
  • 6
    This is not server-side verification – Jemshit May 16 '16 at 08:26
  • You saved my day :) , thanks. For people who can't verify server side should contact me. I successfully verified it server side in php. – Krunal Panchal Aug 06 '16 at 05:23
  • @KrunalPanchal can you please mention how you have implemented server-side verification? – Pinkesh Darji Jan 16 '18 at 10:06
  • I have updated my answer. I hope it is clear to everyone now. Have fun! – Mehul Joisar Jan 17 '18 at 14:37
  • Google recommends to verify signature in a secure server but your code does not verify any signature. The developer should provide this server. – from56 Feb 03 '18 at 19:06
  • @PinkeshDarji https://stackoverflow.com/questions/48392718/android-protecting-in-app-purchases-with-server-side-verification/48531877#48531877 – from56 Feb 03 '18 at 19:07
  • @LluisFelisart please feel free to update the answer and contribute your knowledge on it! – Mehul Joisar Feb 06 '18 at 07:45
  • 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? – user25 May 06 '18 at 11:32
  • 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"}` – Urvashi Soni Jul 06 '23 at 12:41
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 ...

from56
  • 3,976
  • 2
  • 13
  • 23
  • and what is `String data, String signature`? – user25 May 06 '18 at 11:33
  • 1
    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 Bundle – from56 May 06 '18 at 13:32
  • 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? – user25 May 06 '18 at 13:58
  • 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 AsyncTask – from56 May 06 '18 at 14:06
  • do you even verify for already purchased items every time? I mean you always need internet connection for that – user25 May 06 '18 at 16:43
  • Every 8 hours in an AsyncTask I call getPurchases (), if the UrlConnection fails I verify the signatures with java.security. – from56 May 06 '18 at 20:16
  • 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? – user25 May 07 '18 at 18:06
  • is it okay to use this verification on HTTP instead of HTTPS? – user25 May 13 '18 at 07:50
  • 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 9 – from56 Apr 20 '20 at 21:15
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}
Go Goal
  • 81
  • 1
  • 3
  • 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! – Daniele Muscetta Feb 11 '21 at 07:27
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

Oleksii K.
  • 5,359
  • 6
  • 44
  • 72
  • 1
    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. –  Dec 02 '16 at 15:53
  • 1
    How do you validate signature on client side. – Likith Ts Apr 05 '17 at 14:40
  • @Eran can you please post a link to google docs on server side signature validation? – Petr Oct 11 '18 at 10:34
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.