3

I want to verify an Android IAP via Google's API on my central game server.

There is a lot of partial information about this and it is blowing my mind. I have not paid €25 to become a Google Developer, because I am not sure if I will be able to get it to work.

When an IAP is made, a JSON object is returned. This object contains several fields, like the purchaseToken and the productId (source).

I found that you can request information about a bought product via the following GET request: GET https://www.googleapis.com/androidpublisher/v2/applications/packageName/purchases/products/productId/tokens/token.

I could program this no problem, but you need to authorize yourself: "This request requires authorization with the following scope" (source). This is where I started getting confused.

  1. You need to create some sort of login token via the Dev Console (Link). I don't know what type. OAuth or service account?
  2. This token is short lived. You need to refresh it

There are several huge code snippets to be found on the internet that may or may not work, but they are all partial and not very well documented.

I found Googles API library for Java: link. This API seems to be made to fix all these problems with OAuth and tokens for you. However, I am unable to figure out how to get this API to work.

It is probably not that hard, but there are a lot of different ways to do it, and I can't find any clear examples.

TL;DR: I need to verify a Google Play IAP serverside. To do this, I want to use Googles Java API.

EDIT: THIS MIGHT BE A WAY SIMPLER SOLUTION. Passing the original JSON plus the JSON to the server might be way easier, because I could just verify the asymmetric signature server side.

kwantuM
  • 512
  • 6
  • 20
  • This should help: https://stackoverflow.com/questions/35127086/android-inapp-purchase-receipt-validation-google-play/35138885#35138885 – Marc Greenstock Oct 14 '16 at 16:16
  • I've tried to follow it, but their API UI has changed COMPLETELY. And it does not include any solution to the token refreshing problem. – kwantuM Oct 14 '16 at 16:18

2 Answers2

3

I have done that in Scala, but using the Java standard Library. it should be simple to convert that code to Java I believe. The main advantage of this implementation is that it contains zero dependencies on Google's libraries.

  • First of all, you need a service account. You can create that via the Google Dev console. It basically gives you back a generated email account that you will use to authenticate your backend service and generate the tokens.

  • With that account created, you are prompt to download your private key. You need that in order to sign the JWT.

  • You have to generate a JWT in the format Google specifies (I show you how in the code below). See: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatingjwt

  • then, with the JWT, you can request the access token

  • With the access token, you can make requests to validate your purchases

/** Generate JWT(JSON Web Token) to request access token
* How to generate JWT: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatingjwt
*
* If we need to generate a new Service Account in the Google Developer Console,
* we are going to receive a .p12 file as the private key. We need to convert it to .der.
* That way the standard Java library can handle that.
*
* Covert the .p12 file to .pem with the following command:
* openssl pkcs12 -in <FILENAME>.p12 -out <FILENAME>.pem -nodes
*
* Convert the .pem file to .der with the following command:
* openssl pkcs8 -topk8 -inform PEM -outform DER -in <FILENAME>.pem -out <FILENAME>.der -nocrypt
*
* */
private def generateJWT(): String = {

  // Generating the Header
  val header = Json.obj("alg" -> "RS256", "typ" -> "JWT").toString()

  // Generating the Claim Set
  val currentDate = DateTime.now(DateTimeZone.UTC)
  val claimSet =Json.obj(
    "iss" -> "<YOUR_SERVICE_ACCOUNT_EMAIL>",
    "scope" -> "https://www.googleapis.com/auth/androidpublisher",
    "aud" -> "https://www.googleapis.com/oauth2/v4/token",
    "exp" -> currentDate.plusMinutes(5).getMillis / 1000,
    "iat" -> currentDate.getMillis / 1000
  ).toString()

  // Base64URL encoded body
  val encodedHeader = Base64.getEncoder.encodeToString(header.getBytes(StandardCharsets.UTF_8))
  val encodedClaimSet = Base64.getEncoder.encodeToString(claimSet.getBytes(StandardCharsets.UTF_8))

  // use header and claim set as input for signature in the following format:
  // {Base64url encoded JSON header}.{Base64url encoded JSON claim set}
  val jwtSignatureInput = s"$encodedHeader.$encodedClaimSet"
  // use the private key generated by Google Developer console to sign the content. 
  // Maybe cache this content to avoid unnecessary round-trips to the disk.
  val keyFile = Paths.get("<path_to_google_play_store_api.der>");
  val keyBytes = Files.readAllBytes(keyFile);

  val keyFactory = KeyFactory.getInstance("RSA")
  val keySpec = new PKCS8EncodedKeySpec(keyBytes)
  val privateKey = keyFactory.generatePrivate(keySpec)

  // Sign payload using the private key
  val sign = Signature.getInstance("SHA256withRSA")
  sign.initSign(privateKey)
  sign.update(jwtSignatureInput.getBytes(StandardCharsets.UTF_8))
  val signatureByteArray = sign.sign()
  val signature = Base64.getEncoder.encodeToString(signatureByteArray)

  // Generate the JWT in the following format:
  // {Base64url encoded JSON header}.{Base64url encoded JSON claim set}.{Base64url encoded signature}
  s"$encodedHeader.$encodedClaimSet.$signature"
}

Now that you have the JWT generated, you can ask for the access token like that:

/** Request the Google Play access token */
private def getAccessToken(): Future[String] = {

  ws.url("https://www.googleapis.com/oauth2/v4/token")
    .withHeaders("Content-Type" -> "application/x-www-form-urlencoded")
    .post(
      Map(
        "grant_type" -> Seq("urn:ietf:params:oauth:grant-type:jwt-bearer"),
        "assertion" -> Seq(generateJWT()))
    ).map {
    response =>
      try {
        (response.json \ "access_token").as[String]
      } catch {
        case ex: Exception => throw new IllegalArgumentException("GooglePlayAPI - Invalid response: ", ex)
      }
  }

}

With the access token on your hands, you are free to validate your purchases.

I Hope that helps.

Bruno Paulino
  • 5,611
  • 1
  • 41
  • 40
1

Following on from my comment above, the Google API manager has changed in design but the procedure is still the same. You want to create a Service Account, once created you can download the JSON Web token, it's a simple JSON file with the credentials you need to authenticate. The service account should have all access you need to access the Google Play Developer API. You will need to grant access to Finance in the Google Play Developer Console > Settings > API Access page.

You can use the Google APIs Client Library for Java to authenticate and make requests to the Google Play Developer API. Follow the OAuth2 Service accounts docs to get you set up. There is a note on Data store that describes how the refresh token is handled.

There are also docs on how to use the Google Play Developer API Client Library for Java.

As for validating the signature of the purchase JSON, there is a INAPP_DATA_SIGNATURE in the response payload of the purchase intent. See the docs on Purchasing an Item for more information on how to get it. You can verify the INAPP_PURCHASE_DATA by base64 decoding the signature and verifying it with your licence key, found in the Google Play Developer Console > All Applications > [App name] > Services & APIs. Ensure the INAPP_PURCHASE_DATA is intact otherwise you will end up with this problem.

Hope this helps.

Marc Greenstock
  • 11,278
  • 4
  • 30
  • 54